@seanyao/roll 2.602.5 → 2.603.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 +32 -0
- package/bin/roll +473 -92
- package/lib/README.md +0 -1
- package/lib/__pycache__/changelog_generate.cpython-314.pyc +0 -0
- package/lib/__pycache__/loop-fmt.cpython-314.pyc +0 -0
- package/lib/__pycache__/prices_fetcher.cpython-314.pyc +0 -0
- package/lib/changelog_generate.py +221 -32
- package/lib/loop-fmt.py +2 -2
- package/lib/prices_fetcher.py +331 -63
- package/package.json +1 -1
- package/skills/roll-.changelog/SKILL.md +1 -1
- package/skills/roll-loop/SKILL.md +7 -23
- package/lib/changelog_audit.py +0 -155
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="2.
|
|
7
|
+
VERSION="2.603.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"
|
|
@@ -5519,26 +5519,20 @@ import prices_fetcher as pf
|
|
|
5519
5519
|
|
|
5520
5520
|
snapshot_dir = os.path.join(lib_dir, "prices")
|
|
5521
5521
|
|
|
5522
|
-
# FIX-116: vendor-specific default URLs.
|
|
5523
|
-
VENDOR_URLS = {
|
|
5524
|
-
"anthropic": "https://platform.claude.com/docs/en/about-claude/pricing",
|
|
5525
|
-
"deepseek": "https://api-docs.deepseek.com/quick_start/pricing",
|
|
5526
|
-
"kimi": "https://platform.kimi.com/docs/pricing/chat",
|
|
5527
|
-
}
|
|
5528
|
-
|
|
5529
5522
|
if vendor:
|
|
5530
|
-
|
|
5531
|
-
if not default_url:
|
|
5523
|
+
if vendor not in pf.VENDOR_REGISTRY:
|
|
5532
5524
|
print(f"$(msg prices_refresh.roll_unknown_vendor_vendor)", file=sys.stderr)
|
|
5533
5525
|
print(f"$(msg prices_refresh.roll_known_vendors_join_sorted_vendor)", file=sys.stderr)
|
|
5534
5526
|
sys.exit(1)
|
|
5535
|
-
url = override_url or default_url
|
|
5536
|
-
pf.DEFAULT_SOURCE_URL = url
|
|
5537
5527
|
else:
|
|
5538
|
-
|
|
5528
|
+
vendor = "anthropic"
|
|
5539
5529
|
|
|
5540
5530
|
try:
|
|
5541
|
-
action, changes = pf.refresh(
|
|
5531
|
+
action, changes = pf.refresh(
|
|
5532
|
+
snapshot_dir=snapshot_dir,
|
|
5533
|
+
vendor=vendor,
|
|
5534
|
+
url=override_url or None,
|
|
5535
|
+
)
|
|
5542
5536
|
except pf.FetchError as exc:
|
|
5543
5537
|
print(f"[roll] fetch failed: {exc}", file=sys.stderr)
|
|
5544
5538
|
print("[roll] keeping existing snapshot, no changes written 保留旧快照,未写入新文件",
|
|
@@ -5577,42 +5571,127 @@ cmd_prices() {
|
|
|
5577
5571
|
esac
|
|
5578
5572
|
}
|
|
5579
5573
|
|
|
5574
|
+
# FIX-178: AI-style a deterministic changelog draft into the project voice via
|
|
5575
|
+
# the configured agent (roll agent use). Content-preserving polish only — same
|
|
5576
|
+
# items + (ID)s, bold-lead style anchored on the last 3 released versions. Echoes
|
|
5577
|
+
# the styled draft on success; empty output on any failure so the caller falls
|
|
5578
|
+
# back to the deterministic raw draft. Bounded by a 150s watchdog (macOS lacks
|
|
5579
|
+
# timeout(1)). Uses the same agent path as peer review (_agent_argv).
|
|
5580
|
+
_changelog_ai_style() {
|
|
5581
|
+
local raw="$1"
|
|
5582
|
+
[ -n "$raw" ] || return 1
|
|
5583
|
+
local agent; agent=$(_project_agent)
|
|
5584
|
+
local anchors; anchors=$(_changelog_style_anchors CHANGELOG.md 2>/dev/null || true)
|
|
5585
|
+
local prompt
|
|
5586
|
+
prompt="你是 roll 的 changelog 编辑。把【原始草稿】改写成发布说明,严格遵守:
|
|
5587
|
+
1) 不增删条目、不改事实;每条保留它的(ID)。只润色措辞。
|
|
5588
|
+
2) 每条格式:\`- **简短标题(ID)** — 补充说明\`;若原条目结尾有 \`[loop]\` 则保留。
|
|
5589
|
+
3) 保留每个 ### 分类标题与分组。只输出 markdown,从 '## Unreleased' 开始,前后不要任何解释。
|
|
5590
|
+
|
|
5591
|
+
【风格锚点·最近版本】
|
|
5592
|
+
${anchors}
|
|
5593
|
+
|
|
5594
|
+
【原始草稿】
|
|
5595
|
+
${raw}"
|
|
5596
|
+
_agent_argv "$agent" text "$prompt" 2>/dev/null || return 1
|
|
5597
|
+
_agent_bypass_claude_perms
|
|
5598
|
+
local out; out=$(mktemp)
|
|
5599
|
+
"${_AGENT_ARGV[@]}" >"$out" 2>/dev/null &
|
|
5600
|
+
local pid=$!
|
|
5601
|
+
local waited=0
|
|
5602
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
5603
|
+
sleep 1; waited=$((waited + 1))
|
|
5604
|
+
if [ "$waited" -ge 150 ]; then
|
|
5605
|
+
kill "$pid" 2>/dev/null; sleep 1; kill -9 "$pid" 2>/dev/null; break
|
|
5606
|
+
fi
|
|
5607
|
+
done
|
|
5608
|
+
wait "$pid" 2>/dev/null
|
|
5609
|
+
# Normalize agent chrome before extracting: some agents (e.g. kimi) prefix the
|
|
5610
|
+
# rendered answer with a "• " marker and indent the body. Changelog markdown is
|
|
5611
|
+
# flat (##, ###, -, blank), so stripping a leading "• " and any leading
|
|
5612
|
+
# whitespace is safe and is a no-op for plain agents (e.g. claude -p). Then
|
|
5613
|
+
# keep from the first '## Unreleased' line onward (drops any preamble).
|
|
5614
|
+
sed -E 's/^[[:space:]]*•[[:space:]]?//; s/^[[:space:]]+//' "$out" \
|
|
5615
|
+
| awk '/^## Unreleased/{f=1} f{print}'
|
|
5616
|
+
rm -f "$out"
|
|
5617
|
+
}
|
|
5618
|
+
|
|
5619
|
+
# FIX-178: replace (or insert) the ## Unreleased section of CHANGELOG.md with
|
|
5620
|
+
# the given draft (which itself begins with '## Unreleased'). getline-from-file
|
|
5621
|
+
# keeps the multi-line draft intact.
|
|
5622
|
+
_changelog_write_unreleased() {
|
|
5623
|
+
local draft="$1" cl="${2:-CHANGELOG.md}"
|
|
5624
|
+
local dfile; dfile=$(mktemp); printf '%s\n' "$draft" > "$dfile"
|
|
5625
|
+
local tmp; tmp=$(mktemp)
|
|
5626
|
+
if [ -f "$cl" ] && grep -q '^## Unreleased' "$cl"; then
|
|
5627
|
+
awk -v df="$dfile" '
|
|
5628
|
+
/^## Unreleased/ && !done { while ((getline line < df) > 0) print line; print ""; skip=1; done=1; next }
|
|
5629
|
+
skip && /^## / { skip=0 }
|
|
5630
|
+
skip { next }
|
|
5631
|
+
{ print }
|
|
5632
|
+
' "$cl" > "$tmp"
|
|
5633
|
+
else
|
|
5634
|
+
{ [ -f "$cl" ] && head -1 "$cl" || echo "# Changelog"; echo; cat "$dfile"; echo;
|
|
5635
|
+
[ -f "$cl" ] && tail -n +2 "$cl"; } > "$tmp"
|
|
5636
|
+
fi
|
|
5637
|
+
mv "$tmp" "$cl"; rm -f "$dfile"
|
|
5638
|
+
}
|
|
5639
|
+
|
|
5580
5640
|
# FIX-113: changelog audit — list PRs merged to main since latest release
|
|
5581
5641
|
# tag that don't appear in CHANGELOG.md's ## Unreleased section.
|
|
5582
5642
|
# US-CL-006: changelog generate — deterministic draft from backlog Done stories.
|
|
5643
|
+
# FIX-178: generate now AI-styles the deterministic draft via the configured
|
|
5644
|
+
# agent by default (content-preserving); --no-ai / --json stay deterministic.
|
|
5583
5645
|
cmd_changelog() {
|
|
5584
|
-
local subcmd="${1:-
|
|
5646
|
+
local subcmd="${1:-generate}"
|
|
5585
5647
|
shift || true
|
|
5586
5648
|
case "$subcmd" in
|
|
5587
|
-
audit)
|
|
5588
|
-
python3 "${ROLL_PKG_DIR}/lib/changelog_audit.py" "$@"
|
|
5589
|
-
;;
|
|
5590
5649
|
generate)
|
|
5591
|
-
|
|
5650
|
+
local want_ai=1 to_write=0 is_json=0 pyargs=()
|
|
5651
|
+
local a
|
|
5652
|
+
for a in "$@"; do
|
|
5653
|
+
case "$a" in
|
|
5654
|
+
--no-ai) want_ai=0 ;;
|
|
5655
|
+
--write) to_write=1 ;;
|
|
5656
|
+
--json) is_json=1; want_ai=0; pyargs+=("$a") ;;
|
|
5657
|
+
*) pyargs+=("$a") ;;
|
|
5658
|
+
esac
|
|
5659
|
+
done
|
|
5660
|
+
local raw
|
|
5661
|
+
raw=$(python3 "${ROLL_PKG_DIR}/lib/changelog_generate.py" "${pyargs[@]}") || return 1
|
|
5662
|
+
if [ "$is_json" = 1 ]; then printf '%s\n' "$raw"; return 0; fi
|
|
5663
|
+
local final="$raw"
|
|
5664
|
+
if [ "$want_ai" = 1 ]; then
|
|
5665
|
+
local styled; styled=$(_changelog_ai_style "$raw" 2>/dev/null || true)
|
|
5666
|
+
if [ -n "$styled" ] && printf '%s' "$styled" | grep -q '^- '; then
|
|
5667
|
+
final="$styled"
|
|
5668
|
+
else
|
|
5669
|
+
warn "changelog: AI 润色不可用/失败,输出确定性草稿(可加 --no-ai 跳过)"
|
|
5670
|
+
fi
|
|
5671
|
+
fi
|
|
5672
|
+
if [ "$to_write" = 1 ]; then
|
|
5673
|
+
_changelog_write_unreleased "$final"
|
|
5674
|
+
info "Updated CHANGELOG.md"
|
|
5675
|
+
else
|
|
5676
|
+
printf '%s\n' "$final"
|
|
5677
|
+
fi
|
|
5592
5678
|
;;
|
|
5593
5679
|
--help|-h|help)
|
|
5594
5680
|
cat <<EOF
|
|
5595
|
-
Usage: roll changelog
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
generate
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
roll changelog audit # check against latest v* tag
|
|
5606
|
-
roll changelog audit --since v2026.520.1
|
|
5607
|
-
roll changelog audit --json # machine-readable
|
|
5608
|
-
roll changelog generate # preview draft to stdout
|
|
5609
|
-
roll changelog generate --write # append draft to CHANGELOG.md
|
|
5610
|
-
roll changelog generate --json # machine-readable
|
|
5681
|
+
Usage: roll changelog generate [options]
|
|
5682
|
+
|
|
5683
|
+
从 backlog ✅ Done 故事 + 上次发布以来的提交,生成 ## Unreleased 发布说明。
|
|
5684
|
+
默认用配置的 agent(roll agent use)按项目风格润色;失败自动回退确定性草稿。
|
|
5685
|
+
|
|
5686
|
+
roll changelog generate # 预览(AI 润色)
|
|
5687
|
+
roll changelog generate --write # 写入 CHANGELOG.md(AI 润色)
|
|
5688
|
+
roll changelog generate --no-ai # 仅确定性草稿,不调 AI
|
|
5689
|
+
roll changelog generate --json # 机器可读(确定性)
|
|
5611
5690
|
EOF
|
|
5612
5691
|
;;
|
|
5613
5692
|
*)
|
|
5614
5693
|
err "$(msg changelog.unknown_subcommand ${subcmd})"
|
|
5615
|
-
err "Try: roll changelog
|
|
5694
|
+
err "Try: roll changelog generate"
|
|
5616
5695
|
return 1
|
|
5617
5696
|
;;
|
|
5618
5697
|
esac
|
|
@@ -8273,6 +8352,7 @@ _write_alert_loop_runner_script() {
|
|
|
8273
8352
|
local script_path="$1" project_path="$2" roll_bin="$3" log_path="$4"
|
|
8274
8353
|
mkdir -p "$(dirname "$script_path")"
|
|
8275
8354
|
local lock="${project_path}/.roll/loop/.alert-loop.lock"
|
|
8355
|
+
local slug; slug=$(_project_slug "${project_path}")
|
|
8276
8356
|
cat > "$script_path" << ALERTRUNNER
|
|
8277
8357
|
#!/bin/bash -l
|
|
8278
8358
|
set -o pipefail
|
|
@@ -8297,6 +8377,15 @@ fi
|
|
|
8297
8377
|
printf '%s:%s\n' "\$\$" "\$(date -u +%s)" > "\$LOCK"
|
|
8298
8378
|
trap 'rm -f "\$LOCK"' EXIT
|
|
8299
8379
|
cd "${project_path}" || exit 0
|
|
8380
|
+
# FIX-171: bake the project-local runtime dir directly; do not rely on
|
|
8381
|
+
# _loop_runtime_dir which may fail to resolve in fresh shells. Set
|
|
8382
|
+
# _LOOP_ALERT so the dispatched roll reads the project-local ALERT file,
|
|
8383
|
+
# but do not override an externally-supplied value (test sandboxes).
|
|
8384
|
+
_LOOP_RT_DIR="${project_path}/.roll/loop"
|
|
8385
|
+
if [ -d "\$_LOOP_RT_DIR" ]; then
|
|
8386
|
+
: "\${_LOOP_ALERT:=\${_LOOP_RT_DIR}/ALERT-${slug}.md}"
|
|
8387
|
+
export _LOOP_ALERT
|
|
8388
|
+
fi
|
|
8300
8389
|
bash "${roll_bin}" _alert_dispatch >> "${log_path}" 2>&1 || true
|
|
8301
8390
|
ALERTRUNNER
|
|
8302
8391
|
chmod +x "$script_path"
|
|
@@ -8559,6 +8648,11 @@ _runs_append() {
|
|
|
8559
8648
|
if [ -n "\$_story_field" ]; then
|
|
8560
8649
|
_stype_field="\${_story_field%%-*}"
|
|
8561
8650
|
fi
|
|
8651
|
+
# US-LOOP-068: target field — roll-meta for roll-meta stories, empty otherwise
|
|
8652
|
+
local _target_field=""
|
|
8653
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
8654
|
+
_target_field="roll-meta"
|
|
8655
|
+
fi
|
|
8562
8656
|
# US-EVAL-002: compute the objective result_eval block from this cycle's
|
|
8563
8657
|
# facts via the US-EVAL-001 pure-function rubric. Best-effort — when python3
|
|
8564
8658
|
# or the scorer script is unavailable the row is written WITHOUT result_eval
|
|
@@ -8608,6 +8702,7 @@ _runs_append() {
|
|
|
8608
8702
|
--arg tier "\$_tier_field" \\
|
|
8609
8703
|
--arg fallback_from "\$_fallback_from_field" \\
|
|
8610
8704
|
--arg story_type "\$_stype_field" \\
|
|
8705
|
+
--arg target "\$_target_field" \\
|
|
8611
8706
|
--argjson built "\$_built" \\
|
|
8612
8707
|
--argjson skipped '[]' \\
|
|
8613
8708
|
--argjson alerts '[]' \\
|
|
@@ -8618,6 +8713,7 @@ _runs_append() {
|
|
|
8618
8713
|
cycle_id:\$cycle_id, agent:\$agent, tier:\$tier, fallback_from:\$fallback_from,
|
|
8619
8714
|
story_type:\$story_type, built:\$built, skipped:\$skipped, alerts:\$alerts,
|
|
8620
8715
|
tcr_count:\$tcr_count, duration_sec:\$duration_sec, phases:\$phases}
|
|
8716
|
+
+ (if \$target == "" then {} else {target:\$target} end)
|
|
8621
8717
|
+ (if \$result_eval == null then {} else {result_eval:\$result_eval} end)' \\
|
|
8622
8718
|
> "\$_tmp" 2>/dev/null || { rm -f "\$_tmp"; return 0; }
|
|
8623
8719
|
# FIX-157: ensure target exists before append; missing file silently drops
|
|
@@ -8849,6 +8945,10 @@ for _orphan_wt in "\${_SHARED_ROOT}/worktrees/${slug}-cycle-"*; do
|
|
|
8849
8945
|
( cd "\$_orphan_wt" && _loop_publish_pr "\$_orphan_branch" "recover orphan \${_orphan_branch}" ) && _orphan_ok=1
|
|
8850
8946
|
fi
|
|
8851
8947
|
if [ "\$_orphan_ok" -eq 1 ]; then
|
|
8948
|
+
# US-LOOP-068: if orphan contains a roll-meta worktree, clean it first
|
|
8949
|
+
if [ -d "\${_orphan_wt}/.roll/.git" ]; then
|
|
8950
|
+
_loop_roll_meta_worktree_cleanup "\$_orphan_wt" "\$_orphan_branch" "${project_path}" 2>/dev/null || true
|
|
8951
|
+
fi
|
|
8852
8952
|
_worktree_cleanup "\$_orphan_wt" "\$_orphan_branch"
|
|
8853
8953
|
echo "[loop] FIX-040: orphan recovered and cleaned: \$_orphan_branch"
|
|
8854
8954
|
else
|
|
@@ -8856,6 +8956,10 @@ for _orphan_wt in "\${_SHARED_ROOT}/worktrees/${slug}-cycle-"*; do
|
|
|
8856
8956
|
fi
|
|
8857
8957
|
else
|
|
8858
8958
|
echo "[loop] FIX-040: orphan worktree \$_orphan_wt has no commits; cleaning up"
|
|
8959
|
+
# US-LOOP-068: if orphan contains a roll-meta worktree, clean it first
|
|
8960
|
+
if [ -d "\${_orphan_wt}/.roll/.git" ]; then
|
|
8961
|
+
_loop_roll_meta_worktree_cleanup "\$_orphan_wt" "\$_orphan_branch" "${project_path}" 2>/dev/null || true
|
|
8962
|
+
fi
|
|
8859
8963
|
_worktree_cleanup "\$_orphan_wt" "\$_orphan_branch"
|
|
8860
8964
|
fi
|
|
8861
8965
|
done
|
|
@@ -8873,6 +8977,16 @@ if _worktree_fetch_origin main \\
|
|
|
8873
8977
|
# because .roll/ is gitignored and the clean clone has no backlog
|
|
8874
8978
|
# for Claude to read or skill entry points to dispatch to.
|
|
8875
8979
|
_worktree_sync_meta "\$WT" 2>/dev/null || true
|
|
8980
|
+
# US-LOOP-068: for roll-meta stories, replace the synced .roll/ with a
|
|
8981
|
+
# real roll-meta git worktree so commits land in roll-meta remote.
|
|
8982
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
8983
|
+
if _loop_roll_meta_worktree_setup "\$WT" "\$BRANCH" "${project_path}" 2>/dev/null; then
|
|
8984
|
+
echo "[loop] cycle \${CYCLE_ID}: roll-meta worktree \${WT}/.roll on \$BRANCH"
|
|
8985
|
+
else
|
|
8986
|
+
echo "[loop] cycle \${CYCLE_ID}: roll-meta worktree setup failed — falling back to normal"
|
|
8987
|
+
_ROLL_META_TARGET=0
|
|
8988
|
+
fi
|
|
8989
|
+
fi
|
|
8876
8990
|
echo "[loop] cycle \${CYCLE_ID}: worktree \$WT on \$BRANCH"
|
|
8877
8991
|
_loop_event cycle_start "\${CYCLE_ID}" "" "" || true
|
|
8878
8992
|
# US-AGENT-006: per-story routing — pick the next Todo, route to an agent
|
|
@@ -8929,6 +9043,15 @@ if _worktree_fetch_origin main \\
|
|
|
8929
9043
|
fi
|
|
8930
9044
|
CYCLE_AGENT="\${ROLL_LOOP_ROUTED_AGENT:-\$(_project_agent)}"
|
|
8931
9045
|
_loop_event agent_used "\${CYCLE_ID}" "\${CYCLE_AGENT}" "primary" || true
|
|
9046
|
+
# US-LOOP-068: detect roll-meta target after routing is complete
|
|
9047
|
+
_ROLL_META_TARGET=0
|
|
9048
|
+
if [ -n "\$ROLL_LOOP_ROUTED_STORY" ]; then
|
|
9049
|
+
if _loop_is_roll_meta_story "\$ROLL_LOOP_ROUTED_STORY" "\${ROLL_MAIN_PROJECT}/.roll/backlog.md" 2>/dev/null; then
|
|
9050
|
+
_ROLL_META_TARGET=1
|
|
9051
|
+
echo "[loop] story \${ROLL_LOOP_ROUTED_STORY} is roll-meta target"
|
|
9052
|
+
fi
|
|
9053
|
+
fi
|
|
9054
|
+
export ROLL_LOOP_ROLL_META_TARGET="\${_ROLL_META_TARGET}"
|
|
8932
9055
|
_phase_end worktree_setup ok
|
|
8933
9056
|
else
|
|
8934
9057
|
# P3 fix: skip the cycle entirely when worktree isolation fails.
|
|
@@ -9071,12 +9194,25 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
9071
9194
|
if [ "\$_exit" -ne 0 ]; then
|
|
9072
9195
|
_cycle_status="failed"
|
|
9073
9196
|
else
|
|
9074
|
-
|
|
9197
|
+
# US-LOOP-068: for roll-meta stories, count commits in the roll-meta worktree
|
|
9198
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
9199
|
+
_cycle_commits_pre=\$(cd "\$WT/.roll" && git rev-list --count origin/main..HEAD 2>/dev/null || echo 0)
|
|
9200
|
+
else
|
|
9201
|
+
_cycle_commits_pre=\$(cd "\$WT" && git rev-list --count origin/main..HEAD 2>/dev/null || echo 0)
|
|
9202
|
+
fi
|
|
9075
9203
|
if [ "\$_cycle_commits_pre" -gt 0 ]; then
|
|
9076
9204
|
_cycle_status="built"
|
|
9077
|
-
|
|
9205
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
9206
|
+
_cycle_tcr=\$(cd "\$WT/.roll" && git log --oneline origin/main..HEAD -- 2>/dev/null | grep -c ' tcr:' || echo 0)
|
|
9207
|
+
else
|
|
9208
|
+
_cycle_tcr=\$(cd "\$WT" && git log --oneline origin/main..HEAD -- 2>/dev/null | grep -c ' tcr:' || echo 0)
|
|
9209
|
+
fi
|
|
9078
9210
|
if command -v jq >/dev/null 2>&1; then
|
|
9079
|
-
|
|
9211
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
9212
|
+
_cycle_built=\$(cd "\$WT/.roll" && git diff origin/main -- 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 "[]")
|
|
9213
|
+
else
|
|
9214
|
+
_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 "[]")
|
|
9215
|
+
fi
|
|
9080
9216
|
fi
|
|
9081
9217
|
fi
|
|
9082
9218
|
fi
|
|
@@ -9097,8 +9233,17 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
9097
9233
|
if [ "\$_exit" -eq 0 ]; then
|
|
9098
9234
|
# Idle cycle — no commits ahead of origin/main means nothing was built;
|
|
9099
9235
|
# skip publish and reclaim the worktree immediately.
|
|
9100
|
-
|
|
9236
|
+
# US-LOOP-068: for roll-meta stories, count commits in roll-meta worktree
|
|
9237
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
9238
|
+
_cycle_commits=\$(cd "\$WT/.roll" && git rev-list --count origin/main..HEAD 2>/dev/null || echo 0)
|
|
9239
|
+
else
|
|
9240
|
+
_cycle_commits=\$(cd "\$WT" && git rev-list --count origin/main..HEAD 2>/dev/null || echo 0)
|
|
9241
|
+
fi
|
|
9101
9242
|
if [ "\$_cycle_commits" -eq 0 ]; then
|
|
9243
|
+
# US-LOOP-068: clean up roll-meta worktree before product worktree
|
|
9244
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
9245
|
+
_loop_roll_meta_worktree_cleanup "\$WT" "\$BRANCH" "${project_path}" 2>/dev/null || true
|
|
9246
|
+
fi
|
|
9102
9247
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
9103
9248
|
_loop_event idle "\${CYCLE_ID}" "" "" || true
|
|
9104
9249
|
# FIX-F (2026-05-25): explicitly write the terminal "idle" cycle_end +
|
|
@@ -9115,12 +9260,42 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
9115
9260
|
echo "[loop] cycle \${CYCLE_ID}: idle (no new commits); worktree cleaned"
|
|
9116
9261
|
else
|
|
9117
9262
|
_is_doc_only=0
|
|
9118
|
-
|
|
9263
|
+
# US-LOOP-068: for roll-meta stories, doc-only check runs in roll-meta worktree
|
|
9264
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
9265
|
+
( cd "\$WT/.roll" && _loop_is_doc_only_change ) && _is_doc_only=1
|
|
9266
|
+
else
|
|
9267
|
+
( cd "\$WT" && _loop_is_doc_only_change ) && _is_doc_only=1
|
|
9268
|
+
fi
|
|
9269
|
+
# US-LOOP-069: roll-meta boundary guard — abort if a roll-meta story touched product files
|
|
9270
|
+
if ! _loop_guard_roll_meta_boundary "\$WT" "\$ROLL_LOOP_ROUTED_STORY"; then
|
|
9271
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true; _CYCLE_END_WRITTEN=1
|
|
9272
|
+
_phases_guard=\$(_phases_to_json 2>/dev/null); [ -z "\$_phases_guard" ] && _phases_guard='{}'
|
|
9273
|
+
_runs_append "failed" 0 "[]" "\$_phases_guard" 2>/dev/null || true
|
|
9274
|
+
echo "[loop] cycle \${CYCLE_ID}: US-LOOP-069 blocked — roll-meta story touched product files; worktree preserved at \$WT"
|
|
9275
|
+
trap '_inner_cleanup' EXIT
|
|
9276
|
+
exit 0
|
|
9277
|
+
fi
|
|
9278
|
+
# US-LOOP-068: roll-meta test gate — run roll-meta tests when ops/ changed
|
|
9279
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
9280
|
+
if ! _loop_roll_meta_test_gate "\$WT"; then
|
|
9281
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true; _CYCLE_END_WRITTEN=1
|
|
9282
|
+
_phases_test=\$(_phases_to_json 2>/dev/null); [ -z "\$_phases_test" ] && _phases_test='{}'
|
|
9283
|
+
_runs_append "failed" 0 "[]" "\$_phases_test" 2>/dev/null || true
|
|
9284
|
+
echo "[loop] cycle \${CYCLE_ID}: roll-meta test gate failed; worktree preserved at \$WT"
|
|
9285
|
+
trap '_inner_cleanup' EXIT
|
|
9286
|
+
exit 0
|
|
9287
|
+
fi
|
|
9288
|
+
fi
|
|
9119
9289
|
_phase_begin publish_push
|
|
9120
|
-
|
|
9121
|
-
|
|
9290
|
+
# US-LOOP-068: roll-meta stories publish to roll-meta remote
|
|
9291
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
9292
|
+
_loop_roll_meta_publish "\$WT" "\$BRANCH" "loop cycle \${CYCLE_ID}"
|
|
9122
9293
|
else
|
|
9123
|
-
|
|
9294
|
+
if [ "\$_is_doc_only" -eq 1 ]; then
|
|
9295
|
+
( cd "\$WT" && _loop_publish_doc_pr "\$BRANCH" "doc: loop cycle \${CYCLE_ID}" )
|
|
9296
|
+
else
|
|
9297
|
+
( cd "\$WT" && _loop_publish_pr "\$BRANCH" "loop cycle \${CYCLE_ID}" )
|
|
9298
|
+
fi
|
|
9124
9299
|
fi
|
|
9125
9300
|
_publish_status=\$?
|
|
9126
9301
|
if [ "\$_publish_status" -eq 0 ]; then
|
|
@@ -9140,6 +9315,10 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
9140
9315
|
# so dashboard renders #NN ✓/↩/… correctly. Must run while branch ref
|
|
9141
9316
|
# is still resolvable on remote — gh pr view <branch> needs the head ref.
|
|
9142
9317
|
_loop_emit_pr_final "\$BRANCH" 2>/dev/null || true
|
|
9318
|
+
# US-LOOP-068: clean up roll-meta worktree before product worktree
|
|
9319
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
9320
|
+
_loop_roll_meta_worktree_cleanup "\$WT" "\$BRANCH" "${project_path}" 2>/dev/null || true
|
|
9321
|
+
fi
|
|
9143
9322
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
9144
9323
|
_loop_event cycle_end "\${CYCLE_ID}" "" "done" || true; _CYCLE_END_WRITTEN=1
|
|
9145
9324
|
# US-OBS-014: normal-completion path — push a fresh status snapshot.
|
|
@@ -9147,15 +9326,42 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
9147
9326
|
_phase_end cleanup ok
|
|
9148
9327
|
echo "[loop] cycle \${CYCLE_ID}: published; worktree cleaned"
|
|
9149
9328
|
elif [ "\$_publish_status" -eq 2 ]; then
|
|
9150
|
-
|
|
9329
|
+
# US-LOOP-068: for roll-meta, merge_back doesn't apply — skip straight to
|
|
9330
|
+
# the orphan-push safety net (roll-meta commits live on a different remote).
|
|
9331
|
+
_merged_back=0
|
|
9332
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
9333
|
+
:
|
|
9334
|
+
elif ( cd "${project_path}" && _worktree_merge_back "\$BRANCH" ); then
|
|
9151
9335
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
9152
9336
|
# US-LOOP-005 T3: gh unavailable + ff merge_back OK → cycle_end done
|
|
9153
9337
|
_loop_event cycle_end "\${CYCLE_ID}" "" "done" || true; _CYCLE_END_WRITTEN=1
|
|
9154
9338
|
echo "[loop] cycle \${CYCLE_ID}: gh unavailable; merged via ff and cleaned up"
|
|
9339
|
+
_merged_back=1
|
|
9340
|
+
fi
|
|
9341
|
+
# FIX-039: gh unavailable + merge_back failed — push orphan branch+tag to origin
|
|
9342
|
+
# as final safety net so code is never local-only before worktree cleanup.
|
|
9343
|
+
# Skip entirely when merge_back already finalized the cycle (the worktree is
|
|
9344
|
+
# gone, so re-pushing from \$WT would spuriously fail and raise a false alert).
|
|
9345
|
+
# US-LOOP-068: for roll-meta, push from roll-meta worktree
|
|
9346
|
+
if [ "\$_merged_back" -ne 1 ]; then
|
|
9347
|
+
_orphan_tag="loop-orphan-\${CYCLE_ID}"
|
|
9348
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
9349
|
+
if ( cd "\$WT/.roll" && git push origin "\$BRANCH" 2>/dev/null \
|
|
9350
|
+
&& git tag "\$_orphan_tag" 2>/dev/null \
|
|
9351
|
+
&& git push origin "\$_orphan_tag" 2>/dev/null ); then
|
|
9352
|
+
_loop_roll_meta_worktree_cleanup "\$WT" "\$BRANCH" "${project_path}" 2>/dev/null || true
|
|
9353
|
+
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
9354
|
+
# US-LOOP-005 T4: gh unavailable + orphan push OK → cycle_end orphan
|
|
9355
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "orphan" || true; _CYCLE_END_WRITTEN=1
|
|
9356
|
+
_worktree_alert "cycle \${CYCLE_ID}: gh+merge_back failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
|
|
9357
|
+
echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
|
|
9358
|
+
else
|
|
9359
|
+
# US-LOOP-005 T5: gh unavailable + all failed → cycle_end failed
|
|
9360
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true; _CYCLE_END_WRITTEN=1
|
|
9361
|
+
_worktree_alert "cycle \${CYCLE_ID}: gh+merge_back+push all failed; worktree preserved at \$WT"
|
|
9362
|
+
echo "[loop] cycle \${CYCLE_ID}: all publish paths failed; worktree preserved at \$WT"
|
|
9363
|
+
fi
|
|
9155
9364
|
else
|
|
9156
|
-
# FIX-039: gh unavailable + merge_back failed — push orphan branch+tag to origin
|
|
9157
|
-
# as final safety net so code is never local-only before worktree cleanup.
|
|
9158
|
-
_orphan_tag="loop-orphan-\${CYCLE_ID}"
|
|
9159
9365
|
if ( cd "\$WT" && git push origin "\$BRANCH" 2>/dev/null \
|
|
9160
9366
|
&& git tag "\$_orphan_tag" 2>/dev/null \
|
|
9161
9367
|
&& git push origin "\$_orphan_tag" 2>/dev/null ); then
|
|
@@ -9171,23 +9377,43 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
9171
9377
|
echo "[loop] cycle \${CYCLE_ID}: all publish paths failed; worktree preserved at \$WT"
|
|
9172
9378
|
fi
|
|
9173
9379
|
fi
|
|
9380
|
+
fi
|
|
9174
9381
|
else
|
|
9175
9382
|
# FIX-039: PR publish failed — push orphan branch+tag to origin as safety net.
|
|
9176
9383
|
# (_loop_publish_pr may have already pushed the branch; git push is idempotent.)
|
|
9384
|
+
# US-LOOP-068: for roll-meta, push from roll-meta worktree
|
|
9177
9385
|
_orphan_tag="loop-orphan-\${CYCLE_ID}"
|
|
9178
|
-
if
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9386
|
+
if [ "\${_ROLL_META_TARGET:-0}" = "1" ]; then
|
|
9387
|
+
if ( cd "\$WT/.roll" && git push origin "\$BRANCH" 2>/dev/null \
|
|
9388
|
+
&& git tag "\$_orphan_tag" 2>/dev/null \
|
|
9389
|
+
&& git push origin "\$_orphan_tag" 2>/dev/null ); then
|
|
9390
|
+
_loop_roll_meta_worktree_cleanup "\$WT" "\$BRANCH" "${project_path}" 2>/dev/null || true
|
|
9391
|
+
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
9392
|
+
# US-LOOP-005 T6: PR publish failed + orphan push OK → cycle_end orphan
|
|
9393
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "orphan" || true; _CYCLE_END_WRITTEN=1
|
|
9394
|
+
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
|
|
9395
|
+
echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
|
|
9396
|
+
else
|
|
9397
|
+
# US-LOOP-005 T7: PR publish failed + orphan push failed → cycle_end failed
|
|
9398
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true; _CYCLE_END_WRITTEN=1
|
|
9399
|
+
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT (branch \$BRANCH)"
|
|
9400
|
+
echo "[loop] cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT"
|
|
9401
|
+
fi
|
|
9186
9402
|
else
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9403
|
+
if ( cd "\$WT" && git push origin "\$BRANCH" 2>/dev/null \
|
|
9404
|
+
&& git tag "\$_orphan_tag" 2>/dev/null \
|
|
9405
|
+
&& git push origin "\$_orphan_tag" 2>/dev/null ); then
|
|
9406
|
+
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
9407
|
+
# US-LOOP-005 T6: PR publish failed + orphan push OK → cycle_end orphan
|
|
9408
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "orphan" || true; _CYCLE_END_WRITTEN=1
|
|
9409
|
+
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
|
|
9410
|
+
echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
|
|
9411
|
+
else
|
|
9412
|
+
# US-LOOP-005 T7: PR publish failed + orphan push failed → cycle_end failed
|
|
9413
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true; _CYCLE_END_WRITTEN=1
|
|
9414
|
+
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT (branch \$BRANCH)"
|
|
9415
|
+
echo "[loop] cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT"
|
|
9416
|
+
fi
|
|
9191
9417
|
fi
|
|
9192
9418
|
fi
|
|
9193
9419
|
fi
|
|
@@ -11434,7 +11660,10 @@ _loop_pr_do_heal() {
|
|
|
11434
11660
|
_loop_pr_merge_approved() {
|
|
11435
11661
|
local num="$1" ci_state="$2" mergeable="$3" slug="$4"
|
|
11436
11662
|
[ -n "$num" ] && [ -n "$slug" ] || return 0
|
|
11437
|
-
[ "$ci_state" = "success" ]
|
|
11663
|
+
[ "$ci_state" = "success" ] || return 0
|
|
11664
|
+
# MERGEABLE (GraphQL mergeable) or CLEAN (mergeStateStatus) — see
|
|
11665
|
+
# _loop_pr_merge_self_eager for why both spellings must be accepted.
|
|
11666
|
+
case "$mergeable" in MERGEABLE|CLEAN) ;; *) return 0 ;; esac
|
|
11438
11667
|
if gh -R "$slug" pr merge "$num" --squash --delete-branch >/dev/null 2>&1; then
|
|
11439
11668
|
info "PR #${num}: human-approved + CI green — merged"
|
|
11440
11669
|
else
|
|
@@ -11490,7 +11719,7 @@ EOF
|
|
|
11490
11719
|
# can enforce them at Step 2 (story pickup). Pure functions, no side effects.
|
|
11491
11720
|
#
|
|
11492
11721
|
# BACKLOG row format (relevant fragments):
|
|
11493
|
-
# | [US-AUTO-033](...) | desc `depends-on:US-AUTO-037`
|
|
11722
|
+
# | [US-AUTO-033](...) | desc `depends-on:US-AUTO-037` | 📋 Todo |
|
|
11494
11723
|
# | FIX-100 | desc `depends-on:US-A,US-B` | 📋 Todo |
|
|
11495
11724
|
#
|
|
11496
11725
|
# Row matching is anchored on `^| [?<id>\b` so a story-id appearing in some
|
|
@@ -11539,15 +11768,14 @@ _loop_check_depends_on() {
|
|
|
11539
11768
|
return 0
|
|
11540
11769
|
}
|
|
11541
11770
|
|
|
11542
|
-
#
|
|
11543
|
-
#
|
|
11544
|
-
#
|
|
11545
|
-
#
|
|
11546
|
-
#
|
|
11547
|
-
#
|
|
11548
|
-
#
|
|
11549
|
-
|
|
11550
|
-
_loop_is_manual_only() {
|
|
11771
|
+
# FIX-172: detect whether a story routes its output to roll-meta — path-based,
|
|
11772
|
+
# never tag-based. A story is a roll-meta target iff its declared deliverable
|
|
11773
|
+
# **Files:** include a path under .roll/ that is NOT a status-management file
|
|
11774
|
+
# (.roll/backlog.md, .roll/features/). Every cycle flips its own backlog/feature
|
|
11775
|
+
# status under .roll/, so "touches .roll/" must NOT flag a product story; only a
|
|
11776
|
+
# genuine roll-meta deliverable (e.g. .roll/ops/watch.sh) counts.
|
|
11777
|
+
# Returns 0 when the story delivers to roll-meta.
|
|
11778
|
+
_loop_is_roll_meta_story() {
|
|
11551
11779
|
local id="$1"
|
|
11552
11780
|
local backlog="${2:-.roll/backlog.md}"
|
|
11553
11781
|
[ -n "$id" ] || return 1
|
|
@@ -11557,7 +11785,30 @@ _loop_is_manual_only() {
|
|
|
11557
11785
|
row=$(grep -E "^\| \[?${id}[]| ]" "$backlog" | head -1)
|
|
11558
11786
|
[ -n "$row" ] || return 1
|
|
11559
11787
|
|
|
11560
|
-
|
|
11788
|
+
# Resolve the feature file from the row's markdown link: [id](path.md#anchor).
|
|
11789
|
+
# Links appear in two relative forms — ".roll/features/x.md" (rel. to product
|
|
11790
|
+
# root) or "features/x.md" (rel. to .roll). Normalise from "features/" onward
|
|
11791
|
+
# and resolve against the backlog's own directory (the roll-meta root).
|
|
11792
|
+
local link featrel metadir feat
|
|
11793
|
+
link=$(printf '%s\n' "$row" | grep -oE '\([^)]+\.md' | head -1 | sed -E 's/^\(//')
|
|
11794
|
+
[ -n "$link" ] || return 1
|
|
11795
|
+
featrel=$(printf '%s\n' "$link" | sed -E 's#^.*(features/.*)#\1#')
|
|
11796
|
+
metadir=$(dirname "$backlog")
|
|
11797
|
+
feat="${metadir}/${featrel}"
|
|
11798
|
+
[ -f "$feat" ] || return 1
|
|
11799
|
+
|
|
11800
|
+
# Extract this story's **Files:** block, then test for a roll-meta deliverable
|
|
11801
|
+
# path (under .roll/ but not the universal status-management files).
|
|
11802
|
+
awk -v id="$id" '
|
|
11803
|
+
$0 ~ ("^## " id "( |$)") {insec=1; next}
|
|
11804
|
+
insec && /^## / {exit}
|
|
11805
|
+
insec && /^\*\*Files:\*\*/ {infiles=1; next}
|
|
11806
|
+
insec && infiles && /^\*\*/ {exit}
|
|
11807
|
+
insec && infiles {print}
|
|
11808
|
+
' "$feat" \
|
|
11809
|
+
| grep -oE '\.roll/[A-Za-z0-9_./-]+' \
|
|
11810
|
+
| grep -vE '^\.roll/backlog\.md$|^\.roll/features/' \
|
|
11811
|
+
| grep -q .
|
|
11561
11812
|
}
|
|
11562
11813
|
|
|
11563
11814
|
# US-AUTO-034: PR-first inbox — loop processes open PRs before scanning BACKLOG.
|
|
@@ -11598,6 +11849,17 @@ _loop_pr_classify() {
|
|
|
11598
11849
|
fi
|
|
11599
11850
|
echo "loop_self"; return 0
|
|
11600
11851
|
;;
|
|
11852
|
+
claude/*)
|
|
11853
|
+
# Claude-agent-authored PRs are loop-owned for autonomous merge/rebase
|
|
11854
|
+
# once green — same treatment as loop/* — so they close within a
|
|
11855
|
+
# PR-loop tick instead of waiting on a human or a GHA bot review.
|
|
11856
|
+
# CI-red claude/* PRs are deliberately NOT routed to background heal
|
|
11857
|
+
# (no agent re-spawn); they fall through to the stale/eligible paths
|
|
11858
|
+
# below so a human decides what to do with a failing run.
|
|
11859
|
+
if [[ "$ci_state" != "failure" ]]; then
|
|
11860
|
+
echo "loop_self"; return 0
|
|
11861
|
+
fi
|
|
11862
|
+
;;
|
|
11601
11863
|
esac
|
|
11602
11864
|
|
|
11603
11865
|
case "$human_review" in
|
|
@@ -11605,7 +11867,10 @@ _loop_pr_classify() {
|
|
|
11605
11867
|
APPROVED) echo "blocked_human_approved"; return 0 ;;
|
|
11606
11868
|
esac
|
|
11607
11869
|
|
|
11608
|
-
|
|
11870
|
+
# CONFLICTING is the GraphQL `mergeable` enum; DIRTY/BEHIND are
|
|
11871
|
+
# `mergeStateStatus` values (_loop_pr_inbox feeds the latter). Accept both
|
|
11872
|
+
# spellings so a conflicting/out-of-date PR is reliably routed to rebase.
|
|
11873
|
+
if [ "$ci_state" = "failure" ] || [ "$mergeable" = "CONFLICTING" ] || [ "$mergeable" = "DIRTY" ] || [ "$mergeable" = "BEHIND" ]; then
|
|
11609
11874
|
echo "stale"
|
|
11610
11875
|
return 0
|
|
11611
11876
|
fi
|
|
@@ -11795,7 +12060,12 @@ _loop_pr_rebase_stale() {
|
|
|
11795
12060
|
# spec name. Both coexist until Phase 2 collapses the inbox path.
|
|
11796
12061
|
_loop_pr_merge_self_eager() {
|
|
11797
12062
|
local num="$1" ci_state="$2" mergeable="$3" slug="$4"
|
|
11798
|
-
[ "$ci_state" = "success" ]
|
|
12063
|
+
[ "$ci_state" = "success" ] || return 0
|
|
12064
|
+
# Accept both the GraphQL `mergeable` enum (MERGEABLE) and the
|
|
12065
|
+
# `mergeStateStatus` enum (CLEAN). _loop_pr_inbox feeds mergeStateStatus, so
|
|
12066
|
+
# a ready-to-merge PR arrives as CLEAN in production — without it the eager
|
|
12067
|
+
# merge never fired and self-PRs silently waited on repo-level auto-merge.
|
|
12068
|
+
case "$mergeable" in MERGEABLE|CLEAN) ;; *) return 0 ;; esac
|
|
11799
12069
|
gh -R "$slug" pr merge "$num" --squash --delete-branch >/dev/null 2>&1 \
|
|
11800
12070
|
&& info "PR #${num}: loop_self CI green — merged" \
|
|
11801
12071
|
|| warn "PR #${num}: loop_self merge failed — left open"
|
|
@@ -11847,7 +12117,7 @@ _loop_pr_inbox() {
|
|
|
11847
12117
|
# All gates cleared (bot-approved + CI green + no conflicts) → merge directly.
|
|
11848
12118
|
# Relying on repo-level auto-merge being configured is not reliable; loop
|
|
11849
12119
|
# owns the decision here since it already ran the review.
|
|
11850
|
-
if [ "$ci_state" = "success" ] && [ "$mergeable" = "MERGEABLE" ]; then
|
|
12120
|
+
if [ "$ci_state" = "success" ] && { [ "$mergeable" = "MERGEABLE" ] || [ "$mergeable" = "CLEAN" ]; }; then
|
|
11851
12121
|
gh -R "$slug" pr merge "$num" --squash --delete-branch >/dev/null 2>&1 \
|
|
11852
12122
|
&& info "PR #${num}: bot-approved + CI green — merged" \
|
|
11853
12123
|
|| warn "PR #${num}: merge failed (bot-approved + CI green) — left open"
|
|
@@ -11866,7 +12136,21 @@ _loop_pr_inbox() {
|
|
|
11866
12136
|
|
|
11867
12137
|
case "$verdict" in
|
|
11868
12138
|
loop_self)
|
|
11869
|
-
|
|
12139
|
+
# Green self-PR: merge when clean, else rebase onto main first. A
|
|
12140
|
+
# loop/* or claude/* PR that fell BEHIND or now CONFLICTS with main can
|
|
12141
|
+
# never auto-merge until rebased — eager-merge alone would leave it
|
|
12142
|
+
# stuck open forever. Rebase is circuit-gated (≥3 attempts/24h → ALERT)
|
|
12143
|
+
# and merges on a later tick once the rebased head is green + clean.
|
|
12144
|
+
case "$mergeable" in
|
|
12145
|
+
BEHIND|DIRTY|CONFLICTING)
|
|
12146
|
+
if _loop_pr_rebase_circuit "$num"; then
|
|
12147
|
+
_loop_pr_rebase_stale "$num" "$head_ref" || true
|
|
12148
|
+
fi
|
|
12149
|
+
;;
|
|
12150
|
+
*)
|
|
12151
|
+
_loop_pr_merge_self_eager "$num" "$ci_state" "$mergeable" "$slug"
|
|
12152
|
+
;;
|
|
12153
|
+
esac
|
|
11870
12154
|
;;
|
|
11871
12155
|
loop_self_ci_red)
|
|
11872
12156
|
# US-LOOP-062a: a red loop/* PR (classified by US-LOOP-049) is now
|
|
@@ -11902,7 +12186,7 @@ _loop_pr_inbox() {
|
|
|
11902
12186
|
#
|
|
11903
12187
|
# Loop-safe building blocks for the future PR Loop (com.roll.pr.<slug>.plist,
|
|
11904
12188
|
# 5-min cadence). Phase 1 ships the helpers + tests only; Phase 2 (runner /
|
|
11905
|
-
# plist / main-loop wiring) is
|
|
12189
|
+
# plist / main-loop wiring) is wired by hand and out of scope here.
|
|
11906
12190
|
#
|
|
11907
12191
|
# All helpers are lenient on a missing `gh` binary or any gh failure: they
|
|
11908
12192
|
# return 0 so a loop iteration never aborts on a single bad PR. State writes
|
|
@@ -12091,7 +12375,7 @@ _loop_pr_route() {
|
|
|
12091
12375
|
# These six helpers collect CI timing data, classify failures, auto-rerun
|
|
12092
12376
|
# transient flakes, and surface flaky / degradation signals as backlog
|
|
12093
12377
|
# entries. They are NOT yet wired into any runner or launchd plist — that is
|
|
12094
|
-
# Phase 2 (
|
|
12378
|
+
# Phase 2 (wired by hand). Each is unit-tested in
|
|
12095
12379
|
# tests/unit/roll_loop_ci_loop.bats with gh stubbed. Do not delete or inline.
|
|
12096
12380
|
#
|
|
12097
12381
|
# State lives under project-local .roll/state/:
|
|
@@ -12125,9 +12409,20 @@ _ci_record_timing() {
|
|
|
12125
12409
|
local dir; dir=$(_ci_state_dir)
|
|
12126
12410
|
local file="${dir}/ci-timing.jsonl"
|
|
12127
12411
|
|
|
12128
|
-
# Idempotency: skip if this run_id is already recorded
|
|
12412
|
+
# Idempotency: skip if this run_id is already recorded with a non-empty
|
|
12413
|
+
# conclusion. If the existing record has an empty conclusion and the new
|
|
12414
|
+
# data has a conclusion, update in-place so in-progress runs are completed.
|
|
12129
12415
|
if [ -f "$file" ] && grep -q "\"run_id\":${run_id}," "$file" 2>/dev/null; then
|
|
12130
|
-
|
|
12416
|
+
local existing_conclusion new_conclusion
|
|
12417
|
+
existing_conclusion=$(grep "\"run_id\":${run_id}," "$file" 2>/dev/null | jq -r '.conclusion // ""' 2>/dev/null)
|
|
12418
|
+
new_conclusion=$(echo "$json" | jq -r '.conclusion // ""' 2>/dev/null)
|
|
12419
|
+
if [ -n "$existing_conclusion" ] || [ -z "$new_conclusion" ]; then
|
|
12420
|
+
return 0
|
|
12421
|
+
fi
|
|
12422
|
+
# Remove the stale line so the new record can be appended below.
|
|
12423
|
+
local tmpfile="${file}.tmp.$$"
|
|
12424
|
+
grep -v "\"run_id\":${run_id}," "$file" > "$tmpfile" 2>/dev/null || true
|
|
12425
|
+
mv "$tmpfile" "$file"
|
|
12131
12426
|
fi
|
|
12132
12427
|
|
|
12133
12428
|
workflow=$(echo "$json" | jq -r '.workflowName // .name // ""' 2>/dev/null)
|
|
@@ -12446,7 +12741,7 @@ _ci_scan() {
|
|
|
12446
12741
|
# that every loop appends to but nobody reads. The Alert Loop turns it into a
|
|
12447
12742
|
# real consumer: parse → dedup (1h per category) → notify (error always) →
|
|
12448
12743
|
# log → rotate. They are NOT yet wired into any runner or launchd plist — that
|
|
12449
|
-
# is Phase 2 (
|
|
12744
|
+
# is Phase 2 (wired by hand). Each is unit-tested in
|
|
12450
12745
|
# tests/unit/roll_loop_alert_loop.bats with _notify stubbed. Do not delete or
|
|
12451
12746
|
# inline.
|
|
12452
12747
|
#
|
|
@@ -12856,7 +13151,13 @@ _skill_write_self_score() {
|
|
|
12856
13151
|
*) err "_skill_write_self_score: verdict must be good / ok / regression (got '$verdict')"; return 1 ;;
|
|
12857
13152
|
esac
|
|
12858
13153
|
|
|
13154
|
+
# FIX-176: anchor notes to <project>/.roll/notes regardless of cwd. When
|
|
13155
|
+
# invoked with cwd already inside the .roll dir (e.g. a self-score run from
|
|
13156
|
+
# within .roll), the bare ".roll/notes" doubled to .roll/.roll/notes.
|
|
12859
13157
|
local notes_dir=".roll/notes"
|
|
13158
|
+
if [ "$(basename "$PWD")" = ".roll" ]; then
|
|
13159
|
+
notes_dir="notes"
|
|
13160
|
+
fi
|
|
12860
13161
|
mkdir -p "$notes_dir"
|
|
12861
13162
|
local ts; ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
12862
13163
|
local date_part="${ts%%T*}"
|
|
@@ -13113,7 +13414,7 @@ _changelog_audit_gate() {
|
|
|
13113
13414
|
# US-AUTO-036: worktree helpers (loop-safe pure additions).
|
|
13114
13415
|
#
|
|
13115
13416
|
# Phase 1 of worktree isolation — these helpers are NOT yet called by
|
|
13116
|
-
# runner.sh. US-AUTO-037 (
|
|
13417
|
+
# runner.sh. US-AUTO-037 (wired by hand) wires them into
|
|
13117
13418
|
# _write_loop_runner_script. Do not delete or inline; they are unit-tested
|
|
13118
13419
|
# in tests/unit/roll_worktree.bats.
|
|
13119
13420
|
|
|
@@ -13214,6 +13515,58 @@ _worktree_sync_meta() {
|
|
|
13214
13515
|
fi
|
|
13215
13516
|
}
|
|
13216
13517
|
|
|
13518
|
+
# US-LOOP-068: set up .roll/ inside the product worktree as a roll-meta git worktree.
|
|
13519
|
+
# Must be called after the product worktree is created.
|
|
13520
|
+
# Returns 0 on success, 1 on failure.
|
|
13521
|
+
_loop_roll_meta_worktree_setup() {
|
|
13522
|
+
local wt="$1" branch="$2" project_path="$3"
|
|
13523
|
+
local meta_repo="${project_path}/.roll"
|
|
13524
|
+
[ -d "$meta_repo/.git" ] || { echo "[loop] roll-meta setup: ${meta_repo} is not a git repo"; return 1; }
|
|
13525
|
+
rm -rf "${wt}/.roll"
|
|
13526
|
+
git -C "$meta_repo" worktree add "${wt}/.roll" -b "$branch" "origin/main"
|
|
13527
|
+
}
|
|
13528
|
+
|
|
13529
|
+
# US-LOOP-068: clean up the roll-meta worktree inside the product worktree.
|
|
13530
|
+
_loop_roll_meta_worktree_cleanup() {
|
|
13531
|
+
local wt="$1" branch="$2" project_path="$3"
|
|
13532
|
+
local meta_repo="${project_path}/.roll"
|
|
13533
|
+
[ -d "$meta_repo/.git" ] || return 0
|
|
13534
|
+
git -C "$meta_repo" worktree remove --force "${wt}/.roll" 2>/dev/null || true
|
|
13535
|
+
rm -rf "${wt}/.roll" 2>/dev/null || true
|
|
13536
|
+
git -C "$meta_repo" branch -D "$branch" 2>/dev/null || true
|
|
13537
|
+
}
|
|
13538
|
+
|
|
13539
|
+
# US-LOOP-068: publish a PR from the roll-meta worktree.
|
|
13540
|
+
_loop_roll_meta_publish() {
|
|
13541
|
+
local wt="$1" branch="$2" title="$3"
|
|
13542
|
+
(cd "${wt}/.roll" && _loop_publish_pr "$branch" "$title")
|
|
13543
|
+
}
|
|
13544
|
+
|
|
13545
|
+
# US-LOOP-068: run roll-meta test gate when .roll/ops/ files are modified.
|
|
13546
|
+
# Returns 0 when no ops changes or tests pass; 1 when tests fail.
|
|
13547
|
+
_loop_roll_meta_test_gate() {
|
|
13548
|
+
local wt="$1"
|
|
13549
|
+
local _ops_changed
|
|
13550
|
+
_ops_changed=$(cd "${wt}/.roll" && git diff --name-only origin/main..HEAD 2>/dev/null | grep '^ops/' || true)
|
|
13551
|
+
[ -n "$_ops_changed" ] || return 0
|
|
13552
|
+
if ! command -v bats >/dev/null 2>&1; then
|
|
13553
|
+
echo "[loop] roll-meta ops/ changed but bats not installed — skipping test gate"
|
|
13554
|
+
return 0
|
|
13555
|
+
fi
|
|
13556
|
+
local _test_dir="${wt}/.roll/ops/tests"
|
|
13557
|
+
if [ ! -d "$_test_dir" ]; then
|
|
13558
|
+
echo "[loop] roll-meta ops/ changed but no $_test_dir found — skipping test gate"
|
|
13559
|
+
return 0
|
|
13560
|
+
fi
|
|
13561
|
+
echo "[loop] roll-meta test gate: bats ${_test_dir}"
|
|
13562
|
+
if ! bats "${_test_dir}" 2>/dev/null; then
|
|
13563
|
+
echo "[loop] roll-meta test gate failed"
|
|
13564
|
+
return 1
|
|
13565
|
+
fi
|
|
13566
|
+
echo "[loop] roll-meta test gate passed"
|
|
13567
|
+
return 0
|
|
13568
|
+
}
|
|
13569
|
+
|
|
13217
13570
|
# _worktree_merge_back <branch>
|
|
13218
13571
|
# Caller must be in the main worktree (cwd = main). Steps:
|
|
13219
13572
|
# 1. git pull --ff-only origin main (sync local main with remote)
|
|
@@ -13412,8 +13765,8 @@ refs/heads/claude/*"
|
|
|
13412
13765
|
# v1 type/est/risk routing). lib/agent_routes_lint.py is no longer invoked.
|
|
13413
13766
|
|
|
13414
13767
|
# FIX-146: per-story eligibility gate, reusable for pick and re-validation.
|
|
13415
|
-
# Exit 0 when the story id is eligible to be worked (📋 Todo,
|
|
13416
|
-
#
|
|
13768
|
+
# Exit 0 when the story id is eligible to be worked (📋 Todo, deps satisfied,
|
|
13769
|
+
# no open PR). Exit 1 otherwise.
|
|
13417
13770
|
# Args: <story-id> [backlog-path] [open-pr-titles]
|
|
13418
13771
|
_loop_story_is_eligible() {
|
|
13419
13772
|
local id="$1"
|
|
@@ -13431,13 +13784,10 @@ _loop_story_is_eligible() {
|
|
|
13431
13784
|
_status=$(printf '%s\n' "$row" | awk -F'|' '{for(i=NF;i>=1;i--) if($i ~ /[^ \t]/) {gsub(/^[ \t]+|[ \t]+$/, "", $i); print $i; exit}}')
|
|
13432
13785
|
[ "$_status" = "📋 Todo" ] || return 1
|
|
13433
13786
|
|
|
13434
|
-
# Gate 1:
|
|
13435
|
-
_loop_is_manual_only "$id" "$backlog" 2>/dev/null && return 1
|
|
13436
|
-
|
|
13437
|
-
# Gate 2: depends-on
|
|
13787
|
+
# Gate 1: depends-on
|
|
13438
13788
|
_loop_check_depends_on "$id" "$backlog" >/dev/null 2>&1 || return 1
|
|
13439
13789
|
|
|
13440
|
-
# Gate
|
|
13790
|
+
# Gate 2 (FIX-141): skip if an open PR already references this story id.
|
|
13441
13791
|
if [ -n "$open_pr_titles" ] && printf '%s\n' "$open_pr_titles" | grep -qE "${id}([^0-9A-Za-z]|$)"; then
|
|
13442
13792
|
return 1
|
|
13443
13793
|
fi
|
|
@@ -13448,9 +13798,8 @@ _loop_story_is_eligible() {
|
|
|
13448
13798
|
# US-AGENT-006: pick the next eligible 📋 Todo story from .roll/backlog.md
|
|
13449
13799
|
# applying the same gates as the roll-loop SKILL Step 2:
|
|
13450
13800
|
# 1. Status = 📋 Todo
|
|
13451
|
-
# 2.
|
|
13452
|
-
# 3.
|
|
13453
|
-
# 4. Priority order: FIX > US > REFACTOR
|
|
13801
|
+
# 2. depends-on:* (if any) all ✅ Done
|
|
13802
|
+
# 3. Priority order: FIX > US > REFACTOR
|
|
13454
13803
|
#
|
|
13455
13804
|
# stdout: chosen story id (single line)
|
|
13456
13805
|
# exit 0 when picked, 1 when nothing eligible.
|
|
@@ -13947,6 +14296,38 @@ _loop_is_doc_only_change() {
|
|
|
13947
14296
|
return 0
|
|
13948
14297
|
}
|
|
13949
14298
|
|
|
14299
|
+
# _loop_guard_roll_meta_boundary <worktree> <story_id>
|
|
14300
|
+
# US-LOOP-069: for roll-meta-target stories (deliverable under .roll/), verify
|
|
14301
|
+
# that no product-repo tracked files were modified in the worktree. Returns 0 if
|
|
14302
|
+
# safe (or not a roll-meta story), returns 1 if product files were touched and
|
|
14303
|
+
# writes ALERT. FIX-172: roll-meta-ness is path-based, not tag-based.
|
|
14304
|
+
_loop_guard_roll_meta_boundary() {
|
|
14305
|
+
local wt="$1" story_id="$2"
|
|
14306
|
+
[ -n "$story_id" ] || return 0
|
|
14307
|
+
[ -d "$wt" ] || return 0
|
|
14308
|
+
|
|
14309
|
+
local _backlog="${ROLL_MAIN_PROJECT:-.}/.roll/backlog.md"
|
|
14310
|
+
[ -f "$_backlog" ] || return 0
|
|
14311
|
+
|
|
14312
|
+
_loop_is_roll_meta_story "$story_id" "$_backlog" || return 0
|
|
14313
|
+
|
|
14314
|
+
local _changed _violations
|
|
14315
|
+
_changed=$(cd "$wt" && git diff --name-only origin/main..HEAD 2>/dev/null || true)
|
|
14316
|
+
[ -n "$_changed" ] || return 0
|
|
14317
|
+
|
|
14318
|
+
_violations=$(printf '%s\n' "$_changed" | grep -v '^\.roll/' || true)
|
|
14319
|
+
if [ -n "$_violations" ]; then
|
|
14320
|
+
local _alert
|
|
14321
|
+
_alert="US-LOOP-069 guard blocked roll-meta story ${story_id}"
|
|
14322
|
+
_alert="${_alert}"$'\n'"Touched product files (only .roll/ is allowed):"
|
|
14323
|
+
_alert="${_alert}"$'\n'"${_violations}"
|
|
14324
|
+
_worktree_alert "$_alert"
|
|
14325
|
+
return 1
|
|
14326
|
+
fi
|
|
14327
|
+
|
|
14328
|
+
return 0
|
|
14329
|
+
}
|
|
14330
|
+
|
|
13950
14331
|
# _loop_publish_doc_pr <branch> [title]
|
|
13951
14332
|
# Like _loop_publish_pr but merges immediately with --admin (no CI wait).
|
|
13952
14333
|
# For doc-only changes where CI is not meaningful.
|