@seanyao/roll 2026.521.2 → 2026.522.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 +12 -0
- package/README.md +1 -0
- package/bin/roll +913 -48
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_render.cpython-314.pyc +0 -0
- package/lib/__pycache__/slides-render.cpython-314.pyc +0 -0
- package/lib/roll-help.py +1 -0
- package/lib/roll-loop-status.py +37 -3
- package/lib/roll_render.py +20 -1
- package/lib/slides-render.py +488 -0
- package/lib/slides-validate.py +169 -0
- package/package.json +1 -1
- package/skills/roll-.changelog/SKILL.md +19 -17
- package/skills/roll-.dream/SKILL.md +1 -1
- package/skills/roll-deck/SKILL.md +136 -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.522.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"
|
|
@@ -100,7 +100,7 @@ config_get() {
|
|
|
100
100
|
local default="${2:-}"
|
|
101
101
|
if [[ -f "$ROLL_CONFIG" ]]; then
|
|
102
102
|
local val
|
|
103
|
-
val=$(grep -E "^${key}:" "$ROLL_CONFIG" 2>/dev/null | head -1 | sed 's/^[^:]*:[[:space:]]*//' | sed 's/[[:space:]]*$//')
|
|
103
|
+
val=$(grep -E "^${key}:" "$ROLL_CONFIG" 2>/dev/null | head -1 | sed 's/^[^:]*:[[:space:]]*//' | sed 's/[[:space:]]*#.*$//' | sed 's/[[:space:]]*$//')
|
|
104
104
|
if [[ -n "$val" ]]; then
|
|
105
105
|
echo "${val/#\~/$HOME}"
|
|
106
106
|
return
|
|
@@ -1070,6 +1070,9 @@ cmd_init() {
|
|
|
1070
1070
|
_write_backlog "$project_dir/.roll/backlog.md"
|
|
1071
1071
|
_ensure_features_dir "$project_dir/.roll/features"
|
|
1072
1072
|
_write_features_md "$project_dir/.roll/features.md"
|
|
1073
|
+
# US-ONBOARD-019: stamp the project so legacy detection can recognise it
|
|
1074
|
+
# as Roll-onboarded without depending on directory-name heuristics.
|
|
1075
|
+
_write_version_stamp "$project_dir"
|
|
1073
1076
|
} >/dev/null
|
|
1074
1077
|
|
|
1075
1078
|
local sync_status="ok"
|
|
@@ -1196,8 +1199,9 @@ _init_is_legacy_project() {
|
|
|
1196
1199
|
}
|
|
1197
1200
|
|
|
1198
1201
|
# US-ONBOARD-006: Agent discovery + token consumption notice + onboard guidance.
|
|
1199
|
-
#
|
|
1200
|
-
#
|
|
1202
|
+
# US-ONBOARD-018: also auto-launches the chosen agent in interactive mode with
|
|
1203
|
+
# the $roll-onboard skill content pre-loaded as the initial prompt, then chains
|
|
1204
|
+
# into `roll init --apply` when the conversation ends successfully.
|
|
1201
1205
|
_init_legacy_onboard_guide() {
|
|
1202
1206
|
local project_dir="$1"
|
|
1203
1207
|
local count_summary
|
|
@@ -1206,40 +1210,25 @@ _init_legacy_onboard_guide() {
|
|
|
1206
1210
|
info "Detected: legacy project (${count_summary}) 检测到遗留项目"
|
|
1207
1211
|
echo ""
|
|
1208
1212
|
|
|
1209
|
-
#
|
|
1210
|
-
|
|
1211
|
-
local installed=()
|
|
1212
|
-
local missing=()
|
|
1213
|
-
local _key _value _name _dir
|
|
1214
|
-
while IFS=: read -r _key _value; do
|
|
1215
|
-
[[ "$_key" =~ ^ai_ ]] || continue
|
|
1216
|
-
_name="${_key#ai_}"
|
|
1217
|
-
_dir="${_value%%|*}"
|
|
1218
|
-
_dir="${_dir# }" # trim leading space
|
|
1219
|
-
_dir="${_dir/#\~/$HOME}" # expand ~
|
|
1220
|
-
if [[ -d "$_dir" ]]; then
|
|
1221
|
-
installed+=("$_name")
|
|
1222
|
-
else
|
|
1223
|
-
missing+=("$_name")
|
|
1224
|
-
fi
|
|
1225
|
-
done < "$ROLL_CONFIG"
|
|
1213
|
+
# Discover installed agents (writes to globals: _ONBOARD_INSTALLED, _ONBOARD_MISSING).
|
|
1214
|
+
_onboard_discover_agents
|
|
1226
1215
|
|
|
1227
1216
|
echo " Onboarding 需要一个 AI agent 来读懂这个项目。检测到:"
|
|
1228
1217
|
echo " Onboarding requires an AI agent to read your code. Detected:"
|
|
1229
1218
|
echo ""
|
|
1230
1219
|
local n
|
|
1231
|
-
if [[ ${#
|
|
1232
|
-
for n in "${
|
|
1220
|
+
if [[ ${#_ONBOARD_INSTALLED[@]} -gt 0 ]]; then
|
|
1221
|
+
for n in "${_ONBOARD_INSTALLED[@]}"; do
|
|
1233
1222
|
printf " %b✓%b %s (installed)\n" "${GREEN}" "${NC}" "$n"
|
|
1234
1223
|
done
|
|
1235
1224
|
fi
|
|
1236
|
-
if [[ ${#
|
|
1237
|
-
for n in "${
|
|
1225
|
+
if [[ ${#_ONBOARD_MISSING[@]} -gt 0 ]]; then
|
|
1226
|
+
for n in "${_ONBOARD_MISSING[@]}"; do
|
|
1238
1227
|
printf " %b✗%b %s (not found)\n" "${RED}" "${NC}" "$n"
|
|
1239
1228
|
done
|
|
1240
1229
|
fi
|
|
1241
1230
|
|
|
1242
|
-
if [[ ${#
|
|
1231
|
+
if [[ ${#_ONBOARD_INSTALLED[@]} -eq 0 ]]; then
|
|
1243
1232
|
echo ""
|
|
1244
1233
|
err "No AI agent detected. Install one (e.g., 'claude', 'codex', 'kimi') and try again."
|
|
1245
1234
|
err "未检测到 AI agent。请先安装 (如 claude / codex / kimi) 后重试。"
|
|
@@ -1253,17 +1242,169 @@ _init_legacy_onboard_guide() {
|
|
|
1253
1242
|
echo " 代码与对话都留在你的 agent 工具里 —— Roll 本身不上传任何内容。"
|
|
1254
1243
|
echo " Your code and conversation stay in your agent — Roll never uploads anything."
|
|
1255
1244
|
echo ""
|
|
1256
|
-
|
|
1245
|
+
|
|
1246
|
+
# US-ONBOARD-018: select an agent. Single installed → auto-pick. Multiple
|
|
1247
|
+
# installed → ask the user (or honour ROLL_ONBOARD_AGENT for non-interactive
|
|
1248
|
+
# callers and tests).
|
|
1249
|
+
local chosen
|
|
1250
|
+
chosen=$(_onboard_select_agent "${_ONBOARD_INSTALLED[@]}") || return 1
|
|
1251
|
+
[[ -n "$chosen" ]] || return 1
|
|
1252
|
+
|
|
1253
|
+
echo ""
|
|
1254
|
+
info "Launching ${chosen}… 正在启动 ${chosen}…"
|
|
1255
|
+
echo " Conversation ends with /exit (or Ctrl-C). On exit Roll will run apply for you."
|
|
1256
|
+
echo " 在 agent 内用 /exit 结束(或 Ctrl-C)。退出后 Roll 会自动衔接 apply。"
|
|
1257
1257
|
echo ""
|
|
1258
|
-
|
|
1259
|
-
|
|
1258
|
+
|
|
1259
|
+
# US-ONBOARD-018: actually run the agent with the onboard prompt pre-loaded.
|
|
1260
|
+
_run_onboard_agent "$chosen" "$project_dir"
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
# US-ONBOARD-018: discover installed AI agents from ROLL_CONFIG.
|
|
1264
|
+
# Populates global arrays _ONBOARD_INSTALLED and _ONBOARD_MISSING.
|
|
1265
|
+
# Extracted from _init_legacy_onboard_guide so it can be unit-tested.
|
|
1266
|
+
_onboard_discover_agents() {
|
|
1267
|
+
_ONBOARD_INSTALLED=()
|
|
1268
|
+
_ONBOARD_MISSING=()
|
|
1269
|
+
local _key _value _name _dir
|
|
1270
|
+
while IFS=: read -r _key _value; do
|
|
1271
|
+
[[ "$_key" =~ ^ai_ ]] || continue
|
|
1272
|
+
_name="${_key#ai_}"
|
|
1273
|
+
_dir="${_value%%|*}"
|
|
1274
|
+
_dir="${_dir# }"
|
|
1275
|
+
_dir="${_dir/#\~/$HOME}"
|
|
1276
|
+
if [[ -d "$_dir" ]]; then
|
|
1277
|
+
_ONBOARD_INSTALLED+=("$_name")
|
|
1278
|
+
else
|
|
1279
|
+
_ONBOARD_MISSING+=("$_name")
|
|
1280
|
+
fi
|
|
1281
|
+
done < "$ROLL_CONFIG"
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
# US-ONBOARD-018: pick an agent for the onboard flow.
|
|
1285
|
+
# - $ROLL_ONBOARD_AGENT (env) wins if set and present in the candidate list.
|
|
1286
|
+
# - Single candidate → auto-pick (printed to stdout).
|
|
1287
|
+
# - Multiple candidates → prompt user for a number. Stdin EOF / invalid input → return 1.
|
|
1288
|
+
# Echoes the chosen agent name on stdout, nothing else.
|
|
1289
|
+
_onboard_select_agent() {
|
|
1290
|
+
local -a candidates=("$@")
|
|
1291
|
+
[[ ${#candidates[@]} -gt 0 ]] || return 1
|
|
1292
|
+
|
|
1293
|
+
# Explicit override (tests, CI, dotfile aliases).
|
|
1294
|
+
if [[ -n "${ROLL_ONBOARD_AGENT:-}" ]]; then
|
|
1295
|
+
local c
|
|
1296
|
+
for c in "${candidates[@]}"; do
|
|
1297
|
+
if [[ "$c" == "$ROLL_ONBOARD_AGENT" ]]; then
|
|
1298
|
+
printf '%s\n' "$c"
|
|
1299
|
+
return 0
|
|
1300
|
+
fi
|
|
1301
|
+
done
|
|
1302
|
+
err "ROLL_ONBOARD_AGENT='${ROLL_ONBOARD_AGENT}' is not in installed agents." >&2
|
|
1303
|
+
return 1
|
|
1304
|
+
fi
|
|
1305
|
+
|
|
1306
|
+
if [[ ${#candidates[@]} -eq 1 ]]; then
|
|
1307
|
+
printf '%s\n' "${candidates[0]}"
|
|
1308
|
+
return 0
|
|
1309
|
+
fi
|
|
1310
|
+
|
|
1311
|
+
# Multi-agent: prompt the user. To stderr so stdout stays clean for the caller.
|
|
1312
|
+
{
|
|
1313
|
+
echo " 选一个 agent Pick an agent:"
|
|
1314
|
+
local i=1
|
|
1315
|
+
for c in "${candidates[@]}"; do
|
|
1316
|
+
printf " %d) %s\n" "$i" "$c"
|
|
1317
|
+
i=$((i + 1))
|
|
1318
|
+
done
|
|
1319
|
+
printf " Enter number [1-%d]: " "${#candidates[@]}"
|
|
1320
|
+
} >&2
|
|
1321
|
+
|
|
1322
|
+
local choice
|
|
1323
|
+
if ! IFS= read -r choice; then
|
|
1324
|
+
err "No input received. Aborting onboard. 未收到输入,已取消 onboard。" >&2
|
|
1325
|
+
return 1
|
|
1326
|
+
fi
|
|
1327
|
+
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#candidates[@]} )); then
|
|
1328
|
+
err "Invalid choice: '${choice}'. 无效选择。" >&2
|
|
1329
|
+
return 1
|
|
1330
|
+
fi
|
|
1331
|
+
printf '%s\n' "${candidates[$((choice - 1))]}"
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
# US-ONBOARD-018: compose the initial prompt for the onboard agent —
|
|
1335
|
+
# the $roll-onboard skill body (frontmatter stripped) plus a kickoff line.
|
|
1336
|
+
# Returns 1 if the skill file is missing.
|
|
1337
|
+
_onboard_initial_prompt() {
|
|
1338
|
+
local skill_file="${ROLL_PKG_DIR}/skills/roll-onboard/SKILL.md"
|
|
1339
|
+
[[ -f "$skill_file" ]] || {
|
|
1340
|
+
err "Skill file missing: ${skill_file}" >&2
|
|
1341
|
+
return 1
|
|
1342
|
+
}
|
|
1343
|
+
# Lead line orients the agent before the skill body. Keep it stable; tests
|
|
1344
|
+
# match on this exact prefix.
|
|
1345
|
+
printf '%s\n\n' "Run the \$roll-onboard skill below for this project. Follow it end-to-end and write .roll/onboard-plan.yaml when done."
|
|
1346
|
+
_skill_content "$skill_file"
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
# US-ONBOARD-018: print retry / switch-agent guidance after a failed onboard run.
|
|
1350
|
+
# Extracted so we can unit-test the wording without spawning subprocesses.
|
|
1351
|
+
# $1 = chosen agent name, $2 = exit code from the agent.
|
|
1352
|
+
_onboard_failure_hint() {
|
|
1353
|
+
local agent="$1" code="$2"
|
|
1354
|
+
echo "" >&2
|
|
1355
|
+
if [[ "$code" == "130" ]]; then
|
|
1356
|
+
err "Onboard cancelled (Ctrl-C). Onboard 已取消(Ctrl-C)。" >&2
|
|
1260
1357
|
else
|
|
1261
|
-
|
|
1358
|
+
err "Agent '${agent}' exited with code ${code}. agent '${agent}' 异常退出 (code ${code})。" >&2
|
|
1359
|
+
fi
|
|
1360
|
+
echo "" >&2
|
|
1361
|
+
echo " 下一步 Next step:" >&2
|
|
1362
|
+
echo " - 再试一次同一个 agent: roll init" >&2
|
|
1363
|
+
echo " - retry the same agent: roll init" >&2
|
|
1364
|
+
echo " - 换一个 agent: ROLL_ONBOARD_AGENT=<name> roll init" >&2
|
|
1365
|
+
echo " - switch to another: ROLL_ONBOARD_AGENT=<name> roll init" >&2
|
|
1366
|
+
echo "" >&2
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
# US-ONBOARD-018: launch the chosen agent in interactive mode with the onboard
|
|
1370
|
+
# prompt pre-loaded, wait for it to exit, then branch:
|
|
1371
|
+
# exit 0 + .roll/onboard-plan.yaml present → chain into roll init --apply
|
|
1372
|
+
# exit 0 + plan missing → tell user to re-run if they want
|
|
1373
|
+
# exit !0 (incl. 130 from SIGINT) → print retry / switch-agent hint
|
|
1374
|
+
# $1 = chosen agent name, $2 = project dir
|
|
1375
|
+
_run_onboard_agent() {
|
|
1376
|
+
local agent="$1" project_dir="$2"
|
|
1377
|
+
local prompt
|
|
1378
|
+
prompt=$(_onboard_initial_prompt) || return 1
|
|
1379
|
+
_agent_argv "$agent" interactive "$prompt" || {
|
|
1380
|
+
err "Agent '${agent}' has no interactive mode wired up. agent '${agent}' 暂未接入 interactive 模式。" >&2
|
|
1381
|
+
return 1
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
# Run attached to the user's tty so the agent's REPL gets stdin/stdout/stderr.
|
|
1385
|
+
# `set -e` is active script-wide; suppress with `|| rc=$?` so the failure
|
|
1386
|
+
# branch (SIGINT 130, agent error) can be handled instead of aborting init.
|
|
1387
|
+
local rc=0
|
|
1388
|
+
"${_AGENT_ARGV[@]}" || rc=$?
|
|
1389
|
+
|
|
1390
|
+
if [[ "$rc" -ne 0 ]]; then
|
|
1391
|
+
_onboard_failure_hint "$agent" "$rc"
|
|
1392
|
+
return "$rc"
|
|
1262
1393
|
fi
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1394
|
+
|
|
1395
|
+
if [[ ! -f "${project_dir}/.roll/onboard-plan.yaml" ]]; then
|
|
1396
|
+
echo "" >&2
|
|
1397
|
+
err "Agent exited cleanly but did not write .roll/onboard-plan.yaml." >&2
|
|
1398
|
+
err "agent 正常退出但未生成 .roll/onboard-plan.yaml。" >&2
|
|
1399
|
+
echo " Re-run \`roll init\` once you've completed the conversation." >&2
|
|
1400
|
+
echo " 对话完成后再次运行 \`roll init\` 即可。" >&2
|
|
1401
|
+
return 1
|
|
1402
|
+
fi
|
|
1403
|
+
|
|
1404
|
+
# Plan present → chain into apply automatically.
|
|
1405
|
+
echo "" >&2
|
|
1406
|
+
info "Plan written. Running apply… 已写入 plan,正在执行 apply…"
|
|
1407
|
+
( cd "$project_dir" && _init_apply )
|
|
1267
1408
|
}
|
|
1268
1409
|
|
|
1269
1410
|
# Helper: human-readable summary of why this is detected as legacy.
|
|
@@ -1424,6 +1565,15 @@ print(' '.join(p.get('scope', {}).get('approved', [])))
|
|
|
1424
1565
|
_merge_global_to_project "$project_dir"
|
|
1425
1566
|
_merge_claude_to_project "$project_dir"
|
|
1426
1567
|
|
|
1568
|
+
# US-ONBOARD-019: stamp the project at onboard-apply time so subsequent
|
|
1569
|
+
# invocations recognise it as Roll-onboarded (and offboard can sweep it).
|
|
1570
|
+
local _stamp_existed=true
|
|
1571
|
+
[[ -f "$project_dir/.roll/.version" ]] || _stamp_existed=false
|
|
1572
|
+
_write_version_stamp "$project_dir"
|
|
1573
|
+
if [[ "$_stamp_existed" == "false" ]] && [[ -f "$project_dir/.roll/.version" ]]; then
|
|
1574
|
+
_onboard_changeset_record "$project_dir" "files_created" ".roll/.version"
|
|
1575
|
+
fi
|
|
1576
|
+
|
|
1427
1577
|
# Create .roll/ artifacts based on scope.approved
|
|
1428
1578
|
if [[ " $approved " == *" backlog "* ]]; then
|
|
1429
1579
|
_write_backlog "$project_dir/.roll/backlog.md"
|
|
@@ -1946,6 +2096,88 @@ _ensure_features_dir() {
|
|
|
1946
2096
|
_ROLL_MERGE_SUMMARY+=("created|.roll/features/")
|
|
1947
2097
|
}
|
|
1948
2098
|
|
|
2099
|
+
# US-ONBOARD-019: write a Roll version stamp under .roll/.version when a project
|
|
2100
|
+
# is onboarded. Going forward, this stamp is the canonical "this project was
|
|
2101
|
+
# onboarded with Roll" signal — it lets `_check_structure` distinguish a
|
|
2102
|
+
# genuine pre-2.0 Roll project (needs migrate) from a non-Roll project that
|
|
2103
|
+
# coincidentally has BACKLOG.md / docs/features/ from another tool.
|
|
2104
|
+
#
|
|
2105
|
+
# Idempotent: an existing stamp is never overwritten, so the original install
|
|
2106
|
+
# timestamp is preserved across re-runs of `roll init`.
|
|
2107
|
+
_write_version_stamp() {
|
|
2108
|
+
local project_dir="$1"
|
|
2109
|
+
local stamp_path="$project_dir/.roll/.version"
|
|
2110
|
+
if [[ -f "$stamp_path" ]]; then
|
|
2111
|
+
_ROLL_MERGE_SUMMARY+=("unchanged|.roll/.version")
|
|
2112
|
+
return 0
|
|
2113
|
+
fi
|
|
2114
|
+
mkdir -p "$project_dir/.roll"
|
|
2115
|
+
local installed_at; installed_at=$(date -u +%FT%TZ)
|
|
2116
|
+
cat > "$stamp_path" <<EOF
|
|
2117
|
+
# Roll project version stamp — written by \`roll init\` (US-ONBOARD-019).
|
|
2118
|
+
# Used by \`_check_structure\` to recognise a previously-onboarded Roll project
|
|
2119
|
+
# without depending on directory-name heuristics.
|
|
2120
|
+
roll_version: "${VERSION}"
|
|
2121
|
+
installed_at: "${installed_at}"
|
|
2122
|
+
EOF
|
|
2123
|
+
_ROLL_MERGE_SUMMARY+=("created|.roll/.version")
|
|
2124
|
+
return 0
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
# US-ONBOARD-019: is <root> a Roll-onboarded project (current or pre-2.0)?
|
|
2128
|
+
#
|
|
2129
|
+
# Returns 0 (true) when at least one Roll-specific signal is present:
|
|
2130
|
+
# 1. .roll/.version stamp (post-019 onboard)
|
|
2131
|
+
# 2. BACKLOG.md with a Roll-1.x Story table or "Bug Fixes" section
|
|
2132
|
+
# 3. PROPOSALS.md with a Roll-style "## Proposal" heading
|
|
2133
|
+
# 4. docs/features/ containing US-/FIX-/REFACTOR- named .md files
|
|
2134
|
+
# 5. docs/briefs/ or docs/dream/ directory non-empty
|
|
2135
|
+
#
|
|
2136
|
+
# A bare BACKLOG.md/PROPOSALS.md from another tool, or a generic
|
|
2137
|
+
# docs/features/ folder, does NOT count — that's the bug US-ONBOARD-019
|
|
2138
|
+
# fixes (false-positive migrate prompts on non-Roll projects).
|
|
2139
|
+
_has_roll_signature() {
|
|
2140
|
+
local root="$1"
|
|
2141
|
+
|
|
2142
|
+
# Signal 1 — post-019 version stamp
|
|
2143
|
+
[[ -f "$root/.roll/.version" ]] && return 0
|
|
2144
|
+
|
|
2145
|
+
# Signal 2 — Roll-1.x BACKLOG.md content
|
|
2146
|
+
if [[ -f "$root/BACKLOG.md" ]]; then
|
|
2147
|
+
if grep -qE '^\| Story \| Description \| Status \|' "$root/BACKLOG.md" 2>/dev/null \
|
|
2148
|
+
|| grep -qE '^## Epic:' "$root/BACKLOG.md" 2>/dev/null \
|
|
2149
|
+
|| grep -qE '^\| ID \| Problem \| Status \|' "$root/BACKLOG.md" 2>/dev/null; then
|
|
2150
|
+
return 0
|
|
2151
|
+
fi
|
|
2152
|
+
fi
|
|
2153
|
+
|
|
2154
|
+
# Signal 3 — Roll-style PROPOSALS.md
|
|
2155
|
+
if [[ -f "$root/PROPOSALS.md" ]]; then
|
|
2156
|
+
if grep -qE '^## Proposal' "$root/PROPOSALS.md" 2>/dev/null; then
|
|
2157
|
+
return 0
|
|
2158
|
+
fi
|
|
2159
|
+
fi
|
|
2160
|
+
|
|
2161
|
+
# Signal 4 — Roll-named files under docs/features/
|
|
2162
|
+
if [[ -d "$root/docs/features" ]]; then
|
|
2163
|
+
if find "$root/docs/features" -maxdepth 2 -type f -name '*.md' 2>/dev/null \
|
|
2164
|
+
| grep -qE '/(US|FIX|REFACTOR)-[0-9]+'; then
|
|
2165
|
+
return 0
|
|
2166
|
+
fi
|
|
2167
|
+
fi
|
|
2168
|
+
|
|
2169
|
+
# Signal 5 — Roll-1.x process artefacts (docs/briefs/ docs/dream/)
|
|
2170
|
+
local dir
|
|
2171
|
+
for dir in docs/briefs docs/dream; do
|
|
2172
|
+
if [[ -d "$root/$dir" ]] \
|
|
2173
|
+
&& [[ -n "$(find "$root/$dir" -mindepth 1 -maxdepth 2 -type f 2>/dev/null | head -1)" ]]; then
|
|
2174
|
+
return 0
|
|
2175
|
+
fi
|
|
2176
|
+
done
|
|
2177
|
+
|
|
2178
|
+
return 1
|
|
2179
|
+
}
|
|
2180
|
+
|
|
1949
2181
|
# ─── Helper: write starter .roll/features.md (no-op if exists) ────────────────
|
|
1950
2182
|
_write_features_md() {
|
|
1951
2183
|
if [[ -f "$1" ]]; then
|
|
@@ -2708,21 +2940,48 @@ _parse_review_verdict() {
|
|
|
2708
2940
|
# Returns 1 on unknown agent. Adding a new agent only needs an entry here.
|
|
2709
2941
|
_agent_argv() {
|
|
2710
2942
|
local agent="$1" mode="$2" prompt="$3"
|
|
2943
|
+
# US-ONBOARD-018: `interactive` mode launches the agent's REPL with the prompt
|
|
2944
|
+
# pre-loaded as the first user message. The user can then converse normally,
|
|
2945
|
+
# keep their tty, and exit with Ctrl-C or /exit. Used by `roll init` to auto-
|
|
2946
|
+
# start the chosen agent for $roll-onboard without a copy-paste handoff.
|
|
2947
|
+
# Convention: positional arg as initial prompt; no -p / exec / run / --quiet.
|
|
2711
2948
|
case "$agent" in
|
|
2712
2949
|
claude)
|
|
2713
2950
|
case "$mode" in
|
|
2714
|
-
|
|
2715
|
-
|
|
2951
|
+
interactive) _AGENT_ARGV=(claude "$prompt") ;;
|
|
2952
|
+
text|peer) _AGENT_ARGV=(claude -p --output-format text "$prompt") ;;
|
|
2953
|
+
*) _AGENT_ARGV=(claude -p "$prompt") ;;
|
|
2954
|
+
esac ;;
|
|
2955
|
+
kimi)
|
|
2956
|
+
case "$mode" in
|
|
2957
|
+
interactive) _AGENT_ARGV=(kimi "$prompt") ;;
|
|
2958
|
+
*) _AGENT_ARGV=(kimi --quiet -p "$prompt") ;;
|
|
2959
|
+
esac ;;
|
|
2960
|
+
deepseek)
|
|
2961
|
+
# deepseek has the same argv shape in both modes (positional prompt).
|
|
2962
|
+
_AGENT_ARGV=(deepseek "$prompt") ;;
|
|
2963
|
+
pi)
|
|
2964
|
+
case "$mode" in
|
|
2965
|
+
interactive) _AGENT_ARGV=(pi "$prompt") ;;
|
|
2966
|
+
*) _AGENT_ARGV=(pi -p "$prompt") ;;
|
|
2716
2967
|
esac ;;
|
|
2717
|
-
kimi) _AGENT_ARGV=(kimi --quiet -p "$prompt") ;;
|
|
2718
|
-
deepseek) _AGENT_ARGV=(deepseek "$prompt") ;;
|
|
2719
|
-
pi) _AGENT_ARGV=(pi -p "$prompt") ;;
|
|
2720
2968
|
codex)
|
|
2721
2969
|
case "$mode" in
|
|
2722
|
-
|
|
2723
|
-
|
|
2970
|
+
interactive) _AGENT_ARGV=(codex "$prompt") ;;
|
|
2971
|
+
peer) _AGENT_ARGV=(codex exec --json --output-last-message "$prompt") ;;
|
|
2972
|
+
*) _AGENT_ARGV=(codex exec "$prompt") ;;
|
|
2973
|
+
esac ;;
|
|
2974
|
+
opencode)
|
|
2975
|
+
case "$mode" in
|
|
2976
|
+
interactive) _AGENT_ARGV=(opencode "$prompt") ;;
|
|
2977
|
+
*) _AGENT_ARGV=(opencode run "$prompt") ;;
|
|
2978
|
+
esac ;;
|
|
2979
|
+
gemini)
|
|
2980
|
+
# gemini integration is interactive-only for now (used by onboard flow).
|
|
2981
|
+
case "$mode" in
|
|
2982
|
+
interactive) _AGENT_ARGV=(gemini "$prompt") ;;
|
|
2983
|
+
*) return 1 ;;
|
|
2724
2984
|
esac ;;
|
|
2725
|
-
opencode) _AGENT_ARGV=(opencode run "$prompt") ;;
|
|
2726
2985
|
*) return 1 ;;
|
|
2727
2986
|
esac
|
|
2728
2987
|
}
|
|
@@ -2766,6 +3025,463 @@ _agent_run_skill() {
|
|
|
2766
3025
|
"${_AGENT_ARGV[@]}"
|
|
2767
3026
|
}
|
|
2768
3027
|
|
|
3028
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
3029
|
+
# SLIDES — deck.md → HTML rendering pipeline (US-DECK-003 / 004 / 005)
|
|
3030
|
+
#
|
|
3031
|
+
# roll slides build <slug> render .roll/slides/<slug>/deck.md → .html, open
|
|
3032
|
+
# roll slides new "<topic>" invoke selected agent with roll-deck skill (US-DECK-004)
|
|
3033
|
+
# roll slides list list decks as a table (US-DECK-005)
|
|
3034
|
+
# roll slides preview <slug> open .roll/slides/<slug>.html in browser (US-DECK-005)
|
|
3035
|
+
#
|
|
3036
|
+
# All four subcommands are implemented: `build` (DECK-003), `new` (DECK-004),
|
|
3037
|
+
# `list` / `preview` (DECK-005). The AI authoring step happens in `new`;
|
|
3038
|
+
# everything else is pure bash.
|
|
3039
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
3040
|
+
_slides_help() {
|
|
3041
|
+
cat <<'EOF'
|
|
3042
|
+
roll slides — deck.md → HTML rendering
|
|
3043
|
+
roll slides — 幻灯片 deck.md 渲染管线
|
|
3044
|
+
|
|
3045
|
+
USAGE 用法
|
|
3046
|
+
roll slides build <slug> [--no-open]
|
|
3047
|
+
Render .roll/slides/<slug>/deck.md → .roll/slides/<slug>.html
|
|
3048
|
+
渲染 deck.md 为 HTML 并自动打开浏览器
|
|
3049
|
+
roll slides new "<topic>" [--template <name>]
|
|
3050
|
+
Generate a new deck.md from a topic via the selected AI agent
|
|
3051
|
+
通过所选 AI agent 根据主题生成新的 deck.md
|
|
3052
|
+
roll slides list List all decks under .roll/slides/ as a table
|
|
3053
|
+
列出 .roll/slides/ 下所有幻灯片
|
|
3054
|
+
roll slides preview <slug> [--no-open]
|
|
3055
|
+
Open .roll/slides/<slug>.html in the default browser
|
|
3056
|
+
在浏览器中打开已渲染的幻灯片
|
|
3057
|
+
|
|
3058
|
+
OPTIONS 选项
|
|
3059
|
+
--no-open Skip auto-opening the rendered HTML in a browser
|
|
3060
|
+
渲染后不自动打开浏览器
|
|
3061
|
+
--help, -h Show this help
|
|
3062
|
+
显示本帮助
|
|
3063
|
+
EOF
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
# Resolve the renderer / validator paths (shipped with the roll package).
|
|
3067
|
+
_slides_lib() {
|
|
3068
|
+
printf '%s' "${ROLL_PKG_DIR}/lib"
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
# Resolve the template path for a given template name.
|
|
3072
|
+
# Returns 0 + prints the path if the template exists, else returns 1.
|
|
3073
|
+
_slides_template_path() {
|
|
3074
|
+
local name="$1"
|
|
3075
|
+
local tpl="${ROLL_PKG_DIR}/site/slides/templates/${name}.html"
|
|
3076
|
+
if [[ -f "$tpl" ]]; then
|
|
3077
|
+
printf '%s' "$tpl"
|
|
3078
|
+
return 0
|
|
3079
|
+
fi
|
|
3080
|
+
return 1
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
# Read the `template:` value from a deck.md frontmatter. Defaults to
|
|
3084
|
+
# `introduction-v3` if the field is absent (validator will catch missing field
|
|
3085
|
+
# separately).
|
|
3086
|
+
_slides_template_for_deck() {
|
|
3087
|
+
local deck="$1"
|
|
3088
|
+
local tpl
|
|
3089
|
+
tpl=$(awk '
|
|
3090
|
+
/^---[[:space:]]*$/ { d++; if (d==2) exit; next }
|
|
3091
|
+
d==1 && /^template:[[:space:]]*/ {
|
|
3092
|
+
sub(/^template:[[:space:]]*/, "")
|
|
3093
|
+
gsub(/^["'\'']|["'\'']$/, "")
|
|
3094
|
+
print
|
|
3095
|
+
exit
|
|
3096
|
+
}
|
|
3097
|
+
' "$deck" 2>/dev/null)
|
|
3098
|
+
[[ -n "$tpl" ]] || tpl="introduction-v3"
|
|
3099
|
+
printf '%s' "$tpl"
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
# Ensure .roll/.gitignore contains `slides/*.html` so the per-build HTML
|
|
3103
|
+
# artefact is ignored by default. deck.md remains committable. Idempotent.
|
|
3104
|
+
_slides_ensure_gitignore() {
|
|
3105
|
+
local gi=".roll/.gitignore"
|
|
3106
|
+
mkdir -p ".roll"
|
|
3107
|
+
if [[ -f "$gi" ]] && grep -qE '^slides/\*\.html$' "$gi" 2>/dev/null; then
|
|
3108
|
+
return 0
|
|
3109
|
+
fi
|
|
3110
|
+
# Preserve a trailing newline before appending.
|
|
3111
|
+
if [[ -f "$gi" ]] && [[ -s "$gi" ]] && [[ "$(tail -c 1 "$gi" 2>/dev/null)" != $'\n' ]]; then
|
|
3112
|
+
printf '\n' >>"$gi"
|
|
3113
|
+
fi
|
|
3114
|
+
printf 'slides/*.html\n' >>"$gi"
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
# Pick the browser-open command for the current OS. Echoes the command name;
|
|
3118
|
+
# returns 1 if no opener is available (tests + headless CI).
|
|
3119
|
+
_slides_open_cmd() {
|
|
3120
|
+
case "$(uname -s 2>/dev/null)" in
|
|
3121
|
+
Darwin) command -v open >/dev/null 2>&1 && { printf 'open'; return 0; } ;;
|
|
3122
|
+
Linux) command -v xdg-open >/dev/null 2>&1 && { printf 'xdg-open'; return 0; } ;;
|
|
3123
|
+
esac
|
|
3124
|
+
return 1
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
cmd_slides_build() {
|
|
3128
|
+
local slug="" no_open=0
|
|
3129
|
+
while [[ $# -gt 0 ]]; do
|
|
3130
|
+
case "$1" in
|
|
3131
|
+
--no-open) no_open=1; shift ;;
|
|
3132
|
+
--help|-h) _slides_help; return 0 ;;
|
|
3133
|
+
--*) err "Unknown option: $1 未知选项: $1"; return 1 ;;
|
|
3134
|
+
*)
|
|
3135
|
+
if [[ -z "$slug" ]]; then
|
|
3136
|
+
slug="$1"; shift
|
|
3137
|
+
else
|
|
3138
|
+
err "Unexpected argument: $1 多余参数: $1"; return 1
|
|
3139
|
+
fi
|
|
3140
|
+
;;
|
|
3141
|
+
esac
|
|
3142
|
+
done
|
|
3143
|
+
|
|
3144
|
+
if [[ -z "$slug" ]]; then
|
|
3145
|
+
err "Usage: roll slides build <slug> [--no-open]"
|
|
3146
|
+
echo "用法: roll slides build <slug> [--no-open]" >&2
|
|
3147
|
+
return 1
|
|
3148
|
+
fi
|
|
3149
|
+
|
|
3150
|
+
local deck=".roll/slides/${slug}/deck.md"
|
|
3151
|
+
if [[ ! -f "$deck" ]]; then
|
|
3152
|
+
err "Deck not found: ${deck}"
|
|
3153
|
+
echo " 未找到 deck 文件:${deck}" >&2
|
|
3154
|
+
echo " Hint: run 'roll slides new \"<topic>\"' to generate a new deck." >&2
|
|
3155
|
+
echo " 提示:先运行 'roll slides new \"<主题>\"' 生成新的幻灯片。" >&2
|
|
3156
|
+
return 1
|
|
3157
|
+
fi
|
|
3158
|
+
|
|
3159
|
+
local lib_dir; lib_dir=$(_slides_lib)
|
|
3160
|
+
local validator="${lib_dir}/slides-validate.py"
|
|
3161
|
+
local renderer="${lib_dir}/slides-render.py"
|
|
3162
|
+
if [[ ! -f "$validator" || ! -f "$renderer" ]]; then
|
|
3163
|
+
err "Slides toolchain missing — re-run 'roll setup' 渲染工具缺失,请运行 roll setup"
|
|
3164
|
+
return 1
|
|
3165
|
+
fi
|
|
3166
|
+
|
|
3167
|
+
# 1. Validate first (fail-fast on AI-generated decks).
|
|
3168
|
+
if ! python3 "$validator" "$deck"; then
|
|
3169
|
+
err "deck.md validation failed — fix the issues above before building."
|
|
3170
|
+
echo " deck.md 校验失败,请先修复上方提示再重试。" >&2
|
|
3171
|
+
return 1
|
|
3172
|
+
fi
|
|
3173
|
+
|
|
3174
|
+
# 2. Resolve template + render.
|
|
3175
|
+
local tpl_name; tpl_name=$(_slides_template_for_deck "$deck")
|
|
3176
|
+
local tpl_path
|
|
3177
|
+
if ! tpl_path=$(_slides_template_path "$tpl_name"); then
|
|
3178
|
+
err "Template not found: ${tpl_name} 未找到模板:${tpl_name}"
|
|
3179
|
+
return 1
|
|
3180
|
+
fi
|
|
3181
|
+
|
|
3182
|
+
local out=".roll/slides/${slug}.html"
|
|
3183
|
+
mkdir -p ".roll/slides"
|
|
3184
|
+
if ! python3 "$renderer" "$deck" "$tpl_path" "$out"; then
|
|
3185
|
+
err "Render failed for ${deck} 渲染失败:${deck}"
|
|
3186
|
+
return 1
|
|
3187
|
+
fi
|
|
3188
|
+
|
|
3189
|
+
# 3. Default-ignore the HTML artefact so it doesn't accidentally get committed.
|
|
3190
|
+
_slides_ensure_gitignore
|
|
3191
|
+
|
|
3192
|
+
ok "Rendered → ${out} 渲染完成 → ${out}"
|
|
3193
|
+
|
|
3194
|
+
# 4. Auto-open browser unless suppressed (or running inside bats tests).
|
|
3195
|
+
if [[ "$no_open" -eq 1 ]] || [[ -n "${BATS_TEST_NUMBER:-}" ]] || [[ -n "${ROLL_SLIDES_NO_OPEN:-}" ]]; then
|
|
3196
|
+
return 0
|
|
3197
|
+
fi
|
|
3198
|
+
local opener
|
|
3199
|
+
if opener=$(_slides_open_cmd); then
|
|
3200
|
+
"$opener" "$out" >/dev/null 2>&1 || true
|
|
3201
|
+
fi
|
|
3202
|
+
return 0
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
# ─── US-DECK-005 ─────────────────────────────────────────────────────────────
|
|
3206
|
+
# Read a frontmatter field from deck.md. Returns empty string if absent.
|
|
3207
|
+
# Stops scanning at the closing `---` so YAML body keys can't leak through.
|
|
3208
|
+
_slides_frontmatter_field() {
|
|
3209
|
+
local deck="$1" field="$2"
|
|
3210
|
+
awk -v field="$field" '
|
|
3211
|
+
/^---[[:space:]]*$/ { d++; if (d==2) exit; next }
|
|
3212
|
+
d==1 {
|
|
3213
|
+
pat = "^" field "[[:space:]]*:[[:space:]]*"
|
|
3214
|
+
if ($0 ~ pat) {
|
|
3215
|
+
sub(pat, "")
|
|
3216
|
+
gsub(/^["'\'']|["'\'']$/, "")
|
|
3217
|
+
print
|
|
3218
|
+
exit
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
' "$deck" 2>/dev/null
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
# Format a byte count for human-friendly display: 1234 → "1.2K", 2345678 → "2.2M".
|
|
3225
|
+
# Bash arithmetic only — no `bc`, no `numfmt` dependency.
|
|
3226
|
+
_slides_human_size() {
|
|
3227
|
+
local bytes="${1:-0}"
|
|
3228
|
+
if [[ "$bytes" -lt 1024 ]]; then
|
|
3229
|
+
printf '%dB' "$bytes"
|
|
3230
|
+
elif [[ "$bytes" -lt 1048576 ]]; then
|
|
3231
|
+
local tenth=$(( (bytes * 10) / 1024 ))
|
|
3232
|
+
printf '%d.%dK' "$((tenth / 10))" "$((tenth % 10))"
|
|
3233
|
+
else
|
|
3234
|
+
local tenth=$(( (bytes * 10) / 1048576 ))
|
|
3235
|
+
printf '%d.%dM' "$((tenth / 10))" "$((tenth % 10))"
|
|
3236
|
+
fi
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
cmd_slides_list() {
|
|
3240
|
+
while [[ $# -gt 0 ]]; do
|
|
3241
|
+
case "$1" in
|
|
3242
|
+
--help|-h) _slides_help; return 0 ;;
|
|
3243
|
+
--*) err "Unknown option: $1 未知选项: $1"; return 1 ;;
|
|
3244
|
+
*) err "Unexpected argument: $1 多余参数: $1"; return 1 ;;
|
|
3245
|
+
esac
|
|
3246
|
+
done
|
|
3247
|
+
|
|
3248
|
+
local slides_dir=".roll/slides"
|
|
3249
|
+
if [[ ! -d "$slides_dir" ]]; then
|
|
3250
|
+
info "No decks found under .roll/slides/ 无幻灯片"
|
|
3251
|
+
echo " Hint: run 'roll slides new \"<topic>\"' to create one."
|
|
3252
|
+
echo " 提示:运行 'roll slides new \"<主题>\"' 创建第一个幻灯片。"
|
|
3253
|
+
return 0
|
|
3254
|
+
fi
|
|
3255
|
+
|
|
3256
|
+
local -a slugs=()
|
|
3257
|
+
local d slug
|
|
3258
|
+
shopt -s nullglob
|
|
3259
|
+
for d in "$slides_dir"/*/; do
|
|
3260
|
+
slug="${d%/}"
|
|
3261
|
+
slug="${slug##*/}"
|
|
3262
|
+
if [[ -f "${d}deck.md" ]]; then
|
|
3263
|
+
slugs+=("$slug")
|
|
3264
|
+
fi
|
|
3265
|
+
done
|
|
3266
|
+
shopt -u nullglob
|
|
3267
|
+
|
|
3268
|
+
if [[ "${#slugs[@]}" -eq 0 ]]; then
|
|
3269
|
+
info "No decks found under .roll/slides/ 无幻灯片"
|
|
3270
|
+
echo " Hint: run 'roll slides new \"<topic>\"' to create one."
|
|
3271
|
+
echo " 提示:运行 'roll slides new \"<主题>\"' 创建第一个幻灯片。"
|
|
3272
|
+
return 0
|
|
3273
|
+
fi
|
|
3274
|
+
|
|
3275
|
+
local -a sorted_slugs
|
|
3276
|
+
IFS=$'\n' sorted_slugs=($(printf '%s\n' "${slugs[@]}" | sort))
|
|
3277
|
+
unset IFS
|
|
3278
|
+
|
|
3279
|
+
printf '%-20s %-20s %-12s %-12s %-5s %s\n' \
|
|
3280
|
+
"slug" "template" "total_slides" "created" "built" "size"
|
|
3281
|
+
printf '%-20s %-20s %-12s %-12s %-5s %s\n' \
|
|
3282
|
+
"----" "--------" "------------" "-------" "-----" "----"
|
|
3283
|
+
|
|
3284
|
+
local s deck html template total created built size bytes
|
|
3285
|
+
for s in "${sorted_slugs[@]}"; do
|
|
3286
|
+
deck="${slides_dir}/${s}/deck.md"
|
|
3287
|
+
html="${slides_dir}/${s}.html"
|
|
3288
|
+
template=$(_slides_frontmatter_field "$deck" "template")
|
|
3289
|
+
[[ -z "$template" ]] && template="-"
|
|
3290
|
+
total=$(_slides_frontmatter_field "$deck" "total_slides")
|
|
3291
|
+
[[ -z "$total" ]] && total="-"
|
|
3292
|
+
created=$(_slides_frontmatter_field "$deck" "created")
|
|
3293
|
+
[[ -z "$created" ]] && created="-"
|
|
3294
|
+
if [[ -f "$html" ]]; then
|
|
3295
|
+
built="✓"
|
|
3296
|
+
bytes=$(wc -c <"$html" 2>/dev/null | tr -d ' ')
|
|
3297
|
+
[[ -z "$bytes" ]] && bytes=0
|
|
3298
|
+
size=$(_slides_human_size "$bytes")
|
|
3299
|
+
else
|
|
3300
|
+
built="✗"
|
|
3301
|
+
size="-"
|
|
3302
|
+
fi
|
|
3303
|
+
printf '%-20s %-20s %-12s %-12s %-5s %s\n' \
|
|
3304
|
+
"$s" "$template" "$total" "$created" "$built" "$size"
|
|
3305
|
+
done
|
|
3306
|
+
return 0
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
cmd_slides_preview() {
|
|
3310
|
+
local slug="" no_open=0
|
|
3311
|
+
while [[ $# -gt 0 ]]; do
|
|
3312
|
+
case "$1" in
|
|
3313
|
+
--no-open) no_open=1; shift ;;
|
|
3314
|
+
--help|-h) _slides_help; return 0 ;;
|
|
3315
|
+
--*) err "Unknown option: $1 未知选项: $1"; return 1 ;;
|
|
3316
|
+
*)
|
|
3317
|
+
if [[ -z "$slug" ]]; then
|
|
3318
|
+
slug="$1"; shift
|
|
3319
|
+
else
|
|
3320
|
+
err "Unexpected argument: $1 多余参数: $1"; return 1
|
|
3321
|
+
fi
|
|
3322
|
+
;;
|
|
3323
|
+
esac
|
|
3324
|
+
done
|
|
3325
|
+
|
|
3326
|
+
if [[ -z "$slug" ]]; then
|
|
3327
|
+
err "Usage: roll slides preview <slug> [--no-open]"
|
|
3328
|
+
echo "用法: roll slides preview <slug> [--no-open]" >&2
|
|
3329
|
+
return 1
|
|
3330
|
+
fi
|
|
3331
|
+
|
|
3332
|
+
local html=".roll/slides/${slug}.html"
|
|
3333
|
+
if [[ ! -f "$html" ]]; then
|
|
3334
|
+
err "Rendered HTML not found: ${html}"
|
|
3335
|
+
echo " 未找到已渲染的 HTML:${html}" >&2
|
|
3336
|
+
echo " Hint: run 'roll slides build ${slug}' first to render it." >&2
|
|
3337
|
+
echo " 提示:先运行 'roll slides build ${slug}' 渲染幻灯片。" >&2
|
|
3338
|
+
return 1
|
|
3339
|
+
fi
|
|
3340
|
+
|
|
3341
|
+
ok "Preview → ${html} 打开预览 → ${html}"
|
|
3342
|
+
|
|
3343
|
+
if [[ "$no_open" -eq 1 ]] || [[ -n "${BATS_TEST_NUMBER:-}" ]] || [[ -n "${ROLL_SLIDES_NO_OPEN:-}" ]]; then
|
|
3344
|
+
return 0
|
|
3345
|
+
fi
|
|
3346
|
+
local opener
|
|
3347
|
+
if opener=$(_slides_open_cmd); then
|
|
3348
|
+
"$opener" "$html" >/dev/null 2>&1 || true
|
|
3349
|
+
fi
|
|
3350
|
+
return 0
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
# ─── US-DECK-004 ─────────────────────────────────────────────────────────────
|
|
3354
|
+
# Turn a topic string into a kebab-case slug.
|
|
3355
|
+
# Lower-cases, replaces any run of non-alphanumerics with a single dash,
|
|
3356
|
+
# strips leading/trailing dashes. Matches the convention assumed by
|
|
3357
|
+
# `roll slides build <slug>` (lowercase kebab) and the schema.
|
|
3358
|
+
_slides_topic_slug() {
|
|
3359
|
+
local topic="$1"
|
|
3360
|
+
local slug
|
|
3361
|
+
slug=$(printf '%s' "$topic" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-')
|
|
3362
|
+
# Strip leading/trailing dashes.
|
|
3363
|
+
slug="${slug#-}"
|
|
3364
|
+
slug="${slug%-}"
|
|
3365
|
+
printf '%s' "$slug"
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
# `roll slides new "<topic>" [--template <name>]`
|
|
3369
|
+
# AI authoring entry point. Loads the `roll-deck` skill, builds a single text
|
|
3370
|
+
# prompt containing the skill body + topic + slug + template, and hands it
|
|
3371
|
+
# to the selected project agent. The agent is responsible for writing
|
|
3372
|
+
# `.roll/slides/<slug>/deck.md` (and nothing else). After the agent exits,
|
|
3373
|
+
# we print a bilingual hint pointing at `roll slides build <slug>`.
|
|
3374
|
+
cmd_slides_new() {
|
|
3375
|
+
local topic="" template="introduction-v3"
|
|
3376
|
+
while [[ $# -gt 0 ]]; do
|
|
3377
|
+
case "$1" in
|
|
3378
|
+
--template)
|
|
3379
|
+
[[ -n "${2:-}" ]] || { err "--template requires a value --template 需要一个值"; return 1; }
|
|
3380
|
+
template="$2"; shift 2 ;;
|
|
3381
|
+
--template=*) template="${1#--template=}"; shift ;;
|
|
3382
|
+
--help|-h) _slides_help; return 0 ;;
|
|
3383
|
+
--*) err "Unknown option: $1 未知选项: $1"; return 1 ;;
|
|
3384
|
+
*)
|
|
3385
|
+
if [[ -z "$topic" ]]; then
|
|
3386
|
+
topic="$1"; shift
|
|
3387
|
+
else
|
|
3388
|
+
err "Unexpected argument: $1 多余参数: $1"; return 1
|
|
3389
|
+
fi
|
|
3390
|
+
;;
|
|
3391
|
+
esac
|
|
3392
|
+
done
|
|
3393
|
+
|
|
3394
|
+
if [[ -z "$topic" ]]; then
|
|
3395
|
+
err "Usage: roll slides new \"<topic>\" [--template <name>]"
|
|
3396
|
+
echo " 用法:roll slides new \"<主题>\" [--template <模板名>]" >&2
|
|
3397
|
+
return 1
|
|
3398
|
+
fi
|
|
3399
|
+
|
|
3400
|
+
local slug; slug=$(_slides_topic_slug "$topic")
|
|
3401
|
+
if [[ -z "$slug" ]]; then
|
|
3402
|
+
err "Could not derive a slug from topic: $topic"
|
|
3403
|
+
echo " 无法从主题派生 slug:$topic" >&2
|
|
3404
|
+
return 1
|
|
3405
|
+
fi
|
|
3406
|
+
|
|
3407
|
+
local skill_file="${ROLL_PKG_DIR}/skills/roll-deck/SKILL.md"
|
|
3408
|
+
[[ -f "$skill_file" ]] || { err "Skill not found: ${skill_file}"; return 1; }
|
|
3409
|
+
local skill_body; skill_body=$(_skill_content "$skill_file")
|
|
3410
|
+
|
|
3411
|
+
local agent; agent=$(_project_agent)
|
|
3412
|
+
|
|
3413
|
+
# Compose the full prompt: skill body + concrete task context. The agent
|
|
3414
|
+
# reads the skill, then sees the topic / slug / template it must use.
|
|
3415
|
+
local prompt
|
|
3416
|
+
prompt="$(cat <<EOF
|
|
3417
|
+
${skill_body}
|
|
3418
|
+
|
|
3419
|
+
---
|
|
3420
|
+
|
|
3421
|
+
# Task
|
|
3422
|
+
|
|
3423
|
+
topic: ${topic}
|
|
3424
|
+
slug: ${slug}
|
|
3425
|
+
template: ${template}
|
|
3426
|
+
target_file: .roll/slides/${slug}/deck.md
|
|
3427
|
+
|
|
3428
|
+
Generate the 18-slide bilingual deck.md for the topic above, following the workflow and hard constraints in this skill. Write exactly one file: .roll/slides/${slug}/deck.md. Then print the bilingual "Next" hint.
|
|
3429
|
+
|
|
3430
|
+
按本 skill 的工作流和硬约束生成 18 张双语 slide 的 deck.md。只写一个文件:.roll/slides/${slug}/deck.md,然后打印双语 "Next" 提示。
|
|
3431
|
+
EOF
|
|
3432
|
+
)"
|
|
3433
|
+
|
|
3434
|
+
_agent_argv "$agent" text "$prompt" || {
|
|
3435
|
+
err "Unknown agent '${agent}'. Run: roll agent use <claude|kimi|deepseek|pi|codex|opencode>"
|
|
3436
|
+
return 1
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
info "Launching ${agent} with roll-deck skill for topic: ${topic}"
|
|
3440
|
+
info "启动 ${agent} 处理主题:${topic}"
|
|
3441
|
+
"${_AGENT_ARGV[@]}"
|
|
3442
|
+
local rc=$?
|
|
3443
|
+
|
|
3444
|
+
# Whether the agent succeeded or not, point the user at the next bash step.
|
|
3445
|
+
# (Build will surface its own validation errors if deck.md is malformed.)
|
|
3446
|
+
echo
|
|
3447
|
+
echo "Next: roll slides build ${slug}"
|
|
3448
|
+
echo "下一步:roll slides build ${slug}"
|
|
3449
|
+
|
|
3450
|
+
return "$rc"
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
cmd_slides() {
|
|
3454
|
+
local subcmd="${1:-}"
|
|
3455
|
+
shift || true
|
|
3456
|
+
case "$subcmd" in
|
|
3457
|
+
build)
|
|
3458
|
+
cmd_slides_build "$@"
|
|
3459
|
+
;;
|
|
3460
|
+
new)
|
|
3461
|
+
cmd_slides_new "$@"
|
|
3462
|
+
;;
|
|
3463
|
+
list)
|
|
3464
|
+
cmd_slides_list "$@"
|
|
3465
|
+
;;
|
|
3466
|
+
preview)
|
|
3467
|
+
cmd_slides_preview "$@"
|
|
3468
|
+
;;
|
|
3469
|
+
--help|-h|help)
|
|
3470
|
+
_slides_help
|
|
3471
|
+
return 0
|
|
3472
|
+
;;
|
|
3473
|
+
"")
|
|
3474
|
+
_slides_help
|
|
3475
|
+
return 1
|
|
3476
|
+
;;
|
|
3477
|
+
*)
|
|
3478
|
+
err "Unknown subcommand: ${subcmd} 未知子命令:${subcmd}"
|
|
3479
|
+
_slides_help >&2
|
|
3480
|
+
return 1
|
|
3481
|
+
;;
|
|
3482
|
+
esac
|
|
3483
|
+
}
|
|
3484
|
+
|
|
2769
3485
|
cmd_review_pr() {
|
|
2770
3486
|
local pr_number="${1:-}"
|
|
2771
3487
|
[ -n "$pr_number" ] || { err "Usage: roll review-pr <number>"; return 1; }
|
|
@@ -3094,7 +3810,40 @@ fi
|
|
|
3094
3810
|
# _SHARED_ROOT overrides and silently leaked test runs.jsonl writes into prod.
|
|
3095
3811
|
_LOOP_RUNS="${_SHARED_ROOT}/loop/runs.jsonl"
|
|
3096
3812
|
: "${_LOOP_MUTE_FILE:=${_SHARED_ROOT}/loop/mute-${_LOOP_PROJ_SLUG}}"
|
|
3097
|
-
|
|
3813
|
+
# FIX-087: parallel to FIX-065's _SHARED_ROOT auto-sandbox above. Without this,
|
|
3814
|
+
# tests that source bin/roll (directly via BATS or indirectly via a runner-inner
|
|
3815
|
+
# fork under /tmp / /var/folders/) wrote plists into the developer's real
|
|
3816
|
+
# ~/Library/LaunchAgents/ while their runner paths lived in the sandbox. When
|
|
3817
|
+
# the sandbox got cleaned up, those plists outlived their runners and launchd
|
|
3818
|
+
# fired them every hour with EX_CONFIG, silently killing the autonomous loop.
|
|
3819
|
+
# Detection signals mirror the _SHARED_ROOT block; under a sandbox we route
|
|
3820
|
+
# _LAUNCHD_DIR into _SHARED_ROOT so a single teardown removes both.
|
|
3821
|
+
if [ -z "${_LAUNCHD_DIR:-}" ]; then
|
|
3822
|
+
# When HOME itself is a sandbox dir (run_roll-style wrappers, roll_status
|
|
3823
|
+
# setup, etc.) the default ${HOME}/Library/LaunchAgents is ALREADY in the
|
|
3824
|
+
# sandbox — redirecting again would break tests that pre-seed plists under
|
|
3825
|
+
# the sandboxed HOME. Only auto-sandbox when HOME points to a real user dir.
|
|
3826
|
+
case "${HOME:-}" in
|
|
3827
|
+
/tmp/*|/private/tmp/*|*/var/folders/*|*/tmp.*) ;;
|
|
3828
|
+
*)
|
|
3829
|
+
_roll_in_test_ctx=0
|
|
3830
|
+
if [ -n "${BATS_TEST_FILENAME:-}" ]; then
|
|
3831
|
+
_roll_in_test_ctx=1
|
|
3832
|
+
else
|
|
3833
|
+
_roll_caller="${BASH_SOURCE[1]:-}"
|
|
3834
|
+
case "$_roll_caller" in /tmp/*|/private/tmp/*|/var/folders/*) _roll_in_test_ctx=1 ;; esac
|
|
3835
|
+
case "$PWD" in /tmp/*|/private/tmp/*|/var/folders/*) _roll_in_test_ctx=1 ;; esac
|
|
3836
|
+
fi
|
|
3837
|
+
if [ "$_roll_in_test_ctx" = 1 ]; then
|
|
3838
|
+
_LAUNCHD_DIR="${_SHARED_ROOT}/LaunchAgents"
|
|
3839
|
+
mkdir -p "$_LAUNCHD_DIR"
|
|
3840
|
+
export _LAUNCHD_DIR
|
|
3841
|
+
fi
|
|
3842
|
+
unset _roll_in_test_ctx _roll_caller
|
|
3843
|
+
;;
|
|
3844
|
+
esac
|
|
3845
|
+
fi
|
|
3846
|
+
: "${_LAUNCHD_DIR:=${HOME}/Library/LaunchAgents}"
|
|
3098
3847
|
|
|
3099
3848
|
_config_read_int() {
|
|
3100
3849
|
local key="$1" default="$2"
|
|
@@ -3224,6 +3973,25 @@ _write_launchd_plist() {
|
|
|
3224
3973
|
local plist_path="$1" label="$2" project_path="$3"
|
|
3225
3974
|
local minute="$4" hour="$5" runner_script="$6"
|
|
3226
3975
|
|
|
3976
|
+
# FIX-087 tripwire: last line of defense if some caller explicitly set
|
|
3977
|
+
# _LAUNCHD_DIR back to the real path (or built plist_path manually) while
|
|
3978
|
+
# in a test context. Refuse rather than pollute the user's launchd domain.
|
|
3979
|
+
# Skipped when HOME itself has been redirected to a sandbox dir — then
|
|
3980
|
+
# $HOME/Library/LaunchAgents IS the sandbox, not prod.
|
|
3981
|
+
case "${HOME:-}" in
|
|
3982
|
+
/tmp/*|/private/tmp/*|*/var/folders/*|*/tmp.*) ;;
|
|
3983
|
+
*)
|
|
3984
|
+
if [ -n "${HOME:-}" ] && [ "${plist_path#${HOME}/Library/LaunchAgents/}" != "$plist_path" ]; then
|
|
3985
|
+
case "${BATS_TEST_FILENAME:-}${PWD}" in
|
|
3986
|
+
*/tmp.*|*/var/folders/*|/tmp/*|/private/tmp/*|*.bats)
|
|
3987
|
+
echo "[FIX-087] refusing prod plist write: $plist_path (test context)" >&2
|
|
3988
|
+
return 1
|
|
3989
|
+
;;
|
|
3990
|
+
esac
|
|
3991
|
+
fi
|
|
3992
|
+
;;
|
|
3993
|
+
esac
|
|
3994
|
+
|
|
3227
3995
|
local hour_xml=""
|
|
3228
3996
|
[[ -n "$hour" ]] && hour_xml=" <key>Hour</key>
|
|
3229
3997
|
<integer>${hour}</integer>
|
|
@@ -3406,6 +4174,38 @@ _inner_cleanup() {
|
|
|
3406
4174
|
_runs_append "failed" 0 "[]" 2>/dev/null || true
|
|
3407
4175
|
_worktree_alert "cycle \${CYCLE_ID:-unknown}: \${LOOP_CYCLE_TIMEOUT_SEC}s timeout — claude/python killed; in-progress story marked Blocked" 2>/dev/null || true
|
|
3408
4176
|
fi
|
|
4177
|
+
# FIX-086: aborted-path orphan safety net. When the inner script is killed
|
|
4178
|
+
# (e.g. SIGUSR1, parent process death) after claude has committed TCR work
|
|
4179
|
+
# but before publish runs, the existing aborted fallback below writes
|
|
4180
|
+
# cycle_end aborted and the commits are local-only — if the worktree is
|
|
4181
|
+
# later cleaned up, the work is lost. Before falling through to aborted,
|
|
4182
|
+
# detect unpushed commits in the worktree and push them as an orphan
|
|
4183
|
+
# branch + tag (mirroring FIX-039's PR-publish-failed safety net). On
|
|
4184
|
+
# push success → cycle_end orphan; on failure → fall through to aborted
|
|
4185
|
+
# path below (no regression).
|
|
4186
|
+
# Skip when _CYCLE_TIMED_OUT=1: 45-min hard timeout SIGKILLs claude mid-flight,
|
|
4187
|
+
# so commits may not be atomic — keep human-in-loop via blocked path.
|
|
4188
|
+
# Guard uses inequality form so the FIX-066 audit anchors on the aborted
|
|
4189
|
+
# fallback below, not this block.
|
|
4190
|
+
if [ "\${_CYCLE_END_WRITTEN:-0}" != "1" ] \\
|
|
4191
|
+
&& [ "\${_CYCLE_TIMED_OUT:-0}" = "0" ] \\
|
|
4192
|
+
&& [ "\${_USE_WORKTREE:-0}" = "1" ] \\
|
|
4193
|
+
&& [ -n "\${WT:-}" ] \\
|
|
4194
|
+
&& [ -d "\$WT" ] \\
|
|
4195
|
+
&& [ -n "\${CYCLE_ID:-}" ]; then
|
|
4196
|
+
_unpushed=\$(cd "\$WT" && git rev-list --count "origin/main..HEAD" 2>/dev/null || echo 0)
|
|
4197
|
+
if [ "\${_unpushed:-0}" -gt 0 ]; then
|
|
4198
|
+
_orphan_tag="loop-orphan-\${CYCLE_ID}"
|
|
4199
|
+
if ( cd "\$WT" && git push origin "\$BRANCH" 2>/dev/null \\
|
|
4200
|
+
&& git tag "\$_orphan_tag" 2>/dev/null \\
|
|
4201
|
+
&& git push origin "\$_orphan_tag" 2>/dev/null ); then
|
|
4202
|
+
_loop_event cycle_end "\${CYCLE_ID}" "\${BRANCH:-}" "orphan" 2>/dev/null || true
|
|
4203
|
+
_CYCLE_END_WRITTEN=1
|
|
4204
|
+
_runs_append "orphan" 0 "[]" 2>/dev/null || true
|
|
4205
|
+
_worktree_alert "cycle \${CYCLE_ID}: aborted with \${_unpushed} commits; FIX-086 pushed orphan tag \${_orphan_tag}" 2>/dev/null || true
|
|
4206
|
+
fi
|
|
4207
|
+
fi
|
|
4208
|
+
fi
|
|
3409
4209
|
# IDEA-028 / FIX-066: catch every other abort (SIGKILL, set -e fire, ALERT
|
|
3410
4210
|
# poisoning that bypasses the retry budget, etc.). Without this, cycle_start
|
|
3411
4211
|
# is emitted but cycle_end never is, and dashboard renders the cycle as
|
|
@@ -3619,6 +4419,10 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
3619
4419
|
_worktree_alert "cycle \${CYCLE_ID}: FIX-047: PR not merged within timeout — code may not be in main (BRANCH=\${BRANCH})"
|
|
3620
4420
|
fi
|
|
3621
4421
|
fi
|
|
4422
|
+
# US-VIEW-011: emit terminal PR state (merged/closed/open) before cycle_end
|
|
4423
|
+
# so dashboard renders #NN ✓/↩/… correctly. Must run while branch ref
|
|
4424
|
+
# is still resolvable on remote — gh pr view <branch> needs the head ref.
|
|
4425
|
+
_loop_emit_pr_final "\$BRANCH" 2>/dev/null || true
|
|
3622
4426
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
3623
4427
|
_loop_event cycle_end "\${CYCLE_ID}" "" "done" || true; _CYCLE_END_WRITTEN=1
|
|
3624
4428
|
echo "[loop] cycle \${CYCLE_ID}: published; worktree cleaned"
|
|
@@ -4041,8 +4845,19 @@ _loop_off() {
|
|
|
4041
4845
|
warn "Loop not enabled for this project 当前项目 loop 未启用"; return 0
|
|
4042
4846
|
fi
|
|
4043
4847
|
local slug; slug=$(_project_slug "$project_path")
|
|
4848
|
+
local uid; uid=$(id -u)
|
|
4044
4849
|
for svc in loop dream brief; do
|
|
4045
4850
|
rm -f "${_SHARED_ROOT}/${svc}/run-${slug}.sh"
|
|
4851
|
+
# FIX-081: reverse the FIX-059 auto-bootstrap guard. `_install_launchd_plists`
|
|
4852
|
+
# writes `launchctl disable gui/<UID>/<label>` for every brand-new plist
|
|
4853
|
+
# to block macOS FSEvents from auto-bootstrapping it. That write lands in
|
|
4854
|
+
# the host's /private/var/db/com.apple.xpc.launchd/disabled.<UID>.plist —
|
|
4855
|
+
# it ignores any HOME sandbox. Without a symmetric `enable` on teardown,
|
|
4856
|
+
# every short-lived project leaves 3 permanent ghost labels in the host's
|
|
4857
|
+
# disable list, polluting `launchctl print-disabled` forever even after
|
|
4858
|
+
# the project dir, plists, and ~/.roll are gone.
|
|
4859
|
+
local label; label=$(_launchd_label "$svc" "$project_path")
|
|
4860
|
+
launchctl enable "gui/${uid}/${label}" 2>/dev/null || true
|
|
4046
4861
|
done
|
|
4047
4862
|
ok "Loop disabled 已停用"
|
|
4048
4863
|
return 0
|
|
@@ -5426,6 +6241,14 @@ _worktree_sync_meta() {
|
|
|
5426
6241
|
--exclude='events.ndjson*' \
|
|
5427
6242
|
--exclude='runs.jsonl*' \
|
|
5428
6243
|
.roll/ "$path/.roll/" 2>/dev/null || true
|
|
6244
|
+
# FIX-085: hard-constrain the "skip 🔨 In Progress" rule from the runner
|
|
6245
|
+
# side. SKILL.md tells the agent to skip 🔨 rows but agents don't always
|
|
6246
|
+
# comply, so we strip those rows from the worktree's backlog copy — the
|
|
6247
|
+
# agent literally can't pick a row it can't see. Main backlog untouched.
|
|
6248
|
+
if [ -f "$path/.roll/backlog.md" ]; then
|
|
6249
|
+
sed -i.bak '/| 🔨 In Progress |$/d' "$path/.roll/backlog.md" 2>/dev/null || true
|
|
6250
|
+
rm -f "$path/.roll/backlog.md.bak"
|
|
6251
|
+
fi
|
|
5429
6252
|
}
|
|
5430
6253
|
|
|
5431
6254
|
# _worktree_merge_back <branch>
|
|
@@ -5583,11 +6406,45 @@ _loop_publish_pr() {
|
|
|
5583
6406
|
fi
|
|
5584
6407
|
gh -R "$slug" pr merge "$branch" --auto --squash --delete-branch >/dev/null 2>&1 \
|
|
5585
6408
|
|| _worktree_alert "_loop_publish_pr: gh pr merge --auto failed for ${branch} (PR ${pr_url} left open)"
|
|
5586
|
-
|
|
6409
|
+
# US-VIEW-011: emit 'open' at PR creation; cycle_end path emits a follow-up
|
|
6410
|
+
# event with the terminal outcome (merged / closed) via _loop_emit_pr_final.
|
|
6411
|
+
_loop_event pr "$branch" "$pr_url" "open" 2>/dev/null || true
|
|
5587
6412
|
echo "$pr_url"
|
|
5588
6413
|
return 0
|
|
5589
6414
|
}
|
|
5590
6415
|
|
|
6416
|
+
# _loop_emit_pr_final <branch>
|
|
6417
|
+
# US-VIEW-011: after wait_pr_merge resolves, query gh for the PR's terminal
|
|
6418
|
+
# state and emit a second `pr` event so the dashboard renders the correct
|
|
6419
|
+
# landing marker (#NN ✓ merged / #NN ↩ closed / #NN … open).
|
|
6420
|
+
#
|
|
6421
|
+
# gh state mapping:
|
|
6422
|
+
# MERGED → merged (auto-merge landed)
|
|
6423
|
+
# CLOSED → closed (PR closed without merging — wasted cycle)
|
|
6424
|
+
# OPEN → open (still waiting; auto-merge or human reviewer pending)
|
|
6425
|
+
# UNKNOWN/error → open (conservative — don't lie about merged)
|
|
6426
|
+
#
|
|
6427
|
+
# Lenient: returns 0 on any failure (gh missing, slug unparseable, network
|
|
6428
|
+
# error). The earlier 'open' event remains as the conservative default
|
|
6429
|
+
# rendering.
|
|
6430
|
+
_loop_emit_pr_final() {
|
|
6431
|
+
local branch="$1"
|
|
6432
|
+
command -v gh >/dev/null 2>&1 || return 0
|
|
6433
|
+
local slug; _gh_resolve slug || return 0
|
|
6434
|
+
local pr_url state outcome
|
|
6435
|
+
pr_url=$(gh -R "$slug" pr view "$branch" --json url -q .url 2>/dev/null) || pr_url=""
|
|
6436
|
+
state=$(gh -R "$slug" pr view "$branch" --json state -q .state 2>/dev/null || echo "UNKNOWN")
|
|
6437
|
+
case "$state" in
|
|
6438
|
+
MERGED) outcome="merged" ;;
|
|
6439
|
+
CLOSED) outcome="closed" ;;
|
|
6440
|
+
OPEN) outcome="open" ;;
|
|
6441
|
+
*) outcome="open" ;;
|
|
6442
|
+
esac
|
|
6443
|
+
[ -z "$pr_url" ] && return 0
|
|
6444
|
+
_loop_event pr "$branch" "$pr_url" "$outcome" 2>/dev/null || true
|
|
6445
|
+
return 0
|
|
6446
|
+
}
|
|
6447
|
+
|
|
5591
6448
|
# _loop_wait_pr_merge <branch>
|
|
5592
6449
|
# FIX-047: poll GitHub until PR for <branch> is merged (confirms delivery).
|
|
5593
6450
|
# Returns 0: merged. Returns 1: CLOSED or timeout.
|
|
@@ -5940,7 +6797,7 @@ cmd_brief() {
|
|
|
5940
6797
|
# REFACTOR-021 collapsed the changelog double-pipeline so the release script
|
|
5941
6798
|
# generates the version header directly from BACKLOG, leaving these two
|
|
5942
6799
|
# helpers orphaned. Their behaviour is now part of the changelog renderer
|
|
5943
|
-
# called from
|
|
6800
|
+
# called from the maintainer-private release script at roll-meta/ops/release.sh.
|
|
5944
6801
|
|
|
5945
6802
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
5946
6803
|
# BACKLOG — show pending tasks / manage status
|
|
@@ -6764,14 +7621,21 @@ _check_structure() {
|
|
|
6764
7621
|
# If new structure exists, allow
|
|
6765
7622
|
[[ -d "$root/.roll" ]] && return 0
|
|
6766
7623
|
|
|
6767
|
-
#
|
|
6768
|
-
#
|
|
6769
|
-
#
|
|
7624
|
+
# US-ONBOARD-019: only treat the directory as a legacy *Roll* project when an
|
|
7625
|
+
# old-path marker is present AND a Roll-specific content signature confirms
|
|
7626
|
+
# the project was actually onboarded with Roll. Otherwise we'd block any
|
|
7627
|
+
# non-Roll repo that happens to ship a BACKLOG.md (Jira export, board dump,
|
|
7628
|
+
# different tooling) or a generic docs/features/ folder.
|
|
7629
|
+
local _has_old_path=false
|
|
6770
7630
|
if [[ -f "$root/BACKLOG.md" ]] \
|
|
6771
7631
|
|| [[ -f "$root/PROPOSALS.md" ]] \
|
|
6772
7632
|
|| [[ -d "$root/docs/features" ]] \
|
|
6773
7633
|
|| [[ -d "$root/docs/briefs" ]] \
|
|
6774
7634
|
|| [[ -d "$root/docs/dream" ]]; then
|
|
7635
|
+
_has_old_path=true
|
|
7636
|
+
fi
|
|
7637
|
+
|
|
7638
|
+
if [[ "$_has_old_path" == "true" ]] && _has_roll_signature "$root"; then
|
|
6775
7639
|
err "Legacy structure detected at: $root 发现老结构目录"
|
|
6776
7640
|
echo "" >&2
|
|
6777
7641
|
echo " This project uses the pre-2.0 layout (BACKLOG.md / docs/*). Roll 2.0 requires" >&2
|
|
@@ -6814,6 +7678,7 @@ main() {
|
|
|
6814
7678
|
ci) cmd_ci "$@" ;;
|
|
6815
7679
|
doctor) cmd_doctor "$@" ;;
|
|
6816
7680
|
review-pr) cmd_review_pr "$@" ;;
|
|
7681
|
+
slides) cmd_slides "$@" ;;
|
|
6817
7682
|
version|--version|-v) echo "roll v${VERSION}" ;;
|
|
6818
7683
|
help|--help|-h) _help "$@" ;;
|
|
6819
7684
|
"") [[ -f ".roll/backlog.md" ]] && _home || { _help; _show_changelog; } ;;
|