@seanyao/roll 2026.518.4 → 2026.519.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +26 -14
- package/bin/roll +683 -81
- package/conventions/global/AGENTS.md +6 -6
- package/conventions/templates/backend-service/AGENTS.md +4 -4
- package/conventions/templates/cli/AGENTS.md +4 -4
- package/conventions/templates/frontend-only/AGENTS.md +4 -4
- package/conventions/templates/fullstack/AGENTS.md +4 -4
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/roll-backlog.py +3 -3
- package/lib/roll-brief.py +2 -2
- package/lib/roll-help.py +3 -3
- package/lib/roll-home.py +28 -8
- package/lib/roll-init.py +101 -0
- package/lib/roll-loop-status.py +24 -8
- package/lib/roll-plan-validate.py +221 -0
- package/lib/roll-status.py +4 -4
- package/package.json +1 -1
- package/skills/roll-.changelog/SKILL.md +13 -13
- package/skills/roll-.dream/SKILL.md +21 -21
- package/skills/roll-brief/SKILL.md +12 -12
- package/skills/roll-build/SKILL.md +15 -15
- package/skills/roll-debug/SKILL.md +1 -1
- package/skills/roll-design/SKILL.md +46 -46
- package/skills/roll-doc/SKILL.md +10 -10
- package/skills/roll-fix/SKILL.md +7 -7
- package/skills/roll-idea/SKILL.md +4 -4
- package/skills/roll-loop/SKILL.md +11 -11
- package/skills/roll-onboard/SKILL.md +143 -0
- package/skills/roll-peer/SKILL.md +1 -1
- package/skills/roll-propose/SKILL.md +13 -13
- package/skills/roll-sentinel/SKILL.md +1 -1
- package/template/AGENTS.md +2 -2
- /package/template/{BACKLOG.md → .roll/backlog.md} +0 -0
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.519.2"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -913,15 +913,39 @@ _merge_claude_to_project() {
|
|
|
913
913
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
914
914
|
# COMMAND: init
|
|
915
915
|
# Initialize or re-merge a project. Always operates on the current directory.
|
|
916
|
-
# Fresh project: creates AGENTS.md +
|
|
916
|
+
# Fresh project: creates AGENTS.md + .roll/backlog.md + .roll/features/
|
|
917
917
|
# Existing AGENTS.md: re-merges global conventions (section-level, non-destructive)
|
|
918
918
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
919
919
|
cmd_init() {
|
|
920
|
+
# US-VIEW-008: parse --demo before any side effects so the v2 renderer can
|
|
921
|
+
# run standalone (no templates, no project mutation).
|
|
922
|
+
local demo=false
|
|
923
|
+
local args=()
|
|
924
|
+
while [[ $# -gt 0 ]]; do
|
|
925
|
+
case "$1" in
|
|
926
|
+
--demo) demo=true; shift ;;
|
|
927
|
+
*) args+=("$1"); shift ;;
|
|
928
|
+
esac
|
|
929
|
+
done
|
|
930
|
+
set -- "${args[@]:-}"
|
|
931
|
+
|
|
932
|
+
if [[ "${ROLL_UI:-v2}" == "v2" ]] || [[ "$demo" == "true" ]]; then
|
|
933
|
+
python3 "${ROLL_PKG_DIR}/lib/roll-init.py" --demo
|
|
934
|
+
if [[ "$demo" == "true" ]]; then return; fi
|
|
935
|
+
fi
|
|
936
|
+
|
|
920
937
|
if [[ ! -d "$ROLL_TEMPLATES" ]]; then
|
|
921
938
|
err "No templates found. Run 'roll setup' first. 未找到模板,请先运行 'roll setup'。"
|
|
922
939
|
exit 1
|
|
923
940
|
fi
|
|
924
941
|
|
|
942
|
+
# US-ONBOARD-009: --apply consumes onboard-plan.yaml produced by $roll-onboard
|
|
943
|
+
if [[ "${1:-}" == "--apply" ]]; then
|
|
944
|
+
shift
|
|
945
|
+
_init_apply "$@"
|
|
946
|
+
return $?
|
|
947
|
+
fi
|
|
948
|
+
|
|
925
949
|
local project_dir
|
|
926
950
|
project_dir="$(pwd)"
|
|
927
951
|
local has_agents=false
|
|
@@ -930,13 +954,20 @@ cmd_init() {
|
|
|
930
954
|
if [[ -f "$project_dir/AGENTS.md" ]]; then
|
|
931
955
|
has_agents=true
|
|
932
956
|
info "Re-merging global conventions... 正在重新合并全局约定..."
|
|
957
|
+
else
|
|
958
|
+
# US-ONBOARD-006: legacy project detection — guide user through $roll-onboard
|
|
959
|
+
# instead of blindly scaffolding files into an existing codebase.
|
|
960
|
+
if _init_is_legacy_project "$project_dir"; then
|
|
961
|
+
_init_legacy_onboard_guide "$project_dir"
|
|
962
|
+
return 0
|
|
963
|
+
fi
|
|
933
964
|
fi
|
|
934
965
|
|
|
935
966
|
_merge_global_to_project "$project_dir"
|
|
936
967
|
_merge_claude_to_project "$project_dir"
|
|
937
|
-
_write_backlog "$project_dir/
|
|
938
|
-
_ensure_features_dir "$project_dir/
|
|
939
|
-
_write_features_md "$project_dir/
|
|
968
|
+
_write_backlog "$project_dir/.roll/backlog.md"
|
|
969
|
+
_ensure_features_dir "$project_dir/.roll/features"
|
|
970
|
+
_write_features_md "$project_dir/.roll/features.md"
|
|
940
971
|
print_merge_summary
|
|
941
972
|
|
|
942
973
|
echo ""
|
|
@@ -953,6 +984,402 @@ cmd_init() {
|
|
|
953
984
|
fi
|
|
954
985
|
}
|
|
955
986
|
|
|
987
|
+
# US-ONBOARD-006: Legacy detection
|
|
988
|
+
# A project is "Legacy" if it has substantive code but no AGENTS.md to anchor
|
|
989
|
+
# Roll conventions. Definition (per design doc §5.1): no AGENTS.md AND any of
|
|
990
|
+
# src/ app/ lib/ pkg/ cmd/ contains ≥10 non-empty files.
|
|
991
|
+
_init_is_legacy_project() {
|
|
992
|
+
local project_dir="$1"
|
|
993
|
+
local dir count
|
|
994
|
+
for dir in src app lib pkg cmd; do
|
|
995
|
+
if [[ -d "$project_dir/$dir" ]]; then
|
|
996
|
+
count=$(find "$project_dir/$dir" -type f -not -empty 2>/dev/null | wc -l | tr -d ' ')
|
|
997
|
+
if [[ "$count" -ge 10 ]]; then
|
|
998
|
+
return 0
|
|
999
|
+
fi
|
|
1000
|
+
fi
|
|
1001
|
+
done
|
|
1002
|
+
return 1
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
# US-ONBOARD-006: Agent discovery + token consumption notice + onboard guidance.
|
|
1006
|
+
# Reuses _for_each_ai_tool to enumerate installed AI agents. Tells the user
|
|
1007
|
+
# which agent to open, why their tokens will be used, and what to do next.
|
|
1008
|
+
_init_legacy_onboard_guide() {
|
|
1009
|
+
local project_dir="$1"
|
|
1010
|
+
local count_summary
|
|
1011
|
+
count_summary=$(_init_legacy_file_summary "$project_dir")
|
|
1012
|
+
|
|
1013
|
+
info "Detected: legacy project (${count_summary}) 检测到遗留项目"
|
|
1014
|
+
echo ""
|
|
1015
|
+
|
|
1016
|
+
# Enumerate installed agents by parsing ROLL_CONFIG inline.
|
|
1017
|
+
# Output format in config: ai_<name>: <dir>|<config>|<src>
|
|
1018
|
+
local installed=()
|
|
1019
|
+
local missing=()
|
|
1020
|
+
local _key _value _name _dir
|
|
1021
|
+
while IFS=: read -r _key _value; do
|
|
1022
|
+
[[ "$_key" =~ ^ai_ ]] || continue
|
|
1023
|
+
_name="${_key#ai_}"
|
|
1024
|
+
_dir="${_value%%|*}"
|
|
1025
|
+
_dir="${_dir# }" # trim leading space
|
|
1026
|
+
_dir="${_dir/#\~/$HOME}" # expand ~
|
|
1027
|
+
if [[ -d "$_dir" ]]; then
|
|
1028
|
+
installed+=("$_name")
|
|
1029
|
+
else
|
|
1030
|
+
missing+=("$_name")
|
|
1031
|
+
fi
|
|
1032
|
+
done < "$ROLL_CONFIG"
|
|
1033
|
+
|
|
1034
|
+
echo " Onboarding 需要一个 AI agent 来读懂这个项目。检测到:"
|
|
1035
|
+
echo " Onboarding requires an AI agent to read your code. Detected:"
|
|
1036
|
+
echo ""
|
|
1037
|
+
local n
|
|
1038
|
+
if [[ ${#installed[@]} -gt 0 ]]; then
|
|
1039
|
+
for n in "${installed[@]}"; do
|
|
1040
|
+
printf " %b✓%b %s (installed)\n" "${GREEN}" "${NC}" "$n"
|
|
1041
|
+
done
|
|
1042
|
+
fi
|
|
1043
|
+
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
1044
|
+
for n in "${missing[@]}"; do
|
|
1045
|
+
printf " %b✗%b %s (not found)\n" "${RED}" "${NC}" "$n"
|
|
1046
|
+
done
|
|
1047
|
+
fi
|
|
1048
|
+
|
|
1049
|
+
if [[ ${#installed[@]} -eq 0 ]]; then
|
|
1050
|
+
echo ""
|
|
1051
|
+
err "No AI agent detected. Install one (e.g., 'claude', 'codex', 'kimi') and try again."
|
|
1052
|
+
err "未检测到 AI agent。请先安装 (如 claude / codex / kimi) 后重试。"
|
|
1053
|
+
return 1
|
|
1054
|
+
fi
|
|
1055
|
+
|
|
1056
|
+
echo ""
|
|
1057
|
+
echo " 后续过程会使用你的 agent 调用模型,token 消耗在你自己的账户上。"
|
|
1058
|
+
echo " Onboarding uses your agent to call models — tokens are billed to your account."
|
|
1059
|
+
echo ""
|
|
1060
|
+
echo " 代码与对话都留在你的 agent 工具里 —— Roll 本身不上传任何内容。"
|
|
1061
|
+
echo " Your code and conversation stay in your agent — Roll never uploads anything."
|
|
1062
|
+
echo ""
|
|
1063
|
+
echo " 下一步 Next step:"
|
|
1064
|
+
echo ""
|
|
1065
|
+
if [[ ${#installed[@]} -eq 1 ]]; then
|
|
1066
|
+
echo " Open ${installed[0]} and run: $roll-onboard"
|
|
1067
|
+
else
|
|
1068
|
+
echo " Open any installed agent and run: \$roll-onboard"
|
|
1069
|
+
fi
|
|
1070
|
+
echo ""
|
|
1071
|
+
echo " 完成对话后回到这里运行 After the conversation, return and run:"
|
|
1072
|
+
echo " roll init --apply"
|
|
1073
|
+
echo ""
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
# Helper: human-readable summary of why this is detected as legacy.
|
|
1077
|
+
_init_legacy_file_summary() {
|
|
1078
|
+
local project_dir="$1"
|
|
1079
|
+
local dir count parts=()
|
|
1080
|
+
for dir in src app lib pkg cmd; do
|
|
1081
|
+
if [[ -d "$project_dir/$dir" ]]; then
|
|
1082
|
+
count=$(find "$project_dir/$dir" -type f -not -empty 2>/dev/null | wc -l | tr -d ' ')
|
|
1083
|
+
if [[ "$count" -ge 10 ]]; then
|
|
1084
|
+
parts+=("${count} files in ${dir}/")
|
|
1085
|
+
fi
|
|
1086
|
+
fi
|
|
1087
|
+
done
|
|
1088
|
+
echo "no AGENTS.md, ${parts[*]}"
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
# US-ONBOARD-009: roll init --apply
|
|
1092
|
+
# Consume .roll/onboard-plan.yaml (produced by $roll-onboard skill) and execute
|
|
1093
|
+
# all side effects: create .roll/ structure per scope, sync AI tools, write
|
|
1094
|
+
# .gitignore based on user's Q7 choice.
|
|
1095
|
+
#
|
|
1096
|
+
# Plan validation is delegated to lib/roll-plan-validate.py to avoid bash YAML
|
|
1097
|
+
# parsing fragility.
|
|
1098
|
+
_init_apply() {
|
|
1099
|
+
local project_dir; project_dir="$(pwd)"
|
|
1100
|
+
local plan="${project_dir}/.roll/onboard-plan.yaml"
|
|
1101
|
+
local validator="${ROLL_PKG_DIR}/lib/roll-plan-validate.py"
|
|
1102
|
+
|
|
1103
|
+
if [[ ! -f "$plan" ]]; then
|
|
1104
|
+
err "No onboard plan found at .roll/onboard-plan.yaml 未找到 onboard 计划。"
|
|
1105
|
+
echo "" >&2
|
|
1106
|
+
echo " Run \$roll-onboard in your AI agent first to generate the plan." >&2
|
|
1107
|
+
echo " 请先在 AI agent 里运行 \$roll-onboard 生成 plan,再回来执行 apply。" >&2
|
|
1108
|
+
return 1
|
|
1109
|
+
fi
|
|
1110
|
+
|
|
1111
|
+
if [[ ! -f "$validator" ]]; then
|
|
1112
|
+
err "Plan validator missing: $validator 校验器缺失。"
|
|
1113
|
+
return 1
|
|
1114
|
+
fi
|
|
1115
|
+
|
|
1116
|
+
# Validate plan (schema + generated_at freshness + version)
|
|
1117
|
+
if ! python3 "$validator" "$plan"; then
|
|
1118
|
+
err "Plan validation failed. See errors above. Plan 校验失败。"
|
|
1119
|
+
echo "" >&2
|
|
1120
|
+
echo " If the plan is stale (>24h), regenerate by running \$roll-onboard again." >&2
|
|
1121
|
+
return 1
|
|
1122
|
+
fi
|
|
1123
|
+
|
|
1124
|
+
info "Applying onboard plan... 正在应用 onboard 计划..."
|
|
1125
|
+
_ROLL_MERGE_SUMMARY=()
|
|
1126
|
+
|
|
1127
|
+
# Read scope from plan (simple grep — validator confirmed structure)
|
|
1128
|
+
local approved
|
|
1129
|
+
approved=$(python3 -c "
|
|
1130
|
+
import yaml, sys
|
|
1131
|
+
p = yaml.safe_load(open('$plan'))
|
|
1132
|
+
print(' '.join(p.get('scope', {}).get('approved', [])))
|
|
1133
|
+
" 2>/dev/null || echo "")
|
|
1134
|
+
|
|
1135
|
+
_merge_global_to_project "$project_dir"
|
|
1136
|
+
_merge_claude_to_project "$project_dir"
|
|
1137
|
+
|
|
1138
|
+
# Create .roll/ artifacts based on scope.approved
|
|
1139
|
+
if [[ " $approved " == *" backlog "* ]]; then
|
|
1140
|
+
_write_backlog "$project_dir/.roll/backlog.md"
|
|
1141
|
+
fi
|
|
1142
|
+
if [[ " $approved " == *" features "* ]]; then
|
|
1143
|
+
_ensure_features_dir "$project_dir/.roll/features"
|
|
1144
|
+
_write_features_md "$project_dir/.roll/features.md"
|
|
1145
|
+
fi
|
|
1146
|
+
if [[ " $approved " == *" domain "* ]]; then
|
|
1147
|
+
mkdir -p "$project_dir/.roll/domain"
|
|
1148
|
+
fi
|
|
1149
|
+
if [[ " $approved " == *" briefs "* ]]; then
|
|
1150
|
+
mkdir -p "$project_dir/.roll/briefs"
|
|
1151
|
+
fi
|
|
1152
|
+
|
|
1153
|
+
print_merge_summary
|
|
1154
|
+
|
|
1155
|
+
# Q7: .gitignore preference
|
|
1156
|
+
local gitignore_roll
|
|
1157
|
+
gitignore_roll=$(python3 -c "
|
|
1158
|
+
import yaml
|
|
1159
|
+
p = yaml.safe_load(open('$plan'))
|
|
1160
|
+
print('true' if p.get('privacy', {}).get('gitignore_dot_roll', False) else 'false')
|
|
1161
|
+
" 2>/dev/null || echo "false")
|
|
1162
|
+
|
|
1163
|
+
if [[ "$gitignore_roll" == "true" ]]; then
|
|
1164
|
+
local gi="$project_dir/.gitignore"
|
|
1165
|
+
if ! grep -qFx ".roll/" "$gi" 2>/dev/null; then
|
|
1166
|
+
echo ".roll/" >> "$gi"
|
|
1167
|
+
ok "Added .roll/ to .gitignore 已将 .roll/ 加入 .gitignore"
|
|
1168
|
+
fi
|
|
1169
|
+
fi
|
|
1170
|
+
|
|
1171
|
+
echo ""
|
|
1172
|
+
info "Syncing conventions to AI tools... 正在同步约定到 AI 工具..."
|
|
1173
|
+
_sync_conventions
|
|
1174
|
+
echo ""
|
|
1175
|
+
|
|
1176
|
+
ok "Onboard apply complete. Onboard 应用完成。"
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
1180
|
+
# cmd_migrate
|
|
1181
|
+
# US-ONBOARD-003: One-shot migration from old project layout to .roll/ structure.
|
|
1182
|
+
#
|
|
1183
|
+
# Moves process artifacts (.roll/backlog.md, .roll/proposals.md, .roll/features/, .roll/briefs/,
|
|
1184
|
+
# .roll/dream/, .roll/design/, .roll/domain/) into .roll/. Also relocates user docs
|
|
1185
|
+
# (guide/ → guide/, site/ → site/, site/slides/ → site/slides/).
|
|
1186
|
+
#
|
|
1187
|
+
# Three-state idempotency:
|
|
1188
|
+
# - old-only: execute migration via git mv (single atomic commit)
|
|
1189
|
+
# - new-only: no-op with "already migrated" message
|
|
1190
|
+
# - both: error with conflict list (manual resolution required)
|
|
1191
|
+
# - neither: no-op with "nothing to migrate"
|
|
1192
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
1193
|
+
cmd_migrate() {
|
|
1194
|
+
local dry_run=false
|
|
1195
|
+
while [[ $# -gt 0 ]]; do
|
|
1196
|
+
case "$1" in
|
|
1197
|
+
--dry-run|-n) dry_run=true; shift ;;
|
|
1198
|
+
-h|--help) _migrate_help; return 0 ;;
|
|
1199
|
+
*) err "Unknown arg: $1 未知参数: $1"; return 1 ;;
|
|
1200
|
+
esac
|
|
1201
|
+
done
|
|
1202
|
+
|
|
1203
|
+
# Must be in a git repo (git mv preserves history)
|
|
1204
|
+
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
|
1205
|
+
err "Not a git repository. roll migrate requires git. 当前目录不是 git 仓库。"
|
|
1206
|
+
return 1
|
|
1207
|
+
fi
|
|
1208
|
+
|
|
1209
|
+
# Build canonical migration plan (read from stdout to avoid bash 4.3+ nameref)
|
|
1210
|
+
local -a moves=()
|
|
1211
|
+
while IFS= read -r _line; do moves+=("$_line"); done < <(_migrate_build_moves)
|
|
1212
|
+
|
|
1213
|
+
# Detect state: do old paths exist? does .roll/ exist?
|
|
1214
|
+
local has_old=false has_new=false
|
|
1215
|
+
[[ -e .roll ]] && has_new=true
|
|
1216
|
+
local m src
|
|
1217
|
+
for m in "${moves[@]}"; do
|
|
1218
|
+
src="${m%%|*}"
|
|
1219
|
+
if [[ -e "$src" ]]; then
|
|
1220
|
+
has_old=true
|
|
1221
|
+
break
|
|
1222
|
+
fi
|
|
1223
|
+
done
|
|
1224
|
+
|
|
1225
|
+
# Three-state dispatch
|
|
1226
|
+
if [[ "$has_new" == "true" && "$has_old" == "true" ]]; then
|
|
1227
|
+
err "Both old and new structures exist (partial migration detected). 老结构与新结构同时存在(部分迁移)。"
|
|
1228
|
+
echo "" >&2
|
|
1229
|
+
echo "Conflicting paths: 冲突路径:" >&2
|
|
1230
|
+
for m in "${moves[@]}"; do
|
|
1231
|
+
src="${m%%|*}"
|
|
1232
|
+
local tgt="${m##*|}"
|
|
1233
|
+
if [[ -e "$src" && -e "$tgt" ]]; then
|
|
1234
|
+
echo " - $src AND $tgt both exist" >&2
|
|
1235
|
+
fi
|
|
1236
|
+
done
|
|
1237
|
+
echo "" >&2
|
|
1238
|
+
err "Resolve manually then re-run. 请手动解决冲突后重新运行。"
|
|
1239
|
+
return 1
|
|
1240
|
+
fi
|
|
1241
|
+
|
|
1242
|
+
if [[ "$has_new" == "true" && "$has_old" == "false" ]]; then
|
|
1243
|
+
ok "Already migrated. .roll/ exists, no old paths found. 已迁移,无需重复操作。"
|
|
1244
|
+
return 0
|
|
1245
|
+
fi
|
|
1246
|
+
|
|
1247
|
+
if [[ "$has_old" == "false" ]]; then
|
|
1248
|
+
info "No old structure detected. Nothing to migrate. 未发现老结构,无需迁移。"
|
|
1249
|
+
return 0
|
|
1250
|
+
fi
|
|
1251
|
+
|
|
1252
|
+
# State: old-only — proceed. Filter to actually existing paths.
|
|
1253
|
+
local -a active_moves=()
|
|
1254
|
+
for m in "${moves[@]}"; do
|
|
1255
|
+
src="${m%%|*}"
|
|
1256
|
+
[[ -e "$src" ]] && active_moves+=("$m")
|
|
1257
|
+
done
|
|
1258
|
+
|
|
1259
|
+
if [[ ${#active_moves[@]} -eq 0 ]]; then
|
|
1260
|
+
warn "Old structure markers found but no migratable files. 未找到可迁移文件。"
|
|
1261
|
+
return 0
|
|
1262
|
+
fi
|
|
1263
|
+
|
|
1264
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
1265
|
+
_migrate_preview "${active_moves[@]}"
|
|
1266
|
+
return 0
|
|
1267
|
+
fi
|
|
1268
|
+
|
|
1269
|
+
# Real execution requires clean working tree (we'll create a single commit)
|
|
1270
|
+
if ! git diff --quiet --ignore-submodules HEAD 2>/dev/null; then
|
|
1271
|
+
err "Working tree not clean. Commit or stash changes before running migrate. 工作区有未提交改动,请先 commit 或 stash。"
|
|
1272
|
+
return 1
|
|
1273
|
+
fi
|
|
1274
|
+
|
|
1275
|
+
_migrate_execute "${active_moves[@]}"
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
# Build canonical migration plan as "src|target" pairs (one per line).
|
|
1279
|
+
# Single source of truth for what migrates where.
|
|
1280
|
+
# Returns via stdout (bash 3.2-compatible — no nameref).
|
|
1281
|
+
_migrate_build_moves() {
|
|
1282
|
+
# Order matters: directory-renames must precede moves whose target_dir is
|
|
1283
|
+
# the same dir. Otherwise mkdir -p pre-creates the target, and git mv then
|
|
1284
|
+
# places the source INSIDE rather than renaming. Specifically:
|
|
1285
|
+
# - docs/site → site must precede docs/intro → site/slides
|
|
1286
|
+
# - docs/guide/en → guide/en must precede docs/practices/engineering-common-sense.md
|
|
1287
|
+
#
|
|
1288
|
+
# IMPORTANT: the LEFT side of each mapping is a literal OLD path. These must
|
|
1289
|
+
# NOT be sed'd during Story 5 code-ref migration — they drive the migration
|
|
1290
|
+
# for OTHER projects (and self-migrate idempotency).
|
|
1291
|
+
cat << 'EOF'
|
|
1292
|
+
BACKLOG.md|.roll/backlog.md
|
|
1293
|
+
PROPOSALS.md|.roll/proposals.md
|
|
1294
|
+
docs/features.md|.roll/features.md
|
|
1295
|
+
docs/features|.roll/features
|
|
1296
|
+
docs/briefs|.roll/briefs
|
|
1297
|
+
docs/dream|.roll/dream
|
|
1298
|
+
docs/design|.roll/design
|
|
1299
|
+
docs/domain|.roll/domain
|
|
1300
|
+
docs/practices/loop-autorun-verification.md|.roll/verification/loop-autorun-verification.md
|
|
1301
|
+
docs/site|site
|
|
1302
|
+
docs/intro|site/slides
|
|
1303
|
+
docs/guide/en|guide/en
|
|
1304
|
+
docs/guide/zh|guide/zh
|
|
1305
|
+
docs/practices/engineering-common-sense.md|guide/en/practices/engineering-common-sense.md
|
|
1306
|
+
EOF
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
_migrate_preview() {
|
|
1310
|
+
info "Migration preview (dry-run): 迁移预览(dry-run)"
|
|
1311
|
+
echo ""
|
|
1312
|
+
printf " %-60s → %s\n" "Old path 老路径" "New path 新路径"
|
|
1313
|
+
local sep; sep=$(printf '─%.0s' {1..100})
|
|
1314
|
+
printf " %s\n" "$sep"
|
|
1315
|
+
local m
|
|
1316
|
+
for m in "$@"; do
|
|
1317
|
+
local src="${m%%|*}" tgt="${m##*|}"
|
|
1318
|
+
printf " %-60s → %s\n" "$src" "$tgt"
|
|
1319
|
+
done
|
|
1320
|
+
echo ""
|
|
1321
|
+
info "Run without --dry-run to execute. 去掉 --dry-run 即可真实执行。"
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
_migrate_execute() {
|
|
1325
|
+
info "Migrating ${#@} paths via git mv... 正在通过 git mv 迁移 ${#@} 个路径..."
|
|
1326
|
+
local moved=0 m
|
|
1327
|
+
for m in "$@"; do
|
|
1328
|
+
local src="${m%%|*}" tgt="${m##*|}"
|
|
1329
|
+
local target_dir; target_dir=$(dirname "$tgt")
|
|
1330
|
+
[[ -d "$target_dir" ]] || mkdir -p "$target_dir"
|
|
1331
|
+
git mv "$src" "$tgt" || {
|
|
1332
|
+
err "git mv failed: $src → $tgt"
|
|
1333
|
+
err "Aborting; previous moves are staged but not committed. Run 'git reset --hard' to undo. 已 stage 但未 commit,运行 'git reset --hard' 回滚。"
|
|
1334
|
+
return 1
|
|
1335
|
+
}
|
|
1336
|
+
moved=$((moved + 1))
|
|
1337
|
+
done
|
|
1338
|
+
# Clean up empty docs/ shells
|
|
1339
|
+
if [[ -d "docs" ]]; then
|
|
1340
|
+
find docs -type d -empty -delete 2>/dev/null || true
|
|
1341
|
+
fi
|
|
1342
|
+
# Single atomic commit
|
|
1343
|
+
git commit --quiet -m "Migrate project layout to .roll/ structure
|
|
1344
|
+
|
|
1345
|
+
Atomic migration via 'roll migrate' command. Process artifacts moved
|
|
1346
|
+
from root and docs/ into .roll/; user docs relocated to guide/ and site/.
|
|
1347
|
+
|
|
1348
|
+
Paths migrated: ${moved}"
|
|
1349
|
+
ok "Migrated ${moved} paths in a single commit. 已在单 commit 中迁移 ${moved} 个路径。"
|
|
1350
|
+
echo ""
|
|
1351
|
+
echo " Next steps 下一步:"
|
|
1352
|
+
echo " git log -1 # Inspect the migration commit"
|
|
1353
|
+
echo " roll status # Verify new structure"
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
_migrate_help() {
|
|
1357
|
+
cat << 'EOF'
|
|
1358
|
+
Usage: roll migrate [--dry-run]
|
|
1359
|
+
|
|
1360
|
+
Migrate this project's process artifacts (.roll/backlog.md, .roll/proposals.md,
|
|
1361
|
+
.roll/features/, .roll/briefs/, .roll/dream/, .roll/design/, .roll/domain/)
|
|
1362
|
+
into a .roll/ directory. Also relocates guide/ → guide/,
|
|
1363
|
+
site/ → site/, site/slides/ → site/slides/.
|
|
1364
|
+
|
|
1365
|
+
Options:
|
|
1366
|
+
--dry-run, -n Show what would be moved without modifying files
|
|
1367
|
+
--help, -h Show this help
|
|
1368
|
+
|
|
1369
|
+
Three-state idempotency:
|
|
1370
|
+
- Only old paths present → migration executes (single atomic commit)
|
|
1371
|
+
- Only .roll/ present → no-op (already migrated)
|
|
1372
|
+
- Both present → error with conflict list (manual review)
|
|
1373
|
+
- Neither → no-op (nothing to migrate)
|
|
1374
|
+
|
|
1375
|
+
Preconditions:
|
|
1376
|
+
- Current directory is a git repository
|
|
1377
|
+
- Working tree is clean (commit or stash changes first)
|
|
1378
|
+
|
|
1379
|
+
Uses git mv to preserve file history. On success, produces a single commit.
|
|
1380
|
+
EOF
|
|
1381
|
+
}
|
|
1382
|
+
|
|
956
1383
|
# ─── Helper: print a tidy summary of merge actions ───────────────────────────
|
|
957
1384
|
print_merge_summary() {
|
|
958
1385
|
if [[ ${#_ROLL_MERGE_SUMMARY[@]} -eq 0 ]]; then
|
|
@@ -1019,12 +1446,13 @@ scan_project_type_from_files() {
|
|
|
1019
1446
|
fi
|
|
1020
1447
|
}
|
|
1021
1448
|
|
|
1022
|
-
# ─── Helper: write starter
|
|
1449
|
+
# ─── Helper: write starter .roll/backlog.md (no-op if exists) ──────────────────────
|
|
1023
1450
|
_write_backlog() {
|
|
1024
1451
|
if [[ -f "$1" ]]; then
|
|
1025
|
-
_ROLL_MERGE_SUMMARY+=("unchanged
|
|
1452
|
+
_ROLL_MERGE_SUMMARY+=("unchanged|.roll/backlog.md")
|
|
1026
1453
|
return
|
|
1027
1454
|
fi
|
|
1455
|
+
mkdir -p "$(dirname "$1")"
|
|
1028
1456
|
cat > "$1" << 'EOF'
|
|
1029
1457
|
# Project Backlog
|
|
1030
1458
|
|
|
@@ -1036,25 +1464,25 @@ _write_backlog() {
|
|
|
1036
1464
|
| ID | Problem | Status |
|
|
1037
1465
|
|----|---------|--------|
|
|
1038
1466
|
EOF
|
|
1039
|
-
ok "Created:
|
|
1040
|
-
_ROLL_MERGE_SUMMARY+=("created
|
|
1467
|
+
ok "Created: .roll/backlog.md"
|
|
1468
|
+
_ROLL_MERGE_SUMMARY+=("created|.roll/backlog.md")
|
|
1041
1469
|
}
|
|
1042
1470
|
|
|
1043
1471
|
_ensure_features_dir() {
|
|
1044
1472
|
if [[ -d "$1" ]]; then
|
|
1045
|
-
_ROLL_MERGE_SUMMARY+=("unchanged
|
|
1473
|
+
_ROLL_MERGE_SUMMARY+=("unchanged|.roll/features/")
|
|
1046
1474
|
return
|
|
1047
1475
|
fi
|
|
1048
1476
|
|
|
1049
1477
|
mkdir -p "$1"
|
|
1050
|
-
ok "Created:
|
|
1051
|
-
_ROLL_MERGE_SUMMARY+=("created
|
|
1478
|
+
ok "Created: .roll/features/"
|
|
1479
|
+
_ROLL_MERGE_SUMMARY+=("created|.roll/features/")
|
|
1052
1480
|
}
|
|
1053
1481
|
|
|
1054
|
-
# ─── Helper: write starter
|
|
1482
|
+
# ─── Helper: write starter .roll/features.md (no-op if exists) ────────────────
|
|
1055
1483
|
_write_features_md() {
|
|
1056
1484
|
if [[ -f "$1" ]]; then
|
|
1057
|
-
_ROLL_MERGE_SUMMARY+=("unchanged
|
|
1485
|
+
_ROLL_MERGE_SUMMARY+=("unchanged|.roll/features.md")
|
|
1058
1486
|
return
|
|
1059
1487
|
fi
|
|
1060
1488
|
mkdir -p "$(dirname "$1")"
|
|
@@ -1069,8 +1497,8 @@ _write_features_md() {
|
|
|
1069
1497
|
|
|
1070
1498
|
<!-- Add feature entries here as epics are completed -->
|
|
1071
1499
|
EOF
|
|
1072
|
-
ok "Created:
|
|
1073
|
-
_ROLL_MERGE_SUMMARY+=("created
|
|
1500
|
+
ok "Created: .roll/features.md"
|
|
1501
|
+
_ROLL_MERGE_SUMMARY+=("created|.roll/features.md")
|
|
1074
1502
|
}
|
|
1075
1503
|
|
|
1076
1504
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1250,8 +1678,8 @@ _status_loop_overview() {
|
|
|
1250
1678
|
fi
|
|
1251
1679
|
|
|
1252
1680
|
local todo_count=0
|
|
1253
|
-
if [[ -z "$path_note" && -f "${proj_path}/
|
|
1254
|
-
todo_count=$(grep -c '📋 Todo' "${proj_path}/
|
|
1681
|
+
if [[ -z "$path_note" && -f "${proj_path}/.roll/backlog.md" ]]; then
|
|
1682
|
+
todo_count=$(grep -c '📋 Todo' "${proj_path}/.roll/backlog.md" 2>/dev/null; true)
|
|
1255
1683
|
fi
|
|
1256
1684
|
|
|
1257
1685
|
echo -e " ${state_icon} ${proj_name}${path_note} ${schedule} ${todo_count} pending"
|
|
@@ -1998,6 +2426,13 @@ cmd_agent() {
|
|
|
1998
2426
|
# Returns a filesystem-safe slug combining the project basename and a 6-char
|
|
1999
2427
|
# hash of the full path, ensuring uniqueness across sibling dirs with same name.
|
|
2000
2428
|
_project_slug() {
|
|
2429
|
+
# US-LOOP-006: cycle wrapper exports ROLL_MAIN_SLUG so any subshell — worktree,
|
|
2430
|
+
# tmp cwd, or unrelated path — writes events / runs.jsonl under the main project
|
|
2431
|
+
# identity instead of fragmenting into tmp-* / cycle-* phantom slugs.
|
|
2432
|
+
if [[ -n "${ROLL_MAIN_SLUG:-}" ]]; then
|
|
2433
|
+
printf '%s' "$ROLL_MAIN_SLUG"
|
|
2434
|
+
return 0
|
|
2435
|
+
fi
|
|
2001
2436
|
local path="${1:-$(pwd -P 2>/dev/null || pwd)}"
|
|
2002
2437
|
# FIX-056: normalize path to canonical case on macOS case-insensitive filesystem.
|
|
2003
2438
|
# Two paths differing only in case point to the same directory; realpath
|
|
@@ -2358,6 +2793,38 @@ LOOP_CYCLE_TIMEOUT_SEC="\${ROLL_LOOP_CYCLE_TIMEOUT_SEC:-2700}"
|
|
|
2358
2793
|
_CYCLE_TIMED_OUT=0
|
|
2359
2794
|
_on_sigterm() { _CYCLE_TIMED_OUT=1; }
|
|
2360
2795
|
trap '_on_sigterm' TERM
|
|
2796
|
+
# US-LOOP-005: idempotent runs.jsonl writer shared by normal exit, timeout
|
|
2797
|
+
# trap, and worktree-setup-failure early exit. Guards on jq + run_id dedupe so
|
|
2798
|
+
# multiple callers in the same cycle are safe.
|
|
2799
|
+
_runs_append() {
|
|
2800
|
+
local _status="\$1"; local _tcr="\${2:-0}"; local _built="\${3:-[]}"
|
|
2801
|
+
local _runs_dst="${HOME}/.shared/roll/loop/runs.jsonl"
|
|
2802
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
2803
|
+
local _cid="\${CYCLE_ID:-pre-cycle-\$\$}"
|
|
2804
|
+
local _rid="loop-\${_cid%-*}"
|
|
2805
|
+
grep -qF "\"run_id\":\"\$_rid\"" "\$_runs_dst" 2>/dev/null && return 0
|
|
2806
|
+
mkdir -p "\$(dirname "\$_runs_dst")"
|
|
2807
|
+
local _ts_now; _ts_now=\$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
2808
|
+
local _start="\${CYCLE_START:-\$(date -u +%s)}"
|
|
2809
|
+
local _dur=\$(( \$(date -u +%s) - _start ))
|
|
2810
|
+
[ "\$_dur" -lt 0 ] && _dur=0
|
|
2811
|
+
jq -nc \\
|
|
2812
|
+
--arg ts "\$_ts_now" \\
|
|
2813
|
+
--arg project "${slug}" \\
|
|
2814
|
+
--arg run_id "\$_rid" \\
|
|
2815
|
+
--arg status "\$_status" \\
|
|
2816
|
+
--arg cycle_id "\$_cid" \\
|
|
2817
|
+
--argjson built "\$_built" \\
|
|
2818
|
+
--argjson skipped '[]' \\
|
|
2819
|
+
--argjson alerts '[]' \\
|
|
2820
|
+
--argjson tcr_count "\$_tcr" \\
|
|
2821
|
+
--argjson duration_sec "\$_dur" \\
|
|
2822
|
+
'{ts:\$ts, project:\$project, run_id:\$run_id, status:\$status,
|
|
2823
|
+
cycle_id:\$cycle_id,
|
|
2824
|
+
built:\$built, skipped:\$skipped, alerts:\$alerts,
|
|
2825
|
+
tcr_count:\$tcr_count, duration_sec:\$duration_sec}' \\
|
|
2826
|
+
>> "\$_runs_dst" 2>/dev/null || true
|
|
2827
|
+
}
|
|
2361
2828
|
_inner_cleanup() {
|
|
2362
2829
|
local _rc=\$?
|
|
2363
2830
|
# Kill heartbeat + every remaining background job (watchdog, orphan
|
|
@@ -2367,6 +2834,10 @@ _inner_cleanup() {
|
|
|
2367
2834
|
for _pid in \$(jobs -p); do kill "\$_pid" 2>/dev/null; done
|
|
2368
2835
|
if [ "\${_CYCLE_TIMED_OUT:-0}" -eq 1 ]; then
|
|
2369
2836
|
_loop_event cycle_end "\${CYCLE_ID:-unknown}" "\${BRANCH:-}" "blocked" 2>/dev/null || true
|
|
2837
|
+
# US-LOOP-005 T9: timeout path must also write runs.jsonl row so dashboard
|
|
2838
|
+
# has a terminal record (cycle_end alone is insufficient — runs.jsonl is
|
|
2839
|
+
# the canonical history feed for 'roll loop runs').
|
|
2840
|
+
_runs_append "failed" 0 "[]" 2>/dev/null || true
|
|
2370
2841
|
_worktree_alert "cycle \${CYCLE_ID:-unknown}: \${LOOP_CYCLE_TIMEOUT_SEC}s timeout — claude/python killed; in-progress story marked Blocked" 2>/dev/null || true
|
|
2371
2842
|
fi
|
|
2372
2843
|
rm -f "\$INNER_LOCK" "\$HEARTBEAT_FILE"
|
|
@@ -2389,12 +2860,16 @@ _LOOP_PROJ_SLUG="${slug}"
|
|
|
2389
2860
|
_LOOP_ALERT="\${_SHARED_ROOT}/loop/ALERT-${slug}.md"
|
|
2390
2861
|
_LOOP_STATE="\${_SHARED_ROOT}/loop/state-${slug}.yaml"
|
|
2391
2862
|
_LOOP_MUTE_FILE="\${_SHARED_ROOT}/loop/mute-${slug}"
|
|
2863
|
+
# US-LOOP-006: ROLL_MAIN_SLUG is the canonical identity for any subprocess —
|
|
2864
|
+
# claude, loop-fmt.py, _loop_event in arbitrary cwd. _project_slug honors this
|
|
2865
|
+
# env var first, so writes never fragment into tmp-* / cycle-* phantom slugs.
|
|
2866
|
+
export ROLL_MAIN_SLUG="${slug}"
|
|
2392
2867
|
|
|
2393
2868
|
# Pre-claude: try to create a per-cycle isolated worktree on origin/main.
|
|
2394
2869
|
# On any failure (no remote, no main, etc.) fall back to running in the
|
|
2395
2870
|
# project's main tree (degraded — no isolation, like pre-037 behavior).
|
|
2396
|
-
CYCLE_ID="\$(date
|
|
2397
|
-
CYCLE_START=\$(date
|
|
2871
|
+
CYCLE_ID="\$(date +%Y%m%d-%H%M%S)-\$\$"
|
|
2872
|
+
CYCLE_START=\$(date +%s)
|
|
2398
2873
|
WT="\$(_worktree_path "${slug}" "cycle-\${CYCLE_ID}")"
|
|
2399
2874
|
BRANCH="loop/cycle-\${CYCLE_ID}"
|
|
2400
2875
|
_USE_WORKTREE=0
|
|
@@ -2448,6 +2923,10 @@ else
|
|
|
2448
2923
|
# falling back to the main tree without isolation is unacceptable.
|
|
2449
2924
|
_worktree_alert "cycle \${CYCLE_ID}: worktree setup failed — skipping cycle to avoid running without isolation"
|
|
2450
2925
|
echo "[loop] cycle \${CYCLE_ID}: worktree setup failed; skipping cycle (no isolation)"
|
|
2926
|
+
# US-LOOP-005 T10: worktree-setup-failed path leaves no commits and never
|
|
2927
|
+
# emits cycle_start, but dashboard still needs a runs.jsonl row marking the
|
|
2928
|
+
# cycle as failed (otherwise the scheduled tick appears to have vanished).
|
|
2929
|
+
_runs_append "failed" 0 "[]" 2>/dev/null || true
|
|
2451
2930
|
exit 0
|
|
2452
2931
|
fi
|
|
2453
2932
|
|
|
@@ -2501,7 +2980,7 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
2501
2980
|
_cycle_status="built"
|
|
2502
2981
|
_cycle_tcr=\$(cd "\$WT" && git log --oneline origin/main..HEAD -- 2>/dev/null | grep -c ' tcr:' || echo 0)
|
|
2503
2982
|
if command -v jq >/dev/null 2>&1; then
|
|
2504
|
-
_cycle_built=\$(cd "\$WT" && git diff origin/main --
|
|
2983
|
+
_cycle_built=\$(cd "\$WT" && git diff origin/main -- .roll/backlog.md 2>/dev/null | grep '✅ Done' | grep -oE '\[[A-Z]+-[0-9]+\]' | sed 's/^.//;s/.\$//' | jq -R -s 'split("\n") | map(select(length>0))' 2>/dev/null || echo "[]")
|
|
2505
2984
|
fi
|
|
2506
2985
|
fi
|
|
2507
2986
|
fi
|
|
@@ -2549,6 +3028,8 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
2549
3028
|
elif [ "\$_publish_status" -eq 2 ]; then
|
|
2550
3029
|
if ( cd "${project_path}" && _worktree_merge_back "\$BRANCH" ); then
|
|
2551
3030
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
3031
|
+
# US-LOOP-005 T3: gh unavailable + ff merge_back OK → cycle_end done
|
|
3032
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "done" || true
|
|
2552
3033
|
echo "[loop] cycle \${CYCLE_ID}: gh unavailable; merged via ff and cleaned up"
|
|
2553
3034
|
else
|
|
2554
3035
|
# FIX-039: gh unavailable + merge_back failed — push orphan branch+tag to origin
|
|
@@ -2558,9 +3039,13 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
2558
3039
|
&& git tag "\$_orphan_tag" 2>/dev/null \
|
|
2559
3040
|
&& git push origin "\$_orphan_tag" 2>/dev/null ); then
|
|
2560
3041
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
3042
|
+
# US-LOOP-005 T4: gh unavailable + orphan push OK → cycle_end orphan
|
|
3043
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "orphan" || true
|
|
2561
3044
|
_worktree_alert "cycle \${CYCLE_ID}: gh+merge_back failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
|
|
2562
3045
|
echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
|
|
2563
3046
|
else
|
|
3047
|
+
# US-LOOP-005 T5: gh unavailable + all failed → cycle_end failed
|
|
3048
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true
|
|
2564
3049
|
_worktree_alert "cycle \${CYCLE_ID}: gh+merge_back+push all failed; worktree preserved at \$WT"
|
|
2565
3050
|
echo "[loop] cycle \${CYCLE_ID}: all publish paths failed; worktree preserved at \$WT"
|
|
2566
3051
|
fi
|
|
@@ -2573,15 +3058,21 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
2573
3058
|
&& git tag "\$_orphan_tag" 2>/dev/null \
|
|
2574
3059
|
&& git push origin "\$_orphan_tag" 2>/dev/null ); then
|
|
2575
3060
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
3061
|
+
# US-LOOP-005 T6: PR publish failed + orphan push OK → cycle_end orphan
|
|
3062
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "orphan" || true
|
|
2576
3063
|
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
|
|
2577
3064
|
echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
|
|
2578
3065
|
else
|
|
3066
|
+
# US-LOOP-005 T7: PR publish failed + orphan push failed → cycle_end failed
|
|
3067
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true
|
|
2579
3068
|
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT (branch \$BRANCH)"
|
|
2580
3069
|
echo "[loop] cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT"
|
|
2581
3070
|
fi
|
|
2582
3071
|
fi
|
|
2583
3072
|
fi
|
|
2584
3073
|
else
|
|
3074
|
+
# US-LOOP-005 T8: claude session failed after retry budget → cycle_end failed
|
|
3075
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true
|
|
2585
3076
|
_worktree_alert "cycle \${CYCLE_ID}: claude exited \$_exit; worktree preserved at \$WT (branch \$BRANCH)"
|
|
2586
3077
|
echo "[loop] cycle \${CYCLE_ID}: claude failed (exit \$_exit); worktree preserved at \$WT"
|
|
2587
3078
|
fi
|
|
@@ -2592,29 +3083,9 @@ _loop_cleanup_stale_cycle_branches "${project_path}" || true
|
|
|
2592
3083
|
|
|
2593
3084
|
# FIX-044 / Step 5: Write loop cycle run summary to runs.jsonl
|
|
2594
3085
|
# Deterministic — runs in shell regardless of whether agent executes SKILL.md Step 5.
|
|
2595
|
-
#
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
_cycle_end=\$(date -u +%s)
|
|
2599
|
-
_cycle_dur=\$(( _cycle_end - CYCLE_START ))
|
|
2600
|
-
_ts=\$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
2601
|
-
_run_id="loop-\${CYCLE_ID%-*}"
|
|
2602
|
-
if command -v jq >/dev/null 2>&1 && ! grep -qF "\"run_id\":\"\$_run_id\"" "\$_runs_dst" 2>/dev/null; then
|
|
2603
|
-
jq -nc \\
|
|
2604
|
-
--arg ts "\$_ts" \\
|
|
2605
|
-
--arg project "${slug}" \\
|
|
2606
|
-
--arg run_id "\$_run_id" \\
|
|
2607
|
-
--arg status "\$_cycle_status" \\
|
|
2608
|
-
--argjson built "\$_cycle_built" \\
|
|
2609
|
-
--argjson skipped '[]' \\
|
|
2610
|
-
--argjson alerts '[]' \\
|
|
2611
|
-
--argjson tcr_count "\$_cycle_tcr" \\
|
|
2612
|
-
--argjson duration_sec "\$_cycle_dur" \\
|
|
2613
|
-
'{ts:\$ts, project:\$project, run_id:\$run_id, status:\$status,
|
|
2614
|
-
built:\$built, skipped:\$skipped, alerts:\$alerts,
|
|
2615
|
-
tcr_count:\$tcr_count, duration_sec:\$duration_sec}' \\
|
|
2616
|
-
>> "\$_runs_dst" 2>/dev/null || true
|
|
2617
|
-
fi
|
|
3086
|
+
# US-LOOP-005: now routed through _runs_append so timeout/worktree-setup-fail
|
|
3087
|
+
# share the same write logic. _runs_append is idempotent on run_id.
|
|
3088
|
+
_runs_append "\$_cycle_status" "\$_cycle_tcr" "\$_cycle_built" 2>/dev/null || true
|
|
2618
3089
|
INNER
|
|
2619
3090
|
chmod +x "$inner_path"
|
|
2620
3091
|
|
|
@@ -2851,7 +3322,7 @@ _agent_skill_cmd() {
|
|
|
2851
3322
|
return 1
|
|
2852
3323
|
}
|
|
2853
3324
|
# Cron-installed skills (dream / brief / loop) run autonomously and need to
|
|
2854
|
-
# Edit files (
|
|
3325
|
+
# Edit files (.roll/dream/, .roll/briefs/, BACKLOG, etc.). Claude Code 2.1.x's
|
|
2855
3326
|
# pre-write approval UX silently blocks `claude -p` from applying edits in
|
|
2856
3327
|
# non-interactive pipe mode — bypass it for the cron context.
|
|
2857
3328
|
_agent_bypass_claude_perms
|
|
@@ -3083,6 +3554,9 @@ _loop_test() {
|
|
|
3083
3554
|
}
|
|
3084
3555
|
|
|
3085
3556
|
_loop_status() {
|
|
3557
|
+
# FIX-060: backfill merged PRs before rendering — independent of cycle ticks,
|
|
3558
|
+
# so dashboard reflects merges that happened while loop was paused.
|
|
3559
|
+
_loop_backfill_merged >/dev/null 2>&1 || true
|
|
3086
3560
|
# ROLL_UI=v2 (default) routes to the redesigned Python view.
|
|
3087
3561
|
# Set ROLL_UI=v1 to fall back to the legacy bash implementation.
|
|
3088
3562
|
if [[ "${ROLL_UI:-v2}" == "v2" ]]; then
|
|
@@ -3332,6 +3806,9 @@ _loop_runs() {
|
|
|
3332
3806
|
esac
|
|
3333
3807
|
done
|
|
3334
3808
|
|
|
3809
|
+
# FIX-060: refresh merged status before reading, so paused-window merges show up.
|
|
3810
|
+
_loop_backfill_merged >/dev/null 2>&1 || true
|
|
3811
|
+
|
|
3335
3812
|
if [[ ! -f "$_LOOP_RUNS" ]] || [[ ! -s "$_LOOP_RUNS" ]]; then
|
|
3336
3813
|
echo "No loop runs yet 尚无 loop 运行记录"
|
|
3337
3814
|
return 0
|
|
@@ -3363,7 +3840,7 @@ _loop_runs() {
|
|
|
3363
3840
|
[[ "$(uname)" == "Darwin" ]] && _is_darwin="1"
|
|
3364
3841
|
|
|
3365
3842
|
_LOOP_RUNS_BACKLOG=""
|
|
3366
|
-
[[ -f "$project_path/
|
|
3843
|
+
[[ -f "$project_path/.roll/backlog.md" ]] && _LOOP_RUNS_BACKLOG=$(cat "$project_path/.roll/backlog.md")
|
|
3367
3844
|
|
|
3368
3845
|
while IFS= read -r line; do
|
|
3369
3846
|
[[ -z "$line" ]] && continue
|
|
@@ -3671,7 +4148,7 @@ _loop_clear_heal_state() {
|
|
|
3671
4148
|
}
|
|
3672
4149
|
|
|
3673
4150
|
# Verify TCR rhythm after a story completes. Returns 0 if ok, 1 if no TCR commits.
|
|
3674
|
-
# On failure: reverts story in
|
|
4151
|
+
# On failure: reverts story in .roll/backlog.md to 📋 Todo and writes ALERT.
|
|
3675
4152
|
_loop_enforce_tcr() {
|
|
3676
4153
|
local story_id="$1"
|
|
3677
4154
|
local started_at="${2:-}"
|
|
@@ -3682,10 +4159,10 @@ _loop_enforce_tcr() {
|
|
|
3682
4159
|
|
|
3683
4160
|
if [[ "$count" -eq 0 ]]; then
|
|
3684
4161
|
# Revert story status
|
|
3685
|
-
if [[ -f "
|
|
4162
|
+
if [[ -f ".roll/backlog.md" ]]; then
|
|
3686
4163
|
local tmp; tmp=$(mktemp)
|
|
3687
|
-
sed "/\[${story_id}\]/s/ | ✅ Done |/ | 📋 Todo |/"
|
|
3688
|
-
&& mv "$tmp"
|
|
4164
|
+
sed "/\[${story_id}\]/s/ | ✅ Done |/ | 📋 Todo |/" .roll/backlog.md > "$tmp" \
|
|
4165
|
+
&& mv "$tmp" .roll/backlog.md
|
|
3689
4166
|
fi
|
|
3690
4167
|
|
|
3691
4168
|
# Write ALERT
|
|
@@ -3725,7 +4202,7 @@ EOF
|
|
|
3725
4202
|
# Stdout (on exit 1 due to unsatisfied deps): space-separated unsatisfied IDs.
|
|
3726
4203
|
_loop_check_depends_on() {
|
|
3727
4204
|
local id="$1"
|
|
3728
|
-
local backlog="${2
|
|
4205
|
+
local backlog="${2:-.roll/backlog.md}"
|
|
3729
4206
|
[ -n "$id" ] || return 1
|
|
3730
4207
|
[ -f "$backlog" ] || return 1
|
|
3731
4208
|
|
|
@@ -3762,7 +4239,7 @@ _loop_check_depends_on() {
|
|
|
3762
4239
|
# Exit 1: tag absent, story-id not found, or backlog missing.
|
|
3763
4240
|
_loop_is_manual_only() {
|
|
3764
4241
|
local id="$1"
|
|
3765
|
-
local backlog="${2
|
|
4242
|
+
local backlog="${2:-.roll/backlog.md}"
|
|
3766
4243
|
[ -n "$id" ] || return 1
|
|
3767
4244
|
[ -f "$backlog" ] || return 1
|
|
3768
4245
|
|
|
@@ -4072,7 +4549,7 @@ _loop_pr_claimed_stories() {
|
|
|
4072
4549
|
[ -n "$branch" ] || continue
|
|
4073
4550
|
local content
|
|
4074
4551
|
content=$(gh -R "$slug" api \
|
|
4075
|
-
"repos/${slug}/contents/
|
|
4552
|
+
"repos/${slug}/contents/.roll/backlog.md?ref=${branch}" \
|
|
4076
4553
|
-H "Accept: application/vnd.github.raw" 2>/dev/null) || continue
|
|
4077
4554
|
[ -n "$content" ] || continue
|
|
4078
4555
|
local ids
|
|
@@ -4124,7 +4601,7 @@ _changelog_lint_bullet() {
|
|
|
4124
4601
|
if [ "${len:-0}" -gt 50 ]; then
|
|
4125
4602
|
echo "over-length"
|
|
4126
4603
|
fi
|
|
4127
|
-
if printf '%s' "$stripped" | grep -qE '(^|[^A-Za-z0-9_])(docs|bin|tests|scripts)/'; then
|
|
4604
|
+
if printf '%s' "$stripped" | grep -qE '(^|[^A-Za-z0-9_])(\.roll|docs|bin|tests|scripts)/'; then
|
|
4128
4605
|
echo "path-fragment"
|
|
4129
4606
|
fi
|
|
4130
4607
|
return 0
|
|
@@ -4181,7 +4658,7 @@ _changelog_audit_bullet() {
|
|
|
4181
4658
|
|
|
4182
4659
|
# Rule 3: file suffix / path fragment outside backticks.
|
|
4183
4660
|
if printf '%s' "$stripped" | grep -qE '\.(md|sh|yml|ts|bats)([^A-Za-z0-9]|$)' \
|
|
4184
|
-
|| printf '%s' "$stripped" | grep -qE '(^|[^A-Za-z0-9_])(docs|bin|tests|scripts)/'; then
|
|
4661
|
+
|| printf '%s' "$stripped" | grep -qE '(^|[^A-Za-z0-9_])(\.roll|docs|bin|tests|scripts)/'; then
|
|
4185
4662
|
echo "path-or-suffix"
|
|
4186
4663
|
fi
|
|
4187
4664
|
|
|
@@ -4511,13 +4988,15 @@ _loop_wait_pr_merge() {
|
|
|
4511
4988
|
|
|
4512
4989
|
# _loop_is_doc_only_change
|
|
4513
4990
|
# Returns 0 if every file changed since origin/main is doc-only
|
|
4514
|
-
# (
|
|
4991
|
+
# (.roll/backlog.md, CHANGELOG.md, .roll/proposals.md, docs/, .claude/).
|
|
4515
4992
|
# Returns 1 if any code file changed or there are no changes.
|
|
4516
4993
|
_loop_is_doc_only_change() {
|
|
4517
4994
|
local changed
|
|
4518
4995
|
changed=$(git diff --name-only origin/main HEAD 2>/dev/null) || return 1
|
|
4519
4996
|
[ -z "$changed" ] && return 1
|
|
4520
|
-
|
|
4997
|
+
# Post-Phase-1: process artifacts moved into .roll/; user-facing docs at guide/ + site/.
|
|
4998
|
+
# Legacy paths (BACKLOG.md, PROPOSALS.md, docs/) kept as fallback for pre-2.0 projects.
|
|
4999
|
+
echo "$changed" | grep -qvE '^(\.roll/|CHANGELOG\.md|guide/|site/|\.claude/|BACKLOG\.md|PROPOSALS\.md|docs/)' && return 1
|
|
4521
5000
|
return 0
|
|
4522
5001
|
}
|
|
4523
5002
|
|
|
@@ -4556,6 +5035,60 @@ _loop_publish_doc_pr() {
|
|
|
4556
5035
|
return 0
|
|
4557
5036
|
}
|
|
4558
5037
|
|
|
5038
|
+
# _loop_backfill_merged [runs_jsonl_path]
|
|
5039
|
+
# FIX-060: independent PR-merge backfill. Walks runs.jsonl, finds entries
|
|
5040
|
+
# with status:"built" and a cycle_id field, queries GitHub for the matching
|
|
5041
|
+
# loop/cycle-<id> PR, and rewrites entries whose PR is MERGED to
|
|
5042
|
+
# status:"merged" with merged_at + merge_commit fields.
|
|
5043
|
+
#
|
|
5044
|
+
# Designed to run from the outer runner BEFORE the pause check, so the
|
|
5045
|
+
# scan fires every scheduled tick even when the loop is paused — fixes
|
|
5046
|
+
# the pre-FIX-060 behaviour where merge backfill only happened at next
|
|
5047
|
+
# cycle startup and stalled forever during pause.
|
|
5048
|
+
#
|
|
5049
|
+
# Lenient: returns 0 when gh is missing, slug is unresolvable, jq is
|
|
5050
|
+
# missing, or runs.jsonl does not exist. Atomic rewrite via temp file.
|
|
5051
|
+
_loop_backfill_merged() {
|
|
5052
|
+
local runs_path="${1:-${HOME}/.shared/roll/loop/runs.jsonl}"
|
|
5053
|
+
[ -f "$runs_path" ] || return 0
|
|
5054
|
+
command -v gh >/dev/null 2>&1 || return 0
|
|
5055
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
5056
|
+
local slug; _gh_resolve slug || return 0
|
|
5057
|
+
|
|
5058
|
+
local tmp="${runs_path}.tmp.$$"
|
|
5059
|
+
: > "$tmp"
|
|
5060
|
+
local line status cycle_id branch view_json state merged_at merge_commit
|
|
5061
|
+
while IFS= read -r line; do
|
|
5062
|
+
[ -z "$line" ] && continue
|
|
5063
|
+
status=$(printf '%s' "$line" | jq -r '.status // ""' 2>/dev/null)
|
|
5064
|
+
cycle_id=$(printf '%s' "$line" | jq -r '.cycle_id // ""' 2>/dev/null)
|
|
5065
|
+
if [ "$status" != "built" ] || [ -z "$cycle_id" ]; then
|
|
5066
|
+
printf '%s\n' "$line" >> "$tmp"
|
|
5067
|
+
continue
|
|
5068
|
+
fi
|
|
5069
|
+
branch="loop/cycle-${cycle_id}"
|
|
5070
|
+
view_json=$(gh -R "$slug" pr view "$branch" --json state,mergedAt,mergeCommit 2>/dev/null) || view_json=""
|
|
5071
|
+
if [ -z "$view_json" ]; then
|
|
5072
|
+
printf '%s\n' "$line" >> "$tmp"
|
|
5073
|
+
continue
|
|
5074
|
+
fi
|
|
5075
|
+
state=$(printf '%s' "$view_json" | jq -r '.state // ""' 2>/dev/null)
|
|
5076
|
+
if [ "$state" != "MERGED" ]; then
|
|
5077
|
+
printf '%s\n' "$line" >> "$tmp"
|
|
5078
|
+
continue
|
|
5079
|
+
fi
|
|
5080
|
+
merged_at=$(printf '%s' "$view_json" | jq -r '.mergedAt // ""' 2>/dev/null)
|
|
5081
|
+
merge_commit=$(printf '%s' "$view_json" | jq -r '.mergeCommit.oid // ""' 2>/dev/null)
|
|
5082
|
+
printf '%s' "$line" | jq -c \
|
|
5083
|
+
--arg merged_at "$merged_at" \
|
|
5084
|
+
--arg merge_commit "$merge_commit" \
|
|
5085
|
+
'.status = "merged" | .merged_at = $merged_at | .merge_commit = $merge_commit' \
|
|
5086
|
+
>> "$tmp" 2>/dev/null || printf '%s\n' "$line" >> "$tmp"
|
|
5087
|
+
done < "$runs_path"
|
|
5088
|
+
mv "$tmp" "$runs_path" 2>/dev/null || rm -f "$tmp"
|
|
5089
|
+
return 0
|
|
5090
|
+
}
|
|
5091
|
+
|
|
4559
5092
|
_loop_monitor() {
|
|
4560
5093
|
local interval="${1:-3}"
|
|
4561
5094
|
local project_path; project_path=$(pwd -P)
|
|
@@ -4637,7 +5170,7 @@ _loop_monitor() {
|
|
|
4637
5170
|
# Queue: pending items
|
|
4638
5171
|
echo ""
|
|
4639
5172
|
echo -e " ${BOLD}Queue 待处理队列${NC}"
|
|
4640
|
-
local backlog="
|
|
5173
|
+
local backlog=".roll/backlog.md"
|
|
4641
5174
|
if [[ -f "$backlog" ]]; then
|
|
4642
5175
|
local queue_count=0
|
|
4643
5176
|
local fix_pending us_pending refactor_pending
|
|
@@ -4677,7 +5210,7 @@ _loop_monitor() {
|
|
|
4677
5210
|
|
|
4678
5211
|
[[ $queue_count -eq 0 ]] && echo -e " ${GREEN}✓ empty${NC}"
|
|
4679
5212
|
else
|
|
4680
|
-
echo "
|
|
5213
|
+
echo " .roll/backlog.md not found"
|
|
4681
5214
|
fi
|
|
4682
5215
|
|
|
4683
5216
|
# Log tail (launchd.log)
|
|
@@ -4747,7 +5280,7 @@ for line in sys.stdin:
|
|
|
4747
5280
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
4748
5281
|
|
|
4749
5282
|
cmd_brief() {
|
|
4750
|
-
local briefs_dir="
|
|
5283
|
+
local briefs_dir=".roll/briefs"
|
|
4751
5284
|
local latest; latest=$(ls "${briefs_dir}"/*.md 2>/dev/null | sort | tail -1 || true)
|
|
4752
5285
|
|
|
4753
5286
|
if [[ -z "$latest" ]]; then
|
|
@@ -4816,7 +5349,7 @@ PYEOF
|
|
|
4816
5349
|
_backlog_set_status() {
|
|
4817
5350
|
local pattern="$1"
|
|
4818
5351
|
local new_status="$2"
|
|
4819
|
-
local backlog="
|
|
5352
|
+
local backlog=".roll/backlog.md"
|
|
4820
5353
|
python3 -c "
|
|
4821
5354
|
import sys, re
|
|
4822
5355
|
pattern, new_status, filename = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
@@ -4947,9 +5480,9 @@ cmd_ci() {
|
|
|
4947
5480
|
}
|
|
4948
5481
|
|
|
4949
5482
|
cmd_backlog() {
|
|
4950
|
-
local backlog="
|
|
5483
|
+
local backlog=".roll/backlog.md"
|
|
4951
5484
|
if [[ ! -f "$backlog" ]]; then
|
|
4952
|
-
err "
|
|
5485
|
+
err ".roll/backlog.md not found — run 'roll init' first 未找到 .roll/backlog.md,请先运行 roll init"
|
|
4953
5486
|
return 1
|
|
4954
5487
|
fi
|
|
4955
5488
|
|
|
@@ -5095,19 +5628,19 @@ _dash_git_status() {
|
|
|
5095
5628
|
fi
|
|
5096
5629
|
}
|
|
5097
5630
|
|
|
5098
|
-
# ② Loop layer: extract in-progress story id|title|feature-link from
|
|
5631
|
+
# ② Loop layer: extract in-progress story id|title|feature-link from .roll/backlog.md.
|
|
5099
5632
|
# Output empty if no row's *status column* is 🔨 In Progress (substring matches
|
|
5100
5633
|
# anywhere on the row would catch description text that mentions the emoji).
|
|
5101
5634
|
_dash_in_progress_story() {
|
|
5102
|
-
[[ -f "
|
|
5635
|
+
[[ -f ".roll/backlog.md" ]] || return 0
|
|
5103
5636
|
local row
|
|
5104
|
-
row=$(grep -F '| 🔨 In Progress |'
|
|
5637
|
+
row=$(grep -F '| 🔨 In Progress |' .roll/backlog.md | head -1) || return 0
|
|
5105
5638
|
[[ -z "$row" ]] && return 0
|
|
5106
5639
|
local id desc
|
|
5107
5640
|
id=$(echo "$row" | grep -oE '(US|FIX|REFACTOR)-[A-Z]*-?[0-9]+' | head -1)
|
|
5108
5641
|
desc=$(echo "$row" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//' | cut -c1-60)
|
|
5109
5642
|
local link
|
|
5110
|
-
link=$(echo "$row" | grep -oE '
|
|
5643
|
+
link=$(echo "$row" | grep -oE '.roll/features/[^)]+' | head -1 || true)
|
|
5111
5644
|
printf '%s|%s|%s' "$id" "$desc" "$link"
|
|
5112
5645
|
}
|
|
5113
5646
|
|
|
@@ -5140,8 +5673,8 @@ _dash_last_dream_hours() {
|
|
|
5140
5673
|
|
|
5141
5674
|
# ② Dream layer: count of REFACTOR-XXX rows currently 📋 Todo in BACKLOG.
|
|
5142
5675
|
_dash_refactor_pending() {
|
|
5143
|
-
[[ -f "
|
|
5144
|
-
grep -E '^\| REFACTOR-'
|
|
5676
|
+
[[ -f ".roll/backlog.md" ]] || { echo 0; return; }
|
|
5677
|
+
grep -E '^\| REFACTOR-' .roll/backlog.md 2>/dev/null | grep -F '| 📋 Todo |' | wc -l | tr -d ' '
|
|
5145
5678
|
}
|
|
5146
5679
|
|
|
5147
5680
|
# ② Peer layer: last result + days ago from peer log, empty if no log.
|
|
@@ -5161,11 +5694,11 @@ _dash_last_peer() {
|
|
|
5161
5694
|
|
|
5162
5695
|
# ③ Pipeline counts → Idea Backlog Build (Verify/Release reserved).
|
|
5163
5696
|
_dash_pipeline_counts() {
|
|
5164
|
-
[[ -f "
|
|
5697
|
+
[[ -f ".roll/backlog.md" ]] || { echo "0 0 0 0 0"; return; }
|
|
5165
5698
|
local idea backlog build
|
|
5166
|
-
idea=$(grep -E '^\| IDEA-'
|
|
5167
|
-
backlog=$(grep -E '^\| (\[?US-|FIX-|REFACTOR-)'
|
|
5168
|
-
build=$(grep -F '| 🔨 In Progress |'
|
|
5699
|
+
idea=$(grep -E '^\| IDEA-' .roll/backlog.md 2>/dev/null | grep -F '| 📋 Todo |' | wc -l | tr -d ' ')
|
|
5700
|
+
backlog=$(grep -E '^\| (\[?US-|FIX-|REFACTOR-)' .roll/backlog.md 2>/dev/null | grep -F '| 📋 Todo |' | wc -l | tr -d ' ')
|
|
5701
|
+
build=$(grep -F '| 🔨 In Progress |' .roll/backlog.md 2>/dev/null | wc -l | tr -d ' ')
|
|
5169
5702
|
printf '%s %s %s 0 0' "$idea" "$backlog" "$build"
|
|
5170
5703
|
}
|
|
5171
5704
|
|
|
@@ -5235,10 +5768,10 @@ _dash_alert_count() {
|
|
|
5235
5768
|
grep '^# ALERT' "$_LOOP_ALERT" 2>/dev/null | wc -l | tr -d ' '
|
|
5236
5769
|
}
|
|
5237
5770
|
|
|
5238
|
-
# ⑤ Pending proposal count — "## PROPOSAL:" entries in
|
|
5771
|
+
# ⑤ Pending proposal count — "## PROPOSAL:" entries in .roll/proposals.md.
|
|
5239
5772
|
_dash_proposal_count() {
|
|
5240
|
-
[[ -f "
|
|
5241
|
-
grep '^## PROPOSAL'
|
|
5773
|
+
[[ -f ".roll/proposals.md" ]] || { echo 0; return; }
|
|
5774
|
+
grep '^## PROPOSAL' .roll/proposals.md 2>/dev/null | wc -l | tr -d ' '
|
|
5242
5775
|
}
|
|
5243
5776
|
|
|
5244
5777
|
# ⑤ Release-ready signal — true iff there are releasable commits since the
|
|
@@ -5256,7 +5789,7 @@ _dash_release_ready() {
|
|
|
5256
5789
|
| wc -l | tr -d ' ')
|
|
5257
5790
|
[[ "${commits_with_code:-0}" -gt 0 ]] || return 1
|
|
5258
5791
|
local latest
|
|
5259
|
-
latest=$(ls
|
|
5792
|
+
latest=$(ls .roll/briefs/*.md 2>/dev/null | sort | tail -1 || true)
|
|
5260
5793
|
[[ -z "$latest" ]] && return 1
|
|
5261
5794
|
grep -qE '✅ 可发版|Release ready' "$latest" 2>/dev/null
|
|
5262
5795
|
}
|
|
@@ -5434,7 +5967,7 @@ _legacy_home() {
|
|
|
5434
5967
|
printf " ${GREEN}✓ AI 自驱中 — 无需介入${NC}\n"
|
|
5435
5968
|
else
|
|
5436
5969
|
(( alerts > 0 )) && printf " ${RED}⚠ %s ALERT${NC} run: roll alert\n" "$alerts"
|
|
5437
|
-
(( proposals > 0 )) && printf " ${YELLOW}📋 %s PROPOSAL${NC} see:
|
|
5970
|
+
(( proposals > 0 )) && printf " ${YELLOW}📋 %s PROPOSAL${NC} see: .roll/proposals.md\n" "$proposals"
|
|
5438
5971
|
$release_ready && printf " ${GREEN}✓ Release ready${NC} run: roll release\n"
|
|
5439
5972
|
fi
|
|
5440
5973
|
echo ""
|
|
@@ -5443,7 +5976,7 @@ _legacy_home() {
|
|
|
5443
5976
|
printf " ${BOLD}⏰ Schedules & Last Brief${NC}\n"
|
|
5444
5977
|
printf " loop :%02d · dream %02d:%02d · brief %02d:%02d\n" \
|
|
5445
5978
|
"$loop_minute" "$dream_hour" "$dream_minute" "$brief_hour" "$brief_minute"
|
|
5446
|
-
local latest_brief; latest_brief=$(ls
|
|
5979
|
+
local latest_brief; latest_brief=$(ls .roll/briefs/*.md 2>/dev/null | sort | tail -1 || true)
|
|
5447
5980
|
if [[ -n "$latest_brief" ]]; then
|
|
5448
5981
|
local mod_time now age summary
|
|
5449
5982
|
mod_time=$(stat -c %Y "$latest_brief" 2>/dev/null || stat -f %m "$latest_brief" 2>/dev/null || echo 0)
|
|
@@ -5483,7 +6016,7 @@ _legacy_help() {
|
|
|
5483
6016
|
echo "Commands:"
|
|
5484
6017
|
echo " setup [-f] [Machine] First-time install or re-sync 首次安装或重新同步"
|
|
5485
6018
|
echo " update [Upgrade] npm install latest + re-sync 一键升级到最新版"
|
|
5486
|
-
echo " init [Project] Create AGENTS.md +
|
|
6019
|
+
echo " init [Project] Create AGENTS.md + .roll/backlog.md + docs/ 初始化项目工作流文件"
|
|
5487
6020
|
echo " status [Diagnostic] Show current state 显示当前状态"
|
|
5488
6021
|
echo " peer [Peer Review] Cross-agent negotiation 跨 Agent 协商对审"
|
|
5489
6022
|
echo " loop <on|off|now|status|monitor|resume|reset> [Autonomous] Manage scheduled BACKLOG executor 管理自主执行循环"
|
|
@@ -5517,14 +6050,83 @@ _help() {
|
|
|
5517
6050
|
fi
|
|
5518
6051
|
}
|
|
5519
6052
|
|
|
6053
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
6054
|
+
# _check_structure — US-ONBOARD-004
|
|
6055
|
+
#
|
|
6056
|
+
# Refuse to run project commands on legacy directory structure. Pushes users
|
|
6057
|
+
# toward `roll migrate` rather than letting commands silently operate on old
|
|
6058
|
+
# paths and produce confusing results.
|
|
6059
|
+
#
|
|
6060
|
+
# Exempt commands (always allowed regardless of structure):
|
|
6061
|
+
# setup, update, version/--version/-v, help/--help/-h, migrate, doctor
|
|
6062
|
+
# "" (no command — shows home/help)
|
|
6063
|
+
# init — has its own structure-aware logic inside cmd_init
|
|
6064
|
+
#
|
|
6065
|
+
# Detection walks from pwd up to git root (or stays at pwd if not in a git repo).
|
|
6066
|
+
# Decision: old structure markers present AND no .roll/ → refuse.
|
|
6067
|
+
#
|
|
6068
|
+
# Bypass: ROLL_SKIP_STRUCTURE_CHECK=1 (used by integration tests until Story 5
|
|
6069
|
+
# migrates the test fixtures to new structure).
|
|
6070
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
6071
|
+
_check_structure() {
|
|
6072
|
+
[[ "${ROLL_SKIP_STRUCTURE_CHECK:-0}" == "1" ]] && return 0
|
|
6073
|
+
|
|
6074
|
+
local cmd="$1"
|
|
6075
|
+
case "$cmd" in
|
|
6076
|
+
setup|update|migrate|doctor|version|--version|-v|help|--help|-h|"") return 0 ;;
|
|
6077
|
+
init) return 0 ;; # cmd_init handles its own structure logic
|
|
6078
|
+
esac
|
|
6079
|
+
|
|
6080
|
+
# Determine project root: git root if available, else pwd
|
|
6081
|
+
local root
|
|
6082
|
+
if root=$(git rev-parse --show-toplevel 2>/dev/null); then
|
|
6083
|
+
:
|
|
6084
|
+
else
|
|
6085
|
+
root="$(pwd -P)"
|
|
6086
|
+
fi
|
|
6087
|
+
|
|
6088
|
+
# If new structure exists, allow
|
|
6089
|
+
[[ -d "$root/.roll" ]] && return 0
|
|
6090
|
+
|
|
6091
|
+
# Check for legacy structure markers (literal old-path strings — must NOT be
|
|
6092
|
+
# rewritten during Story 5 code-ref migration; this is the detection logic
|
|
6093
|
+
# for pre-2.0 user projects).
|
|
6094
|
+
if [[ -f "$root/BACKLOG.md" ]] \
|
|
6095
|
+
|| [[ -f "$root/PROPOSALS.md" ]] \
|
|
6096
|
+
|| [[ -d "$root/docs/features" ]] \
|
|
6097
|
+
|| [[ -d "$root/docs/briefs" ]] \
|
|
6098
|
+
|| [[ -d "$root/docs/dream" ]]; then
|
|
6099
|
+
err "Legacy structure detected at: $root 发现老结构目录"
|
|
6100
|
+
echo "" >&2
|
|
6101
|
+
echo " This project uses the pre-2.0 layout (BACKLOG.md / docs/*). Roll 2.0 requires" >&2
|
|
6102
|
+
echo " process artifacts to live in .roll/. Run the migration to upgrade:" >&2
|
|
6103
|
+
echo "" >&2
|
|
6104
|
+
echo " roll migrate --dry-run # Preview changes" >&2
|
|
6105
|
+
echo " roll migrate # Execute (single atomic commit)" >&2
|
|
6106
|
+
echo "" >&2
|
|
6107
|
+
echo " Migration guide: ${ROLL_PKG_DIR}/guide/en/migration-2.0.md" >&2
|
|
6108
|
+
echo "" >&2
|
|
6109
|
+
echo " To roll back to Roll 1.x temporarily:" >&2
|
|
6110
|
+
echo " npm install -g @seanyao/roll@1" >&2
|
|
6111
|
+
exit 1
|
|
6112
|
+
fi
|
|
6113
|
+
|
|
6114
|
+
# No structure detected — empty project or non-Roll dir. Allow.
|
|
6115
|
+
return 0
|
|
6116
|
+
}
|
|
6117
|
+
|
|
5520
6118
|
main() {
|
|
5521
6119
|
local cmd="${1:-}"
|
|
5522
6120
|
shift || true
|
|
5523
6121
|
|
|
6122
|
+
# US-ONBOARD-004: refuse to run project commands on legacy structure
|
|
6123
|
+
_check_structure "$cmd"
|
|
6124
|
+
|
|
5524
6125
|
case "$cmd" in
|
|
5525
6126
|
setup) cmd_setup "$@" ;;
|
|
5526
6127
|
update) cmd_update "$@" ;;
|
|
5527
6128
|
init) cmd_init "$@" ;;
|
|
6129
|
+
migrate) cmd_migrate "$@" ;;
|
|
5528
6130
|
status) cmd_status "$@" ;;
|
|
5529
6131
|
peer) cmd_peer "$@" ;;
|
|
5530
6132
|
loop) cmd_loop "$@" ;;
|
|
@@ -5537,7 +6139,7 @@ main() {
|
|
|
5537
6139
|
review-pr) cmd_review_pr "$@" ;;
|
|
5538
6140
|
version|--version|-v) echo "roll v${VERSION}" ;;
|
|
5539
6141
|
help|--help|-h) _help "$@" ;;
|
|
5540
|
-
"") [[ -f "
|
|
6142
|
+
"") [[ -f ".roll/backlog.md" ]] && _home || { _help; _show_changelog; } ;;
|
|
5541
6143
|
*)
|
|
5542
6144
|
err "Unknown command: $cmd 未知命令: $cmd"
|
|
5543
6145
|
echo ""
|