@seanyao/roll 2026.529.1 → 2026.529.3
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 +18 -0
- package/README.md +1 -0
- package/bin/roll +721 -9
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/agent_routes_lint.py +203 -0
- package/lib/loop_pick_agent.py +245 -0
- package/lib/roll-help.py +1 -0
- package/lib/roll-loop-status.py +109 -0
- package/lib/test_quality_gate.py +143 -0
- package/package.json +1 -1
- package/skills/roll-brief/SKILL.md +7 -0
- package/skills/roll-build/SKILL.md +95 -0
- package/skills/roll-design/SKILL.md +45 -0
- package/skills/roll-doc/SKILL.md +71 -1
- package/skills/roll-fix/SKILL.md +76 -0
- package/skills/roll-loop/SKILL.md +13 -0
- package/skills/roll-onboard/SKILL.md +6 -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.529.
|
|
7
|
+
VERSION="2026.529.3"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -1406,6 +1406,10 @@ cmd_init() {
|
|
|
1406
1406
|
_write_backlog "$project_dir/.roll/backlog.md"
|
|
1407
1407
|
_ensure_features_dir "$project_dir/.roll/features"
|
|
1408
1408
|
_write_features_md "$project_dir/.roll/features.md"
|
|
1409
|
+
# US-AGENT-003: seed agent-routes.yaml from template. Env override:
|
|
1410
|
+
# ROLL_AGENT_ROUTES_TEMPLATE=minimal roll init
|
|
1411
|
+
# Onboard plan can also set agent_routes_template; _init_apply reads it.
|
|
1412
|
+
_init_seed_agent_routes "${ROLL_AGENT_ROUTES_TEMPLATE:-default}" "$project_dir" || true
|
|
1409
1413
|
# US-ONBOARD-019: stamp the project so legacy detection can recognise it
|
|
1410
1414
|
# as Roll-onboarded without depending on directory-name heuristics.
|
|
1411
1415
|
_write_version_stamp "$project_dir"
|
|
@@ -1931,6 +1935,26 @@ print(' '.join(p.get('scope', {}).get('approved', [])))
|
|
|
1931
1935
|
_write_backlog "$project_dir/.roll/backlog.md"
|
|
1932
1936
|
_onboard_changeset_record "$project_dir" "files_created" ".roll/backlog.md"
|
|
1933
1937
|
fi
|
|
1938
|
+
|
|
1939
|
+
# US-AGENT-003: seed agent-routes.yaml. Template precedence:
|
|
1940
|
+
# 1. plan.agent_routes_template (set by $roll-onboard interactive flow)
|
|
1941
|
+
# 2. ROLL_AGENT_ROUTES_TEMPLATE env var
|
|
1942
|
+
# 3. "default"
|
|
1943
|
+
# Set to "skip" to omit seeding entirely.
|
|
1944
|
+
local _routes_template
|
|
1945
|
+
_routes_template=$(python3 -c "
|
|
1946
|
+
import yaml
|
|
1947
|
+
p = yaml.safe_load(open('$plan')) or {}
|
|
1948
|
+
print(p.get('agent_routes_template', '') or '')
|
|
1949
|
+
" 2>/dev/null || echo "")
|
|
1950
|
+
if [[ -z "$_routes_template" ]]; then
|
|
1951
|
+
_routes_template="${ROLL_AGENT_ROUTES_TEMPLATE:-default}"
|
|
1952
|
+
fi
|
|
1953
|
+
if [[ "$_routes_template" != "skip" ]]; then
|
|
1954
|
+
if _init_seed_agent_routes "$_routes_template" "$project_dir"; then
|
|
1955
|
+
_onboard_changeset_record "$project_dir" "files_created" ".roll/agent-routes.yaml"
|
|
1956
|
+
fi
|
|
1957
|
+
fi
|
|
1934
1958
|
if [[ " $approved " == *" features "* ]]; then
|
|
1935
1959
|
_ensure_features_dir "$project_dir/.roll/features"
|
|
1936
1960
|
_write_features_md "$project_dir/.roll/features.md"
|
|
@@ -2449,6 +2473,31 @@ EOF
|
|
|
2449
2473
|
_ROLL_MERGE_SUMMARY+=("created|.roll/backlog.md")
|
|
2450
2474
|
}
|
|
2451
2475
|
|
|
2476
|
+
# US-AGENT-003: seed .roll/agent-routes.yaml from a named template (default /
|
|
2477
|
+
# minimal / heavy). Idempotent — leaves an existing file untouched. Templates
|
|
2478
|
+
# live under ${ROLL_TEMPLATES}/agent-routes/<name>.yaml.
|
|
2479
|
+
_init_seed_agent_routes() {
|
|
2480
|
+
local template_name="${1:-default}"
|
|
2481
|
+
local project_dir="${2:-$(pwd)}"
|
|
2482
|
+
local dest="${project_dir}/.roll/agent-routes.yaml"
|
|
2483
|
+
|
|
2484
|
+
if [[ -f "$dest" ]]; then
|
|
2485
|
+
_ROLL_MERGE_SUMMARY+=("unchanged|.roll/agent-routes.yaml")
|
|
2486
|
+
return 0
|
|
2487
|
+
fi
|
|
2488
|
+
|
|
2489
|
+
local src="${ROLL_TEMPLATES}/agent-routes/${template_name}.yaml"
|
|
2490
|
+
if [[ ! -f "$src" ]]; then
|
|
2491
|
+
err "agent-routes template not found: ${template_name} (looked for ${src})"
|
|
2492
|
+
return 1
|
|
2493
|
+
fi
|
|
2494
|
+
|
|
2495
|
+
mkdir -p "$(dirname "$dest")"
|
|
2496
|
+
cp "$src" "$dest"
|
|
2497
|
+
ok "Created: .roll/agent-routes.yaml (template: ${template_name})"
|
|
2498
|
+
_ROLL_MERGE_SUMMARY+=("created|.roll/agent-routes.yaml")
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2452
2501
|
_ensure_features_dir() {
|
|
2453
2502
|
if [[ -d "$1" ]]; then
|
|
2454
2503
|
_ROLL_MERGE_SUMMARY+=("unchanged|.roll/features/")
|
|
@@ -3340,7 +3389,7 @@ _agent_argv() {
|
|
|
3340
3389
|
fi
|
|
3341
3390
|
case "$mode" in
|
|
3342
3391
|
interactive) _AGENT_ARGV=("$_kimi_bin" "$prompt") ;;
|
|
3343
|
-
*) _AGENT_ARGV=("$_kimi_bin"
|
|
3392
|
+
*) _AGENT_ARGV=("$_kimi_bin" -p "$prompt") ;; # FIX-133: kimi-code 无 --quiet,-p 自带 auto 审批
|
|
3344
3393
|
esac ;;
|
|
3345
3394
|
deepseek)
|
|
3346
3395
|
# deepseek has the same argv shape in both modes (positional prompt).
|
|
@@ -5905,6 +5954,10 @@ _write_runner_script() {
|
|
|
5905
5954
|
_write_loop_runner_script() {
|
|
5906
5955
|
local script_path="$1" project_path="$2" cmd="$3" log_path="$4"
|
|
5907
5956
|
local active_start="${5:-10}" active_end="${6:-18}"
|
|
5957
|
+
# FIX-134: skill md path. When set, the inner script rebuilds the agent
|
|
5958
|
+
# command at runtime from the routed cycle agent; when empty it falls back to
|
|
5959
|
+
# the baked command (backwards compatible with callers that omit it).
|
|
5960
|
+
local skill_path="${7:-}"
|
|
5908
5961
|
# FIX-054: terminal preference detection removed. Popup is hard-coded to
|
|
5909
5962
|
# macOS Terminal.app; the 7th positional arg, if any, is ignored for
|
|
5910
5963
|
# backwards compatibility with existing callers.
|
|
@@ -5936,6 +5989,15 @@ _write_loop_runner_script() {
|
|
|
5936
5989
|
agent_cmd="${agent_cmd/--output-format stream-json/--output-format stream-json --add-dir \"\$WT\"}"
|
|
5937
5990
|
fi
|
|
5938
5991
|
local slug; slug=$(_project_slug "$project_path")
|
|
5992
|
+
# FIX-134: emit a runtime command-builder line when skill_path is known, so
|
|
5993
|
+
# the cycle agent is resolved live (routing-aware). Otherwise leave _CYCLE_CMD
|
|
5994
|
+
# empty and the inner script uses the baked fallback command below.
|
|
5995
|
+
local cycle_cmd_line
|
|
5996
|
+
if [[ -n "$skill_path" ]]; then
|
|
5997
|
+
cycle_cmd_line="_CYCLE_CMD=\$(_loop_cycle_agent_cmd \"${skill_path}\" \"\$CYCLE_AGENT\" \"\$WT\" 2>/dev/null || true)"
|
|
5998
|
+
else
|
|
5999
|
+
cycle_cmd_line="_CYCLE_CMD="
|
|
6000
|
+
fi
|
|
5939
6001
|
cat > "$inner_path" << INNER
|
|
5940
6002
|
#!/bin/bash -l
|
|
5941
6003
|
set -o pipefail
|
|
@@ -6106,12 +6168,22 @@ _runs_append() {
|
|
|
6106
6168
|
# FIX-123: atomic write — write to .tmp.$$ first, then cat >> to append,
|
|
6107
6169
|
# then remove. If interrupted between jq and rm, the next call cleans it.
|
|
6108
6170
|
local _tmp="\$_runs_dst.tmp.\$\$"
|
|
6171
|
+
# US-AGENT-005/010: emit agent + story_type so historical hit rates and
|
|
6172
|
+
# status-page summaries have data to aggregate. Empty when not routed.
|
|
6173
|
+
local _agent_field="\${ROLL_LOOP_ROUTED_AGENT:-\${CYCLE_AGENT:-}}"
|
|
6174
|
+
local _story_field="\${ROLL_LOOP_ROUTED_STORY:-}"
|
|
6175
|
+
local _stype_field=""
|
|
6176
|
+
if [ -n "\$_story_field" ]; then
|
|
6177
|
+
_stype_field="\${_story_field%%-*}"
|
|
6178
|
+
fi
|
|
6109
6179
|
jq -nc \\
|
|
6110
6180
|
--arg ts "\$_ts_now" \\
|
|
6111
6181
|
--arg project "${slug}" \\
|
|
6112
6182
|
--arg run_id "\$_rid" \\
|
|
6113
6183
|
--arg status "\$_status" \\
|
|
6114
6184
|
--arg cycle_id "\$_cid" \\
|
|
6185
|
+
--arg agent "\$_agent_field" \\
|
|
6186
|
+
--arg story_type "\$_stype_field" \\
|
|
6115
6187
|
--argjson built "\$_built" \\
|
|
6116
6188
|
--argjson skipped '[]' \\
|
|
6117
6189
|
--argjson alerts '[]' \\
|
|
@@ -6119,7 +6191,7 @@ _runs_append() {
|
|
|
6119
6191
|
--argjson duration_sec "\$_dur" \\
|
|
6120
6192
|
--argjson phases "\$_phases_json" \\
|
|
6121
6193
|
'{ts:\$ts, project:\$project, run_id:\$run_id, status:\$status,
|
|
6122
|
-
cycle_id:\$cycle_id,
|
|
6194
|
+
cycle_id:\$cycle_id, agent:\$agent, story_type:\$story_type,
|
|
6123
6195
|
built:\$built, skipped:\$skipped, alerts:\$alerts,
|
|
6124
6196
|
tcr_count:\$tcr_count, duration_sec:\$duration_sec, phases:\$phases}' \\
|
|
6125
6197
|
> "\$_tmp" 2>/dev/null || { rm -f "\$_tmp"; return 0; }
|
|
@@ -6345,7 +6417,27 @@ if _worktree_fetch_origin main \\
|
|
|
6345
6417
|
_worktree_sync_meta "\$WT" 2>/dev/null || true
|
|
6346
6418
|
echo "[loop] cycle \${CYCLE_ID}: worktree \$WT on \$BRANCH"
|
|
6347
6419
|
_loop_event cycle_start "\${CYCLE_ID}" "" "" || true
|
|
6348
|
-
|
|
6420
|
+
# US-AGENT-006: per-story routing — pick the next Todo, route to an agent
|
|
6421
|
+
# based on its Agent profile + agent-routes.yaml, fall back to
|
|
6422
|
+
# _project_agent when no story is pickable or routing returns nothing.
|
|
6423
|
+
ROLL_LOOP_ROUTED_STORY=""
|
|
6424
|
+
ROLL_LOOP_ROUTED_AGENT=""
|
|
6425
|
+
ROLL_LOOP_ROUTED_RULE=""
|
|
6426
|
+
ROLL_LOOP_ROUTE_RATIONALE=""
|
|
6427
|
+
( cd "\$WT" >/dev/null 2>&1 && ROLL_LOOP_ROUTED_STORY=\$(_loop_pick_next_story 2>/dev/null) || true )
|
|
6428
|
+
ROLL_LOOP_ROUTED_STORY=\$( (cd "\$WT" 2>/dev/null && _loop_pick_next_story 2>/dev/null) || echo "" )
|
|
6429
|
+
if [ -n "\$ROLL_LOOP_ROUTED_STORY" ]; then
|
|
6430
|
+
_route_line=\$( (cd "\$WT" 2>/dev/null && _loop_pick_agent_for_story "\$ROLL_LOOP_ROUTED_STORY" 2>/dev/null) || echo "" )
|
|
6431
|
+
if [ -n "\$_route_line" ]; then
|
|
6432
|
+
ROLL_LOOP_ROUTED_AGENT=\$(echo "\$_route_line" | awk '{print \$1}')
|
|
6433
|
+
ROLL_LOOP_ROUTED_RULE=\$(echo "\$_route_line" | awk '{print \$2}')
|
|
6434
|
+
ROLL_LOOP_ROUTE_RATIONALE=\$(echo "\$_route_line" | cut -d' ' -f3-)
|
|
6435
|
+
echo "[loop] story \${ROLL_LOOP_ROUTED_STORY} routed to \${ROLL_LOOP_ROUTED_AGENT} via \${ROLL_LOOP_ROUTED_RULE}"
|
|
6436
|
+
_loop_event story_routed "\${CYCLE_ID}" "\${ROLL_LOOP_ROUTED_STORY}" "\${ROLL_LOOP_ROUTED_AGENT}|\${ROLL_LOOP_ROUTED_RULE}" || true
|
|
6437
|
+
fi
|
|
6438
|
+
fi
|
|
6439
|
+
CYCLE_AGENT="\${ROLL_LOOP_ROUTED_AGENT:-\$(_project_agent)}"
|
|
6440
|
+
_loop_event agent_used "\${CYCLE_ID}" "\${CYCLE_AGENT}" "primary" || true
|
|
6349
6441
|
_phase_end worktree_setup ok
|
|
6350
6442
|
else
|
|
6351
6443
|
# P3 fix: skip the cycle entirely when worktree isolation fails.
|
|
@@ -6372,7 +6464,9 @@ export LOOP_CYCLE_ID="\${CYCLE_ID}"
|
|
|
6372
6464
|
export LOOP_SHARED_ROOT="\${_SHARED_ROOT:-\$HOME/.shared/roll}"
|
|
6373
6465
|
# US-LOOP-010: tell loop-fmt.py which agent is running so it can branch
|
|
6374
6466
|
# rendering: claude → stream-json parser, others → transparent passthrough.
|
|
6375
|
-
|
|
6467
|
+
# US-AGENT-006: prefer the per-story routed agent (set above) when present.
|
|
6468
|
+
export ROLL_LOOP_AGENT="\${CYCLE_AGENT:-\$(_project_agent)}"
|
|
6469
|
+
export ROLL_LOOP_ROUTED_STORY ROLL_LOOP_ROUTED_AGENT ROLL_LOOP_ROUTED_RULE
|
|
6376
6470
|
_phase_begin agent_invoke
|
|
6377
6471
|
for _attempt in 1 2 3; do
|
|
6378
6472
|
# FIX-068: defensive reset before each attempt — _CYCLE_TIMED_OUT carries
|
|
@@ -6395,10 +6489,15 @@ for _attempt in 1 2 3; do
|
|
|
6395
6489
|
pkill -KILL -f "\$WT" 2>/dev/null
|
|
6396
6490
|
} ) &
|
|
6397
6491
|
_WATCHDOG_PID=\$!
|
|
6492
|
+
${cycle_cmd_line}
|
|
6493
|
+
# FIX-134: prefer the runtime-rebuilt command (routing-aware); fall back to
|
|
6494
|
+
# the baked command (project agent at \`roll loop on\` time) when empty.
|
|
6398
6495
|
if [ -f "\$FMT" ]; then
|
|
6399
|
-
( cd "\$WT" &&
|
|
6496
|
+
if [ -n "\$_CYCLE_CMD" ]; then ( cd "\$WT" && eval "\$_CYCLE_CMD" ) | python3 "\$FMT"
|
|
6497
|
+
else ( cd "\$WT" && ${agent_cmd} ) | python3 "\$FMT"; fi
|
|
6400
6498
|
else
|
|
6401
|
-
( cd "\$WT" &&
|
|
6499
|
+
if [ -n "\$_CYCLE_CMD" ]; then ( cd "\$WT" && eval "\$_CYCLE_CMD" )
|
|
6500
|
+
else ( cd "\$WT" && ${agent_cmd} ); fi
|
|
6402
6501
|
fi
|
|
6403
6502
|
_exit=\$?
|
|
6404
6503
|
kill "\$_WATCHDOG_PID" 2>/dev/null
|
|
@@ -6858,7 +6957,7 @@ _install_launchd_plists() {
|
|
|
6858
6957
|
local cmd; cmd=$(_agent_skill_cmd "${sd}/${skill}/SKILL.md" 2>/dev/null || echo "roll loop now")
|
|
6859
6958
|
|
|
6860
6959
|
if [[ "$svc" == "loop" ]]; then
|
|
6861
|
-
_write_loop_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log" "$active_start" "$active_end"
|
|
6960
|
+
_write_loop_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log" "$active_start" "$active_end" "${sd}/${skill}/SKILL.md"
|
|
6862
6961
|
else
|
|
6863
6962
|
_write_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log"
|
|
6864
6963
|
fi
|
|
@@ -6903,7 +7002,9 @@ _install_launchd_plists() {
|
|
|
6903
7002
|
|
|
6904
7003
|
_agent_skill_cmd() {
|
|
6905
7004
|
local skill_path="$1"
|
|
6906
|
-
|
|
7005
|
+
# FIX-134: accept an explicit agent (loop routing passes the resolved cycle
|
|
7006
|
+
# agent); default to the project agent for non-routed callers.
|
|
7007
|
+
local agent="${2:-$(_project_agent)}"
|
|
6907
7008
|
local strip="awk 'NR==1 && /^---$/{skip=1;next} skip && /^---$/{skip=0;next} !skip{print}' '${skill_path}'"
|
|
6908
7009
|
_agent_argv "$agent" plain "__PROMPT__" || {
|
|
6909
7010
|
err "Unknown agent '${agent}'. Run: roll agent use <claude|kimi|deepseek|pi|openai|codex|opencode|qwen|gemini>"
|
|
@@ -6924,6 +7025,23 @@ _agent_skill_cmd() {
|
|
|
6924
7025
|
echo "${out} \"\$(${strip})\""
|
|
6925
7026
|
}
|
|
6926
7027
|
|
|
7028
|
+
# FIX-134: build the full per-cycle agent command at RUNTIME, routing-aware.
|
|
7029
|
+
# The loop inner script calls this with the resolved cycle agent (CYCLE_AGENT =
|
|
7030
|
+
# ROLL_LOOP_ROUTED_AGENT or project agent) so routing actually switches the
|
|
7031
|
+
# executed binary — instead of running a constant baked at `roll loop on` time.
|
|
7032
|
+
# Reproduces the claude-only verbose / stream-json / add-dir enhancements that
|
|
7033
|
+
# _write_loop_runner_script previously baked into the runner.
|
|
7034
|
+
_loop_cycle_agent_cmd() {
|
|
7035
|
+
local skill_path="$1" agent="${2:-$(_project_agent)}" wt="${3:-$WT}"
|
|
7036
|
+
[ -n "$skill_path" ] || return 1
|
|
7037
|
+
local cmd; cmd=$(_agent_skill_cmd "$skill_path" "$agent") || return 1
|
|
7038
|
+
cmd="${cmd/claude -p/claude -p --verbose --dangerously-skip-permissions --output-format stream-json}"
|
|
7039
|
+
if [[ "$cmd" == *"--output-format stream-json"* ]]; then
|
|
7040
|
+
cmd="${cmd/--output-format stream-json/--output-format stream-json --add-dir \"$wt\"}"
|
|
7041
|
+
fi
|
|
7042
|
+
printf '%s' "$cmd"
|
|
7043
|
+
}
|
|
7044
|
+
|
|
6927
7045
|
cmd_loop() {
|
|
6928
7046
|
local subcmd="${1:-status}"; shift || true
|
|
6929
7047
|
case "$subcmd" in
|
|
@@ -6949,6 +7067,8 @@ cmd_loop() {
|
|
|
6949
7067
|
precheck-ci) _loop_precheck_ci ;;
|
|
6950
7068
|
hotfix-head-context) _loop_hotfix_head_context "${1:-}" ;;
|
|
6951
7069
|
branches) _loop_branches "$(pwd -P)" ;;
|
|
7070
|
+
agent-routes) _loop_agent_routes "${1:-show}" "${@:2}" ;;
|
|
7071
|
+
test-quality-check) _loop_test_quality_check "$@" ;;
|
|
6952
7072
|
*) cat <<'HELP'
|
|
6953
7073
|
Usage: roll loop <on|off|now|test|status|monitor|runs|log|story|events|attach|mute|unmute|pause|resume|reset|gc|branches>
|
|
6954
7074
|
|
|
@@ -6971,6 +7091,7 @@ Usage: roll loop <on|off|now|test|status|monitor|runs|log|story|events|attach|mu
|
|
|
6971
7091
|
gc [--dry-run] [--keep-days N]
|
|
6972
7092
|
Garbage-collect orphan slugs, tmp debris, expired backups
|
|
6973
7093
|
branches List loop-related branches
|
|
7094
|
+
agent-routes Show / lint agent routing config (.roll/agent-routes.yaml)
|
|
6974
7095
|
|
|
6975
7096
|
Internal (called by roll-loop SKILL):
|
|
6976
7097
|
notify Send macOS notification
|
|
@@ -8720,6 +8841,205 @@ _loop_mark_in_progress() {
|
|
|
8720
8841
|
' "$backlog" > "$tmp" && mv "$tmp" "$backlog"
|
|
8721
8842
|
}
|
|
8722
8843
|
|
|
8844
|
+
# US-AGENT-008: _loop_mark_hold <story-id> <reason> [backlog-path]
|
|
8845
|
+
# Flip "🔨 In Progress" or "📋 Todo" row to "🚫 Hold" with a parenthetical
|
|
8846
|
+
# reason suffix appended to the description column. Idempotent — if the
|
|
8847
|
+
# row is already 🚫 Hold the call is a no-op (status compare is exact).
|
|
8848
|
+
_loop_mark_hold() {
|
|
8849
|
+
local story_id="$1"
|
|
8850
|
+
local reason="${2:-self-downgrade}"
|
|
8851
|
+
local backlog="${3:-${ROLL_MAIN_PROJECT:-$PWD}/.roll/backlog.md}"
|
|
8852
|
+
[ -n "$story_id" ] || return 1
|
|
8853
|
+
[ -f "$backlog" ] || return 0
|
|
8854
|
+
local tmp; tmp=$(mktemp "${backlog}.XXXXXX") || return 1
|
|
8855
|
+
awk -v sid="$story_id" -v reason="$reason" '
|
|
8856
|
+
{
|
|
8857
|
+
line = $0
|
|
8858
|
+
changed = 0
|
|
8859
|
+
if (index(line, "🔨 In Progress") > 0 || index(line, "📋 Todo") > 0) {
|
|
8860
|
+
n = split(line, cols, "|")
|
|
8861
|
+
if (n >= 2) {
|
|
8862
|
+
id_cell = cols[2]
|
|
8863
|
+
gsub(/[[:space:]]/, "", id_cell)
|
|
8864
|
+
sub(/^\[/, "", id_cell)
|
|
8865
|
+
sub(/\].*$/, "", id_cell)
|
|
8866
|
+
if (id_cell == sid) {
|
|
8867
|
+
sub(/🔨 In Progress/, "🚫 Hold", line)
|
|
8868
|
+
sub(/📋 Todo/, "🚫 Hold", line)
|
|
8869
|
+
# Append reason to the description column (cols[3]) only if not
|
|
8870
|
+
# already present.
|
|
8871
|
+
if (index(line, "→ " reason) == 0 && index(line, "(" reason ")") == 0) {
|
|
8872
|
+
# Insert before the trailing " | 🚫 Hold |"
|
|
8873
|
+
sub(/ \| 🚫 Hold \|/, " → " reason " | 🚫 Hold |", line)
|
|
8874
|
+
}
|
|
8875
|
+
changed = 1
|
|
8876
|
+
}
|
|
8877
|
+
}
|
|
8878
|
+
}
|
|
8879
|
+
print line
|
|
8880
|
+
}
|
|
8881
|
+
' "$backlog" > "$tmp" && mv "$tmp" "$backlog"
|
|
8882
|
+
}
|
|
8883
|
+
|
|
8884
|
+
# US-AGENT-008: self-downgrade primitive.
|
|
8885
|
+
# _loop_self_downgrade <story-id> <reason> <sub-ids-csv>
|
|
8886
|
+
# Flips story to 🚫 Hold (with sub list embedded), writes ALERT, emits a
|
|
8887
|
+
# story_self_downgrade event. The actual sub-story rows + feature md are
|
|
8888
|
+
# produced by the SKILL invocation of roll-design --from-story (this helper
|
|
8889
|
+
# just records the contract).
|
|
8890
|
+
_loop_self_downgrade() {
|
|
8891
|
+
local story_id="$1"
|
|
8892
|
+
local reason="${2:-too_big}"
|
|
8893
|
+
local subs="${3:-}"
|
|
8894
|
+
[ -n "$story_id" ] || return 1
|
|
8895
|
+
local backlog="${ROLL_MAIN_PROJECT:-$PWD}/.roll/backlog.md"
|
|
8896
|
+
local annotation
|
|
8897
|
+
if [ -n "$subs" ]; then
|
|
8898
|
+
annotation="split to ${subs}"
|
|
8899
|
+
else
|
|
8900
|
+
annotation="$reason"
|
|
8901
|
+
fi
|
|
8902
|
+
_loop_mark_hold "$story_id" "$annotation" "$backlog" || true
|
|
8903
|
+
|
|
8904
|
+
# ALERT line for human visibility. Slug derives from main project dir.
|
|
8905
|
+
local main_dir="${ROLL_MAIN_PROJECT:-$PWD}"
|
|
8906
|
+
local slug; slug=$(_project_slug "$main_dir" 2>/dev/null || basename "$main_dir")
|
|
8907
|
+
local shared_root="${_SHARED_ROOT:-$HOME/.shared/roll}"
|
|
8908
|
+
local alert_file="${shared_root}/loop/ALERT-${slug}.md"
|
|
8909
|
+
mkdir -p "$(dirname "$alert_file")"
|
|
8910
|
+
printf '[%s] self-downgrade: %s — reason: %s; subs: %s\n' \
|
|
8911
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$story_id" "$reason" "${subs:-<none>}" \
|
|
8912
|
+
>> "$alert_file"
|
|
8913
|
+
|
|
8914
|
+
# Best-effort event emission (tolerates missing helpers).
|
|
8915
|
+
if declare -F _loop_event >/dev/null 2>&1; then
|
|
8916
|
+
_loop_event "agent_self_downgrade" "${LOOP_CYCLE_ID:-$story_id}" "$story_id" "${reason}|${subs}" || true
|
|
8917
|
+
fi
|
|
8918
|
+
echo "[loop] self-downgrade ${story_id}: ${reason}; subs=${subs:-<none>}"
|
|
8919
|
+
}
|
|
8920
|
+
|
|
8921
|
+
# US-AGENT-009: _loop_chain_depth_cap_check <story-id> [backlog]
|
|
8922
|
+
# Returns 0 when auto re-split is still allowed (story's chain_depth < 2),
|
|
8923
|
+
# 1 when the cap is hit (≥ 2 already — the third re-split would be #3 in
|
|
8924
|
+
# the chain). Reads chain_depth from the story's feature md profile;
|
|
8925
|
+
# missing profile is treated as depth 0 (split allowed).
|
|
8926
|
+
_loop_chain_depth_cap_check() {
|
|
8927
|
+
local story_id="$1"
|
|
8928
|
+
local backlog="${2:-${ROLL_MAIN_PROJECT:-$PWD}/.roll/backlog.md}"
|
|
8929
|
+
[ -n "$story_id" ] || return 0
|
|
8930
|
+
[ -f "$backlog" ] || return 0
|
|
8931
|
+
|
|
8932
|
+
# Resolve feature md from backlog link.
|
|
8933
|
+
local md_path
|
|
8934
|
+
md_path=$(grep -E "\[${story_id}\]\(" "$backlog" 2>/dev/null \
|
|
8935
|
+
| head -1 \
|
|
8936
|
+
| sed -E "s/.*\[${story_id}\]\(([^)#]+)#?[^)]*\).*/\1/")
|
|
8937
|
+
[ -n "$md_path" ] || return 0
|
|
8938
|
+
[ -f "$md_path" ] || return 0
|
|
8939
|
+
|
|
8940
|
+
# Find the section for this story id and extract chain_depth.
|
|
8941
|
+
local anchor; anchor=$(echo "$story_id" | tr '[:upper:]' '[:lower:]')
|
|
8942
|
+
local depth
|
|
8943
|
+
depth=$(awk -v anchor="$anchor" '
|
|
8944
|
+
/<a id="/ {
|
|
8945
|
+
if (match($0, /<a id="[^"]+"/)) {
|
|
8946
|
+
cur = substr($0, RSTART + 7, RLENGTH - 8)
|
|
8947
|
+
in_section = (cur == anchor)
|
|
8948
|
+
}
|
|
8949
|
+
next
|
|
8950
|
+
}
|
|
8951
|
+
in_section && /^- chain_depth:/ {
|
|
8952
|
+
gsub(/^- chain_depth:[ \t]*/, "")
|
|
8953
|
+
gsub(/[ \t].*$/, "")
|
|
8954
|
+
print
|
|
8955
|
+
exit
|
|
8956
|
+
}
|
|
8957
|
+
' "$md_path")
|
|
8958
|
+
|
|
8959
|
+
# Empty / non-numeric → treat as 0.
|
|
8960
|
+
case "$depth" in
|
|
8961
|
+
''|*[!0-9]*) depth=0 ;;
|
|
8962
|
+
esac
|
|
8963
|
+
|
|
8964
|
+
[ "$depth" -lt 2 ]
|
|
8965
|
+
}
|
|
8966
|
+
|
|
8967
|
+
# US-AGENT-009: cap-hit path. Story has reached chain_depth ≥ 2 — refuse
|
|
8968
|
+
# further auto re-split, flip 🚫 Hold + write a high-priority ALERT with
|
|
8969
|
+
# chain context for human triage.
|
|
8970
|
+
_loop_split_cap_hit() {
|
|
8971
|
+
local story_id="$1"
|
|
8972
|
+
local reason="${2:-cap-hit}"
|
|
8973
|
+
[ -n "$story_id" ] || return 1
|
|
8974
|
+
local backlog="${ROLL_MAIN_PROJECT:-$PWD}/.roll/backlog.md"
|
|
8975
|
+
_loop_mark_hold "$story_id" "StorySplitCapHit: $reason" "$backlog" || true
|
|
8976
|
+
|
|
8977
|
+
local main_dir="${ROLL_MAIN_PROJECT:-$PWD}"
|
|
8978
|
+
local slug; slug=$(_project_slug "$main_dir" 2>/dev/null || basename "$main_dir")
|
|
8979
|
+
local shared_root="${_SHARED_ROOT:-$HOME/.shared/roll}"
|
|
8980
|
+
local alert_file="${shared_root}/loop/ALERT-${slug}.md"
|
|
8981
|
+
mkdir -p "$(dirname "$alert_file")"
|
|
8982
|
+
printf '[%s] StorySplitCapHit: %s — chain_depth >= 2 (third auto-split refused). %s. Human triage required.\n' \
|
|
8983
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$story_id" "$reason" \
|
|
8984
|
+
>> "$alert_file"
|
|
8985
|
+
|
|
8986
|
+
if declare -F _loop_event >/dev/null 2>&1; then
|
|
8987
|
+
_loop_event "story_split_cap_hit" "${LOOP_CYCLE_ID:-$story_id}" "$story_id" "$reason" || true
|
|
8988
|
+
fi
|
|
8989
|
+
echo "[loop] StorySplitCapHit ${story_id}: chain_depth >= 2 — held for human triage"
|
|
8990
|
+
}
|
|
8991
|
+
|
|
8992
|
+
# US-SKILL-010: unified self-score note writer for roll-build /
|
|
8993
|
+
# roll-fix / roll-design. Lands under .roll/notes/ with YAML frontmatter
|
|
8994
|
+
# so subsequent stories (US-SKILL-014 trend, US-SKILL-015 docs) can
|
|
8995
|
+
# read/aggregate without parsing free text.
|
|
8996
|
+
#
|
|
8997
|
+
# Args: skill story_id score:int verdict [rationale...]
|
|
8998
|
+
_skill_write_self_score() {
|
|
8999
|
+
local skill="${1:-}"
|
|
9000
|
+
local story="${2:-}"
|
|
9001
|
+
local score="${3:-}"
|
|
9002
|
+
local verdict="${4:-}"
|
|
9003
|
+
shift 4 2>/dev/null || true
|
|
9004
|
+
local rationale="${*:-}"
|
|
9005
|
+
|
|
9006
|
+
case "$skill" in
|
|
9007
|
+
roll-build|roll-fix|roll-design) ;;
|
|
9008
|
+
*) err "_skill_write_self_score: skill must be roll-build / roll-fix / roll-design (got '$skill')"; return 1 ;;
|
|
9009
|
+
esac
|
|
9010
|
+
[ -n "$story" ] || { err "_skill_write_self_score: story id required"; return 1; }
|
|
9011
|
+
case "$score" in
|
|
9012
|
+
''|*[!0-9]*) err "_skill_write_self_score: score must be integer 1..10"; return 1 ;;
|
|
9013
|
+
esac
|
|
9014
|
+
if [ "$score" -lt 1 ] || [ "$score" -gt 10 ]; then
|
|
9015
|
+
err "_skill_write_self_score: score out of range (1..10): $score"
|
|
9016
|
+
return 1
|
|
9017
|
+
fi
|
|
9018
|
+
case "$verdict" in
|
|
9019
|
+
good|ok|regression) ;;
|
|
9020
|
+
*) err "_skill_write_self_score: verdict must be good / ok / regression (got '$verdict')"; return 1 ;;
|
|
9021
|
+
esac
|
|
9022
|
+
|
|
9023
|
+
local notes_dir=".roll/notes"
|
|
9024
|
+
mkdir -p "$notes_dir"
|
|
9025
|
+
local ts; ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
9026
|
+
local date_part="${ts%%T*}"
|
|
9027
|
+
local epoch; epoch=$(date -u +%s)
|
|
9028
|
+
local file="${notes_dir}/${date_part}-${skill}-${story}-${epoch}.md"
|
|
9029
|
+
|
|
9030
|
+
{
|
|
9031
|
+
printf -- '---\n'
|
|
9032
|
+
printf 'skill: %s\n' "$skill"
|
|
9033
|
+
printf 'story: %s\n' "$story"
|
|
9034
|
+
printf 'score: %s\n' "$score"
|
|
9035
|
+
printf 'verdict: %s\n' "$verdict"
|
|
9036
|
+
printf 'ts: %s\n' "$ts"
|
|
9037
|
+
printf -- '---\n'
|
|
9038
|
+
printf '\n'
|
|
9039
|
+
printf '%s\n' "$rationale"
|
|
9040
|
+
} > "$file"
|
|
9041
|
+
}
|
|
9042
|
+
|
|
8723
9043
|
# _loop_mark_todo <story-id> [backlog-path]
|
|
8724
9044
|
# Revert a row from "🔨 In Progress" back to "📋 Todo". Called when a
|
|
8725
9045
|
# cycle's executor fails so the next cycle can pick the story up again.
|
|
@@ -9250,6 +9570,218 @@ refs/heads/claude/*"
|
|
|
9250
9570
|
return 0
|
|
9251
9571
|
}
|
|
9252
9572
|
|
|
9573
|
+
# US-AGENT-002: agent-routes.yaml management.
|
|
9574
|
+
# `roll loop agent-routes [show|lint] [path]` — dispatch + helpers.
|
|
9575
|
+
# Default path order: $ROLL_AGENT_ROUTES (if set) → ./.roll/agent-routes.yaml
|
|
9576
|
+
# → built-in default in templates/agent-routes/default.yaml.
|
|
9577
|
+
_loop_agent_routes_path() {
|
|
9578
|
+
if [ -n "${ROLL_AGENT_ROUTES:-}" ] && [ -f "$ROLL_AGENT_ROUTES" ]; then
|
|
9579
|
+
printf '%s\n' "$ROLL_AGENT_ROUTES"
|
|
9580
|
+
return 0
|
|
9581
|
+
fi
|
|
9582
|
+
if [ -f ".roll/agent-routes.yaml" ]; then
|
|
9583
|
+
printf '%s\n' ".roll/agent-routes.yaml"
|
|
9584
|
+
return 0
|
|
9585
|
+
fi
|
|
9586
|
+
# Fallback: built-in default shipped with the package.
|
|
9587
|
+
# ROLL_INSTALL_DIR is set when installed via npm; fall back to script dir.
|
|
9588
|
+
local install_dir
|
|
9589
|
+
install_dir="${ROLL_INSTALL_DIR:-$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")}"
|
|
9590
|
+
local default_tpl="$install_dir/templates/agent-routes/default.yaml"
|
|
9591
|
+
if [ -f "$default_tpl" ]; then
|
|
9592
|
+
printf '%s\n' "$default_tpl"
|
|
9593
|
+
return 0
|
|
9594
|
+
fi
|
|
9595
|
+
return 1
|
|
9596
|
+
}
|
|
9597
|
+
|
|
9598
|
+
_loop_agent_routes_show() {
|
|
9599
|
+
local path
|
|
9600
|
+
path=$(_loop_agent_routes_path) || {
|
|
9601
|
+
echo "agent-routes: no config found (set ROLL_AGENT_ROUTES, drop .roll/agent-routes.yaml, or run roll init)" >&2
|
|
9602
|
+
return 1
|
|
9603
|
+
}
|
|
9604
|
+
echo "# source: $path"
|
|
9605
|
+
cat "$path"
|
|
9606
|
+
}
|
|
9607
|
+
|
|
9608
|
+
_loop_agent_routes_lint() {
|
|
9609
|
+
local path="${1:-}"
|
|
9610
|
+
if [ -z "$path" ]; then
|
|
9611
|
+
path=$(_loop_agent_routes_path) || {
|
|
9612
|
+
echo "agent-routes lint: no config found" >&2
|
|
9613
|
+
return 1
|
|
9614
|
+
}
|
|
9615
|
+
fi
|
|
9616
|
+
if [ ! -f "$path" ]; then
|
|
9617
|
+
echo "agent-routes lint: file not found: $path" >&2
|
|
9618
|
+
return 1
|
|
9619
|
+
fi
|
|
9620
|
+
local install_dir
|
|
9621
|
+
install_dir="${ROLL_INSTALL_DIR:-$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")}"
|
|
9622
|
+
python3 "$install_dir/lib/agent_routes_lint.py" "$path"
|
|
9623
|
+
}
|
|
9624
|
+
|
|
9625
|
+
# US-AGENT-006: pick the next eligible 📋 Todo story from .roll/backlog.md
|
|
9626
|
+
# applying the same gates as the roll-loop SKILL Step 2:
|
|
9627
|
+
# 1. Status = 📋 Todo
|
|
9628
|
+
# 2. NOT manual-only:* tagged
|
|
9629
|
+
# 3. depends-on:* (if any) all ✅ Done
|
|
9630
|
+
# 4. Priority order: FIX > US > REFACTOR
|
|
9631
|
+
#
|
|
9632
|
+
# stdout: chosen story id (single line)
|
|
9633
|
+
# exit 0 when picked, 1 when nothing eligible.
|
|
9634
|
+
_loop_pick_next_story() {
|
|
9635
|
+
local backlog="${1:-.roll/backlog.md}"
|
|
9636
|
+
[ -f "$backlog" ] || return 1
|
|
9637
|
+
|
|
9638
|
+
# Two passes over the file, once per type prefix, return first hit.
|
|
9639
|
+
local prefix
|
|
9640
|
+
for prefix in FIX US REFACTOR; do
|
|
9641
|
+
local id
|
|
9642
|
+
while IFS= read -r line; do
|
|
9643
|
+
[ -z "$line" ] && continue
|
|
9644
|
+
# Skip non-Todo rows fast
|
|
9645
|
+
case "$line" in
|
|
9646
|
+
*'📋 Todo'*) ;;
|
|
9647
|
+
*) continue ;;
|
|
9648
|
+
esac
|
|
9649
|
+
# Extract id like FIX-XXX-NNN / US-XXX-NNN / REFACTOR-XXX-NNN.
|
|
9650
|
+
id=$(printf '%s\n' "$line" | grep -oE "${prefix}-[A-Za-z0-9_-]+" | head -1)
|
|
9651
|
+
[ -n "$id" ] || continue
|
|
9652
|
+
# Gate 1: manual-only
|
|
9653
|
+
_loop_is_manual_only "$id" "$backlog" 2>/dev/null && continue
|
|
9654
|
+
# Gate 2: depends-on
|
|
9655
|
+
if ! _loop_check_depends_on "$id" "$backlog" >/dev/null 2>&1; then
|
|
9656
|
+
continue
|
|
9657
|
+
fi
|
|
9658
|
+
printf '%s\n' "$id"
|
|
9659
|
+
return 0
|
|
9660
|
+
done < "$backlog"
|
|
9661
|
+
done
|
|
9662
|
+
return 1
|
|
9663
|
+
}
|
|
9664
|
+
|
|
9665
|
+
# US-AGENT-004: pick the agent for a given backlog story by reading its
|
|
9666
|
+
# Agent profile from the linked feature md and matching against the active
|
|
9667
|
+
# agent-routes.yaml. Falls back to history.cold_start_default when no agent
|
|
9668
|
+
# matches or the story has no profile.
|
|
9669
|
+
#
|
|
9670
|
+
# stdout: "<agent> <rule_kind> <rationale...>"
|
|
9671
|
+
# exit 0 on success, 1 if the story id isn't found.
|
|
9672
|
+
_loop_pick_agent_for_story() {
|
|
9673
|
+
local story_id="${1:-}"
|
|
9674
|
+
if [ -z "$story_id" ]; then
|
|
9675
|
+
echo "_loop_pick_agent_for_story: story id required" >&2
|
|
9676
|
+
return 1
|
|
9677
|
+
fi
|
|
9678
|
+
local routes
|
|
9679
|
+
routes=$(_loop_agent_routes_path 2>/dev/null) || {
|
|
9680
|
+
echo "_loop_pick_agent_for_story: no agent-routes.yaml available" >&2
|
|
9681
|
+
return 1
|
|
9682
|
+
}
|
|
9683
|
+
local backlog=".roll/backlog.md"
|
|
9684
|
+
[ -f "$backlog" ] || {
|
|
9685
|
+
echo "_loop_pick_agent_for_story: $backlog not found" >&2
|
|
9686
|
+
return 1
|
|
9687
|
+
}
|
|
9688
|
+
local install_dir
|
|
9689
|
+
install_dir="${ROLL_INSTALL_DIR:-$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")}"
|
|
9690
|
+
# US-AGENT-005: pass runs.jsonl when available so history soft preference
|
|
9691
|
+
# can layer on top of hard rules. Project-local first, then shared.
|
|
9692
|
+
local runs_arg=""
|
|
9693
|
+
if [ -f ".roll/runs.jsonl" ]; then
|
|
9694
|
+
runs_arg="--runs .roll/runs.jsonl"
|
|
9695
|
+
elif [ -f "${HOME}/.shared/roll/loop/runs.jsonl" ]; then
|
|
9696
|
+
runs_arg="--runs ${HOME}/.shared/roll/loop/runs.jsonl"
|
|
9697
|
+
fi
|
|
9698
|
+
# shellcheck disable=SC2086
|
|
9699
|
+
python3 "$install_dir/lib/loop_pick_agent.py" \
|
|
9700
|
+
--story-id "$story_id" \
|
|
9701
|
+
--backlog "$backlog" \
|
|
9702
|
+
--routes "$routes" \
|
|
9703
|
+
$runs_arg
|
|
9704
|
+
}
|
|
9705
|
+
|
|
9706
|
+
# US-QA-012: merge-time test-quality gate. Scan bats files for ❼ + ❽
|
|
9707
|
+
# violations; loop auto-merge waits on a clean exit. PR description
|
|
9708
|
+
# `[skip-test-quality]` marker → US-QA-013 passes --skip here.
|
|
9709
|
+
#
|
|
9710
|
+
# Usage:
|
|
9711
|
+
# roll loop test-quality-check [--skip] file.bats [file.bats ...]
|
|
9712
|
+
_loop_test_quality_check() {
|
|
9713
|
+
local install_dir
|
|
9714
|
+
install_dir="${ROLL_INSTALL_DIR:-$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")}"
|
|
9715
|
+
python3 "$install_dir/lib/test_quality_gate.py" "$@"
|
|
9716
|
+
}
|
|
9717
|
+
|
|
9718
|
+
# US-QA-013: PR description marker check. Returns 0 if the body contains
|
|
9719
|
+
# `[skip-test-quality]` (case-insensitive); 1 otherwise.
|
|
9720
|
+
_loop_pr_body_has_skip_test_quality() {
|
|
9721
|
+
local body="${1:-}"
|
|
9722
|
+
[ -n "$body" ] || return 1
|
|
9723
|
+
printf '%s' "$body" | grep -qiE '\[skip-test-quality\]'
|
|
9724
|
+
}
|
|
9725
|
+
|
|
9726
|
+
# US-QA-013: gate + ALERT wrapper. Runs the test-quality gate; on
|
|
9727
|
+
# violations writes a structured ALERT-<slug>.md entry so the human (or
|
|
9728
|
+
# next brief) sees what blocked auto-merge. The wrapper is the entry
|
|
9729
|
+
# point loop calls; it accepts --skip to honor the PR bypass marker.
|
|
9730
|
+
_loop_test_quality_check_with_alert() {
|
|
9731
|
+
local skip=0
|
|
9732
|
+
if [ "${1:-}" = "--skip" ]; then
|
|
9733
|
+
skip=1; shift
|
|
9734
|
+
fi
|
|
9735
|
+
if [ "$skip" -eq 1 ]; then
|
|
9736
|
+
return 0
|
|
9737
|
+
fi
|
|
9738
|
+
local install_dir
|
|
9739
|
+
install_dir="${ROLL_INSTALL_DIR:-$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")}"
|
|
9740
|
+
local report
|
|
9741
|
+
report=$(python3 "$install_dir/lib/test_quality_gate.py" "$@" 2>&1)
|
|
9742
|
+
local rc=$?
|
|
9743
|
+
if [ "$rc" -eq 0 ]; then
|
|
9744
|
+
return 0
|
|
9745
|
+
fi
|
|
9746
|
+
|
|
9747
|
+
local main_dir="${ROLL_MAIN_PROJECT:-$PWD}"
|
|
9748
|
+
local slug; slug=$(_project_slug "$main_dir" 2>/dev/null || basename "$main_dir")
|
|
9749
|
+
local shared_root="${_SHARED_ROOT:-$HOME/.shared/roll}"
|
|
9750
|
+
local alert_file="${shared_root}/loop/ALERT-${slug}.md"
|
|
9751
|
+
mkdir -p "$(dirname "$alert_file")"
|
|
9752
|
+
{
|
|
9753
|
+
printf '[%s] test-quality gate blocked auto-merge (rubric ❼ / ❽).\n' \
|
|
9754
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
9755
|
+
printf '%s\n' "$report"
|
|
9756
|
+
printf 'Bypass: add `[skip-test-quality]` to the PR description and re-run.\n'
|
|
9757
|
+
printf '\n'
|
|
9758
|
+
} >> "$alert_file"
|
|
9759
|
+
|
|
9760
|
+
if declare -F _loop_event >/dev/null 2>&1; then
|
|
9761
|
+
_loop_event "test_quality_gate_block" "${LOOP_CYCLE_ID:-test-quality}" "${1:-}" "$report" || true
|
|
9762
|
+
fi
|
|
9763
|
+
echo "[loop] test-quality gate blocked: see $alert_file" >&2
|
|
9764
|
+
return 1
|
|
9765
|
+
}
|
|
9766
|
+
|
|
9767
|
+
_loop_agent_routes() {
|
|
9768
|
+
local sub="${1:-show}"; shift || true
|
|
9769
|
+
case "$sub" in
|
|
9770
|
+
show) _loop_agent_routes_show ;;
|
|
9771
|
+
lint) _loop_agent_routes_lint "${1:-}" ;;
|
|
9772
|
+
path) _loop_agent_routes_path ;;
|
|
9773
|
+
*)
|
|
9774
|
+
cat >&2 <<'HELP'
|
|
9775
|
+
Usage: roll loop agent-routes <show|lint|path> [path]
|
|
9776
|
+
|
|
9777
|
+
show Print active agent-routes config (source + content)
|
|
9778
|
+
lint [path] Validate schema (default: active config). Exit 1 on errors.
|
|
9779
|
+
path Print which file is currently active.
|
|
9780
|
+
HELP
|
|
9781
|
+
return 1 ;;
|
|
9782
|
+
esac
|
|
9783
|
+
}
|
|
9784
|
+
|
|
9253
9785
|
# US-AUTO-033: publish a loop cycle branch as a GitHub PR with auto-merge.
|
|
9254
9786
|
#
|
|
9255
9787
|
# _loop_publish_pr <branch> [title]
|
|
@@ -9821,6 +10353,185 @@ cmd_alert() {
|
|
|
9821
10353
|
esac
|
|
9822
10354
|
}
|
|
9823
10355
|
|
|
10356
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
10357
|
+
# FEEDBACK — one-shot GitHub issue from the CLI (US-FB-001)
|
|
10358
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
10359
|
+
|
|
10360
|
+
# Derive owner/repo from git origin. Returns "" when not a github remote.
|
|
10361
|
+
_feedback_origin_repo() {
|
|
10362
|
+
local url
|
|
10363
|
+
url=$(git remote get-url origin 2>/dev/null) || return 0
|
|
10364
|
+
case "$url" in
|
|
10365
|
+
git@github.com:*)
|
|
10366
|
+
url="${url#git@github.com:}"
|
|
10367
|
+
url="${url%.git}"
|
|
10368
|
+
printf '%s\n' "$url"
|
|
10369
|
+
;;
|
|
10370
|
+
https://github.com/*)
|
|
10371
|
+
url="${url#https://github.com/}"
|
|
10372
|
+
url="${url%.git}"
|
|
10373
|
+
printf '%s\n' "$url"
|
|
10374
|
+
;;
|
|
10375
|
+
*) printf '\n' ;;
|
|
10376
|
+
esac
|
|
10377
|
+
}
|
|
10378
|
+
|
|
10379
|
+
# US-FB-003: feedback target repo precedence:
|
|
10380
|
+
# 1. --repo flag (caller already resolved this; not part of this helper)
|
|
10381
|
+
# 2. ROLL_FEEDBACK_REPO env var
|
|
10382
|
+
# 3. .roll/local.yaml `feedback_repo:`
|
|
10383
|
+
# 4. ~/.roll/config.yaml `feedback_repo:`
|
|
10384
|
+
# 5. origin-derived github owner/repo
|
|
10385
|
+
_feedback_yaml_field() {
|
|
10386
|
+
local file="$1" field="$2"
|
|
10387
|
+
[ -f "$file" ] || return 0
|
|
10388
|
+
awk -v key="$field" '
|
|
10389
|
+
$0 ~ "^"key":" {
|
|
10390
|
+
v=$0; sub("^"key":[[:space:]]*", "", v); gsub("^[\"\x27]|[\"\x27]$", "", v); print v; exit
|
|
10391
|
+
}' "$file"
|
|
10392
|
+
}
|
|
10393
|
+
|
|
10394
|
+
_feedback_default_repo() {
|
|
10395
|
+
if [ -n "${ROLL_FEEDBACK_REPO:-}" ]; then
|
|
10396
|
+
printf '%s\n' "$ROLL_FEEDBACK_REPO"
|
|
10397
|
+
return 0
|
|
10398
|
+
fi
|
|
10399
|
+
local project_local=".roll/local.yaml"
|
|
10400
|
+
local v
|
|
10401
|
+
v=$(_feedback_yaml_field "$project_local" "feedback_repo")
|
|
10402
|
+
if [ -n "$v" ]; then
|
|
10403
|
+
printf '%s\n' "$v"
|
|
10404
|
+
return 0
|
|
10405
|
+
fi
|
|
10406
|
+
local global="${HOME}/.roll/config.yaml"
|
|
10407
|
+
v=$(_feedback_yaml_field "$global" "feedback_repo")
|
|
10408
|
+
if [ -n "$v" ]; then
|
|
10409
|
+
printf '%s\n' "$v"
|
|
10410
|
+
return 0
|
|
10411
|
+
fi
|
|
10412
|
+
_feedback_origin_repo
|
|
10413
|
+
}
|
|
10414
|
+
|
|
10415
|
+
# Map --type to GitHub label list (single, no spaces).
|
|
10416
|
+
_feedback_label_for_type() {
|
|
10417
|
+
case "${1:-}" in
|
|
10418
|
+
bug) printf 'bug,FIX\n' ;;
|
|
10419
|
+
idea) printf 'idea,enhancement,US\n' ;;
|
|
10420
|
+
ux) printf 'ux,enhancement\n' ;;
|
|
10421
|
+
*) printf 'feedback\n' ;;
|
|
10422
|
+
esac
|
|
10423
|
+
}
|
|
10424
|
+
|
|
10425
|
+
# Percent-encode for use in a GitHub issue URL query string.
|
|
10426
|
+
_feedback_urlencode() {
|
|
10427
|
+
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1"
|
|
10428
|
+
}
|
|
10429
|
+
|
|
10430
|
+
# US-FB-002: compose env info appendix attached to feedback body unless
|
|
10431
|
+
# --no-env is set. Single source of truth so future feedback paths
|
|
10432
|
+
# (web embedded, slack, etc.) can reuse.
|
|
10433
|
+
_feedback_env_block() {
|
|
10434
|
+
local roll_v os_name shell_name agent lang project
|
|
10435
|
+
roll_v="${VERSION:-unknown}"
|
|
10436
|
+
os_name="$(uname -srm 2>/dev/null || echo unknown)"
|
|
10437
|
+
shell_name="$(basename "${SHELL:-/bin/sh}")"
|
|
10438
|
+
agent=$(_project_agent 2>/dev/null || echo "unknown")
|
|
10439
|
+
lang="${LANG:-${LC_ALL:-unknown}}"
|
|
10440
|
+
project="$(basename "$(pwd -P)")"
|
|
10441
|
+
cat <<EOF
|
|
10442
|
+
|
|
10443
|
+
---
|
|
10444
|
+
|
|
10445
|
+
### Environment
|
|
10446
|
+
- roll version: $roll_v
|
|
10447
|
+
- OS: $os_name
|
|
10448
|
+
- shell: $shell_name
|
|
10449
|
+
- current agent: $agent
|
|
10450
|
+
- language: $lang
|
|
10451
|
+
- project: $project
|
|
10452
|
+
EOF
|
|
10453
|
+
}
|
|
10454
|
+
|
|
10455
|
+
cmd_feedback() {
|
|
10456
|
+
local type="" title="" body="" repo="" print_url=0 attach_env=1
|
|
10457
|
+
while [ $# -gt 0 ]; do
|
|
10458
|
+
case "$1" in
|
|
10459
|
+
--type) type="$2"; shift 2 ;;
|
|
10460
|
+
--title) title="$2"; shift 2 ;;
|
|
10461
|
+
--body) body="$2"; shift 2 ;;
|
|
10462
|
+
--repo) repo="$2"; shift 2 ;;
|
|
10463
|
+
--no-env) attach_env=0; shift ;;
|
|
10464
|
+
--print-url) print_url=1; shift ;;
|
|
10465
|
+
--help|-h)
|
|
10466
|
+
cat <<'HELP'
|
|
10467
|
+
Usage: roll feedback [options]
|
|
10468
|
+
roll feedback (一句话提反馈)
|
|
10469
|
+
|
|
10470
|
+
Open a GitHub issue from the CLI. Type auto-labels (bug → FIX label;
|
|
10471
|
+
idea → US label; ux → ux label).
|
|
10472
|
+
|
|
10473
|
+
Options:
|
|
10474
|
+
--type <bug|idea|ux> Classify the feedback (default: bug)
|
|
10475
|
+
--title <text> Issue title (required)
|
|
10476
|
+
--body <text> Issue body
|
|
10477
|
+
--repo <owner/repo> Target repo (default: derived from origin)
|
|
10478
|
+
--no-env Skip the auto-attached Environment section
|
|
10479
|
+
(roll version, OS, agent, language, project)
|
|
10480
|
+
--print-url Print the prefilled github.com URL instead of
|
|
10481
|
+
invoking `gh`. Falls back to this automatically
|
|
10482
|
+
when `gh` is not installed.
|
|
10483
|
+
HELP
|
|
10484
|
+
return 0 ;;
|
|
10485
|
+
*)
|
|
10486
|
+
err "feedback: unknown flag $1"
|
|
10487
|
+
return 1 ;;
|
|
10488
|
+
esac
|
|
10489
|
+
done
|
|
10490
|
+
|
|
10491
|
+
if [ -z "$title" ]; then
|
|
10492
|
+
err "feedback: --title is required"
|
|
10493
|
+
return 1
|
|
10494
|
+
fi
|
|
10495
|
+
if [ -z "$type" ]; then
|
|
10496
|
+
type="bug"
|
|
10497
|
+
fi
|
|
10498
|
+
case "$type" in
|
|
10499
|
+
bug|idea|ux) ;;
|
|
10500
|
+
*)
|
|
10501
|
+
err "feedback: unknown --type '$type' (expected one of: bug, idea, ux)"
|
|
10502
|
+
return 1 ;;
|
|
10503
|
+
esac
|
|
10504
|
+
|
|
10505
|
+
if [ -z "$repo" ]; then
|
|
10506
|
+
repo=$(_feedback_default_repo)
|
|
10507
|
+
fi
|
|
10508
|
+
if [ -z "$repo" ]; then
|
|
10509
|
+
err "feedback: cannot derive owner/repo from origin; pass --repo owner/repo"
|
|
10510
|
+
return 1
|
|
10511
|
+
fi
|
|
10512
|
+
|
|
10513
|
+
# US-FB-002: compose final body with optional env appendix.
|
|
10514
|
+
if [ "$attach_env" -eq 1 ]; then
|
|
10515
|
+
body="${body}$(_feedback_env_block)"
|
|
10516
|
+
fi
|
|
10517
|
+
|
|
10518
|
+
local labels; labels=$(_feedback_label_for_type "$type")
|
|
10519
|
+
|
|
10520
|
+
# Decide path: --print-url or gh missing → print URL; else use gh.
|
|
10521
|
+
if [ "$print_url" -eq 1 ] || ! command -v gh >/dev/null 2>&1; then
|
|
10522
|
+
local t_enc b_enc l_enc
|
|
10523
|
+
t_enc=$(_feedback_urlencode "$title")
|
|
10524
|
+
b_enc=$(_feedback_urlencode "$body")
|
|
10525
|
+
l_enc=$(_feedback_urlencode "$labels")
|
|
10526
|
+
printf 'https://github.com/%s/issues/new?title=%s&body=%s&labels=%s\n' \
|
|
10527
|
+
"$repo" "$t_enc" "$b_enc" "$l_enc"
|
|
10528
|
+
return 0
|
|
10529
|
+
fi
|
|
10530
|
+
|
|
10531
|
+
# Real path: gh issue create
|
|
10532
|
+
gh issue create --repo "$repo" --title "$title" --body "$body" --label "$labels"
|
|
10533
|
+
}
|
|
10534
|
+
|
|
9824
10535
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
9825
10536
|
# LANG — switch / inspect Roll's UI language (US-I18N-001)
|
|
9826
10537
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -10681,6 +11392,7 @@ main() {
|
|
|
10681
11392
|
brief) cmd_brief "$@" ;;
|
|
10682
11393
|
backlog) cmd_backlog "$@" ;;
|
|
10683
11394
|
alert) cmd_alert "$@" ;;
|
|
11395
|
+
feedback) cmd_feedback "$@" ;;
|
|
10684
11396
|
lang) cmd_lang "$@" ;;
|
|
10685
11397
|
agent) cmd_agent "$@" ;;
|
|
10686
11398
|
ci) cmd_ci "$@" ;;
|