@seanyao/roll 2026.529.1 → 2026.529.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -0
- package/bin/roll +679 -4
- 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-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.2"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -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/")
|
|
@@ -6106,12 +6155,22 @@ _runs_append() {
|
|
|
6106
6155
|
# FIX-123: atomic write — write to .tmp.$$ first, then cat >> to append,
|
|
6107
6156
|
# then remove. If interrupted between jq and rm, the next call cleans it.
|
|
6108
6157
|
local _tmp="\$_runs_dst.tmp.\$\$"
|
|
6158
|
+
# US-AGENT-005/010: emit agent + story_type so historical hit rates and
|
|
6159
|
+
# status-page summaries have data to aggregate. Empty when not routed.
|
|
6160
|
+
local _agent_field="\${ROLL_LOOP_ROUTED_AGENT:-\${CYCLE_AGENT:-}}"
|
|
6161
|
+
local _story_field="\${ROLL_LOOP_ROUTED_STORY:-}"
|
|
6162
|
+
local _stype_field=""
|
|
6163
|
+
if [ -n "\$_story_field" ]; then
|
|
6164
|
+
_stype_field="\${_story_field%%-*}"
|
|
6165
|
+
fi
|
|
6109
6166
|
jq -nc \\
|
|
6110
6167
|
--arg ts "\$_ts_now" \\
|
|
6111
6168
|
--arg project "${slug}" \\
|
|
6112
6169
|
--arg run_id "\$_rid" \\
|
|
6113
6170
|
--arg status "\$_status" \\
|
|
6114
6171
|
--arg cycle_id "\$_cid" \\
|
|
6172
|
+
--arg agent "\$_agent_field" \\
|
|
6173
|
+
--arg story_type "\$_stype_field" \\
|
|
6115
6174
|
--argjson built "\$_built" \\
|
|
6116
6175
|
--argjson skipped '[]' \\
|
|
6117
6176
|
--argjson alerts '[]' \\
|
|
@@ -6119,7 +6178,7 @@ _runs_append() {
|
|
|
6119
6178
|
--argjson duration_sec "\$_dur" \\
|
|
6120
6179
|
--argjson phases "\$_phases_json" \\
|
|
6121
6180
|
'{ts:\$ts, project:\$project, run_id:\$run_id, status:\$status,
|
|
6122
|
-
cycle_id:\$cycle_id,
|
|
6181
|
+
cycle_id:\$cycle_id, agent:\$agent, story_type:\$story_type,
|
|
6123
6182
|
built:\$built, skipped:\$skipped, alerts:\$alerts,
|
|
6124
6183
|
tcr_count:\$tcr_count, duration_sec:\$duration_sec, phases:\$phases}' \\
|
|
6125
6184
|
> "\$_tmp" 2>/dev/null || { rm -f "\$_tmp"; return 0; }
|
|
@@ -6345,7 +6404,27 @@ if _worktree_fetch_origin main \\
|
|
|
6345
6404
|
_worktree_sync_meta "\$WT" 2>/dev/null || true
|
|
6346
6405
|
echo "[loop] cycle \${CYCLE_ID}: worktree \$WT on \$BRANCH"
|
|
6347
6406
|
_loop_event cycle_start "\${CYCLE_ID}" "" "" || true
|
|
6348
|
-
|
|
6407
|
+
# US-AGENT-006: per-story routing — pick the next Todo, route to an agent
|
|
6408
|
+
# based on its Agent profile + agent-routes.yaml, fall back to
|
|
6409
|
+
# _project_agent when no story is pickable or routing returns nothing.
|
|
6410
|
+
ROLL_LOOP_ROUTED_STORY=""
|
|
6411
|
+
ROLL_LOOP_ROUTED_AGENT=""
|
|
6412
|
+
ROLL_LOOP_ROUTED_RULE=""
|
|
6413
|
+
ROLL_LOOP_ROUTE_RATIONALE=""
|
|
6414
|
+
( cd "\$WT" >/dev/null 2>&1 && ROLL_LOOP_ROUTED_STORY=\$(_loop_pick_next_story 2>/dev/null) || true )
|
|
6415
|
+
ROLL_LOOP_ROUTED_STORY=\$( (cd "\$WT" 2>/dev/null && _loop_pick_next_story 2>/dev/null) || echo "" )
|
|
6416
|
+
if [ -n "\$ROLL_LOOP_ROUTED_STORY" ]; then
|
|
6417
|
+
_route_line=\$( (cd "\$WT" 2>/dev/null && _loop_pick_agent_for_story "\$ROLL_LOOP_ROUTED_STORY" 2>/dev/null) || echo "" )
|
|
6418
|
+
if [ -n "\$_route_line" ]; then
|
|
6419
|
+
ROLL_LOOP_ROUTED_AGENT=\$(echo "\$_route_line" | awk '{print \$1}')
|
|
6420
|
+
ROLL_LOOP_ROUTED_RULE=\$(echo "\$_route_line" | awk '{print \$2}')
|
|
6421
|
+
ROLL_LOOP_ROUTE_RATIONALE=\$(echo "\$_route_line" | cut -d' ' -f3-)
|
|
6422
|
+
echo "[loop] story \${ROLL_LOOP_ROUTED_STORY} routed to \${ROLL_LOOP_ROUTED_AGENT} via \${ROLL_LOOP_ROUTED_RULE}"
|
|
6423
|
+
_loop_event story_routed "\${CYCLE_ID}" "\${ROLL_LOOP_ROUTED_STORY}" "\${ROLL_LOOP_ROUTED_AGENT}|\${ROLL_LOOP_ROUTED_RULE}" || true
|
|
6424
|
+
fi
|
|
6425
|
+
fi
|
|
6426
|
+
CYCLE_AGENT="\${ROLL_LOOP_ROUTED_AGENT:-\$(_project_agent)}"
|
|
6427
|
+
_loop_event agent_used "\${CYCLE_ID}" "\${CYCLE_AGENT}" "primary" || true
|
|
6349
6428
|
_phase_end worktree_setup ok
|
|
6350
6429
|
else
|
|
6351
6430
|
# P3 fix: skip the cycle entirely when worktree isolation fails.
|
|
@@ -6372,7 +6451,9 @@ export LOOP_CYCLE_ID="\${CYCLE_ID}"
|
|
|
6372
6451
|
export LOOP_SHARED_ROOT="\${_SHARED_ROOT:-\$HOME/.shared/roll}"
|
|
6373
6452
|
# US-LOOP-010: tell loop-fmt.py which agent is running so it can branch
|
|
6374
6453
|
# rendering: claude → stream-json parser, others → transparent passthrough.
|
|
6375
|
-
|
|
6454
|
+
# US-AGENT-006: prefer the per-story routed agent (set above) when present.
|
|
6455
|
+
export ROLL_LOOP_AGENT="\${CYCLE_AGENT:-\$(_project_agent)}"
|
|
6456
|
+
export ROLL_LOOP_ROUTED_STORY ROLL_LOOP_ROUTED_AGENT ROLL_LOOP_ROUTED_RULE
|
|
6376
6457
|
_phase_begin agent_invoke
|
|
6377
6458
|
for _attempt in 1 2 3; do
|
|
6378
6459
|
# FIX-068: defensive reset before each attempt — _CYCLE_TIMED_OUT carries
|
|
@@ -6949,6 +7030,8 @@ cmd_loop() {
|
|
|
6949
7030
|
precheck-ci) _loop_precheck_ci ;;
|
|
6950
7031
|
hotfix-head-context) _loop_hotfix_head_context "${1:-}" ;;
|
|
6951
7032
|
branches) _loop_branches "$(pwd -P)" ;;
|
|
7033
|
+
agent-routes) _loop_agent_routes "${1:-show}" "${@:2}" ;;
|
|
7034
|
+
test-quality-check) _loop_test_quality_check "$@" ;;
|
|
6952
7035
|
*) cat <<'HELP'
|
|
6953
7036
|
Usage: roll loop <on|off|now|test|status|monitor|runs|log|story|events|attach|mute|unmute|pause|resume|reset|gc|branches>
|
|
6954
7037
|
|
|
@@ -6971,6 +7054,7 @@ Usage: roll loop <on|off|now|test|status|monitor|runs|log|story|events|attach|mu
|
|
|
6971
7054
|
gc [--dry-run] [--keep-days N]
|
|
6972
7055
|
Garbage-collect orphan slugs, tmp debris, expired backups
|
|
6973
7056
|
branches List loop-related branches
|
|
7057
|
+
agent-routes Show / lint agent routing config (.roll/agent-routes.yaml)
|
|
6974
7058
|
|
|
6975
7059
|
Internal (called by roll-loop SKILL):
|
|
6976
7060
|
notify Send macOS notification
|
|
@@ -8720,6 +8804,205 @@ _loop_mark_in_progress() {
|
|
|
8720
8804
|
' "$backlog" > "$tmp" && mv "$tmp" "$backlog"
|
|
8721
8805
|
}
|
|
8722
8806
|
|
|
8807
|
+
# US-AGENT-008: _loop_mark_hold <story-id> <reason> [backlog-path]
|
|
8808
|
+
# Flip "🔨 In Progress" or "📋 Todo" row to "🚫 Hold" with a parenthetical
|
|
8809
|
+
# reason suffix appended to the description column. Idempotent — if the
|
|
8810
|
+
# row is already 🚫 Hold the call is a no-op (status compare is exact).
|
|
8811
|
+
_loop_mark_hold() {
|
|
8812
|
+
local story_id="$1"
|
|
8813
|
+
local reason="${2:-self-downgrade}"
|
|
8814
|
+
local backlog="${3:-${ROLL_MAIN_PROJECT:-$PWD}/.roll/backlog.md}"
|
|
8815
|
+
[ -n "$story_id" ] || return 1
|
|
8816
|
+
[ -f "$backlog" ] || return 0
|
|
8817
|
+
local tmp; tmp=$(mktemp "${backlog}.XXXXXX") || return 1
|
|
8818
|
+
awk -v sid="$story_id" -v reason="$reason" '
|
|
8819
|
+
{
|
|
8820
|
+
line = $0
|
|
8821
|
+
changed = 0
|
|
8822
|
+
if (index(line, "🔨 In Progress") > 0 || index(line, "📋 Todo") > 0) {
|
|
8823
|
+
n = split(line, cols, "|")
|
|
8824
|
+
if (n >= 2) {
|
|
8825
|
+
id_cell = cols[2]
|
|
8826
|
+
gsub(/[[:space:]]/, "", id_cell)
|
|
8827
|
+
sub(/^\[/, "", id_cell)
|
|
8828
|
+
sub(/\].*$/, "", id_cell)
|
|
8829
|
+
if (id_cell == sid) {
|
|
8830
|
+
sub(/🔨 In Progress/, "🚫 Hold", line)
|
|
8831
|
+
sub(/📋 Todo/, "🚫 Hold", line)
|
|
8832
|
+
# Append reason to the description column (cols[3]) only if not
|
|
8833
|
+
# already present.
|
|
8834
|
+
if (index(line, "→ " reason) == 0 && index(line, "(" reason ")") == 0) {
|
|
8835
|
+
# Insert before the trailing " | 🚫 Hold |"
|
|
8836
|
+
sub(/ \| 🚫 Hold \|/, " → " reason " | 🚫 Hold |", line)
|
|
8837
|
+
}
|
|
8838
|
+
changed = 1
|
|
8839
|
+
}
|
|
8840
|
+
}
|
|
8841
|
+
}
|
|
8842
|
+
print line
|
|
8843
|
+
}
|
|
8844
|
+
' "$backlog" > "$tmp" && mv "$tmp" "$backlog"
|
|
8845
|
+
}
|
|
8846
|
+
|
|
8847
|
+
# US-AGENT-008: self-downgrade primitive.
|
|
8848
|
+
# _loop_self_downgrade <story-id> <reason> <sub-ids-csv>
|
|
8849
|
+
# Flips story to 🚫 Hold (with sub list embedded), writes ALERT, emits a
|
|
8850
|
+
# story_self_downgrade event. The actual sub-story rows + feature md are
|
|
8851
|
+
# produced by the SKILL invocation of roll-design --from-story (this helper
|
|
8852
|
+
# just records the contract).
|
|
8853
|
+
_loop_self_downgrade() {
|
|
8854
|
+
local story_id="$1"
|
|
8855
|
+
local reason="${2:-too_big}"
|
|
8856
|
+
local subs="${3:-}"
|
|
8857
|
+
[ -n "$story_id" ] || return 1
|
|
8858
|
+
local backlog="${ROLL_MAIN_PROJECT:-$PWD}/.roll/backlog.md"
|
|
8859
|
+
local annotation
|
|
8860
|
+
if [ -n "$subs" ]; then
|
|
8861
|
+
annotation="split to ${subs}"
|
|
8862
|
+
else
|
|
8863
|
+
annotation="$reason"
|
|
8864
|
+
fi
|
|
8865
|
+
_loop_mark_hold "$story_id" "$annotation" "$backlog" || true
|
|
8866
|
+
|
|
8867
|
+
# ALERT line for human visibility. Slug derives from main project dir.
|
|
8868
|
+
local main_dir="${ROLL_MAIN_PROJECT:-$PWD}"
|
|
8869
|
+
local slug; slug=$(_project_slug "$main_dir" 2>/dev/null || basename "$main_dir")
|
|
8870
|
+
local shared_root="${_SHARED_ROOT:-$HOME/.shared/roll}"
|
|
8871
|
+
local alert_file="${shared_root}/loop/ALERT-${slug}.md"
|
|
8872
|
+
mkdir -p "$(dirname "$alert_file")"
|
|
8873
|
+
printf '[%s] self-downgrade: %s — reason: %s; subs: %s\n' \
|
|
8874
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$story_id" "$reason" "${subs:-<none>}" \
|
|
8875
|
+
>> "$alert_file"
|
|
8876
|
+
|
|
8877
|
+
# Best-effort event emission (tolerates missing helpers).
|
|
8878
|
+
if declare -F _loop_event >/dev/null 2>&1; then
|
|
8879
|
+
_loop_event "agent_self_downgrade" "${LOOP_CYCLE_ID:-$story_id}" "$story_id" "${reason}|${subs}" || true
|
|
8880
|
+
fi
|
|
8881
|
+
echo "[loop] self-downgrade ${story_id}: ${reason}; subs=${subs:-<none>}"
|
|
8882
|
+
}
|
|
8883
|
+
|
|
8884
|
+
# US-AGENT-009: _loop_chain_depth_cap_check <story-id> [backlog]
|
|
8885
|
+
# Returns 0 when auto re-split is still allowed (story's chain_depth < 2),
|
|
8886
|
+
# 1 when the cap is hit (≥ 2 already — the third re-split would be #3 in
|
|
8887
|
+
# the chain). Reads chain_depth from the story's feature md profile;
|
|
8888
|
+
# missing profile is treated as depth 0 (split allowed).
|
|
8889
|
+
_loop_chain_depth_cap_check() {
|
|
8890
|
+
local story_id="$1"
|
|
8891
|
+
local backlog="${2:-${ROLL_MAIN_PROJECT:-$PWD}/.roll/backlog.md}"
|
|
8892
|
+
[ -n "$story_id" ] || return 0
|
|
8893
|
+
[ -f "$backlog" ] || return 0
|
|
8894
|
+
|
|
8895
|
+
# Resolve feature md from backlog link.
|
|
8896
|
+
local md_path
|
|
8897
|
+
md_path=$(grep -E "\[${story_id}\]\(" "$backlog" 2>/dev/null \
|
|
8898
|
+
| head -1 \
|
|
8899
|
+
| sed -E "s/.*\[${story_id}\]\(([^)#]+)#?[^)]*\).*/\1/")
|
|
8900
|
+
[ -n "$md_path" ] || return 0
|
|
8901
|
+
[ -f "$md_path" ] || return 0
|
|
8902
|
+
|
|
8903
|
+
# Find the section for this story id and extract chain_depth.
|
|
8904
|
+
local anchor; anchor=$(echo "$story_id" | tr '[:upper:]' '[:lower:]')
|
|
8905
|
+
local depth
|
|
8906
|
+
depth=$(awk -v anchor="$anchor" '
|
|
8907
|
+
/<a id="/ {
|
|
8908
|
+
if (match($0, /<a id="[^"]+"/)) {
|
|
8909
|
+
cur = substr($0, RSTART + 7, RLENGTH - 8)
|
|
8910
|
+
in_section = (cur == anchor)
|
|
8911
|
+
}
|
|
8912
|
+
next
|
|
8913
|
+
}
|
|
8914
|
+
in_section && /^- chain_depth:/ {
|
|
8915
|
+
gsub(/^- chain_depth:[ \t]*/, "")
|
|
8916
|
+
gsub(/[ \t].*$/, "")
|
|
8917
|
+
print
|
|
8918
|
+
exit
|
|
8919
|
+
}
|
|
8920
|
+
' "$md_path")
|
|
8921
|
+
|
|
8922
|
+
# Empty / non-numeric → treat as 0.
|
|
8923
|
+
case "$depth" in
|
|
8924
|
+
''|*[!0-9]*) depth=0 ;;
|
|
8925
|
+
esac
|
|
8926
|
+
|
|
8927
|
+
[ "$depth" -lt 2 ]
|
|
8928
|
+
}
|
|
8929
|
+
|
|
8930
|
+
# US-AGENT-009: cap-hit path. Story has reached chain_depth ≥ 2 — refuse
|
|
8931
|
+
# further auto re-split, flip 🚫 Hold + write a high-priority ALERT with
|
|
8932
|
+
# chain context for human triage.
|
|
8933
|
+
_loop_split_cap_hit() {
|
|
8934
|
+
local story_id="$1"
|
|
8935
|
+
local reason="${2:-cap-hit}"
|
|
8936
|
+
[ -n "$story_id" ] || return 1
|
|
8937
|
+
local backlog="${ROLL_MAIN_PROJECT:-$PWD}/.roll/backlog.md"
|
|
8938
|
+
_loop_mark_hold "$story_id" "StorySplitCapHit: $reason" "$backlog" || true
|
|
8939
|
+
|
|
8940
|
+
local main_dir="${ROLL_MAIN_PROJECT:-$PWD}"
|
|
8941
|
+
local slug; slug=$(_project_slug "$main_dir" 2>/dev/null || basename "$main_dir")
|
|
8942
|
+
local shared_root="${_SHARED_ROOT:-$HOME/.shared/roll}"
|
|
8943
|
+
local alert_file="${shared_root}/loop/ALERT-${slug}.md"
|
|
8944
|
+
mkdir -p "$(dirname "$alert_file")"
|
|
8945
|
+
printf '[%s] StorySplitCapHit: %s — chain_depth >= 2 (third auto-split refused). %s. Human triage required.\n' \
|
|
8946
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$story_id" "$reason" \
|
|
8947
|
+
>> "$alert_file"
|
|
8948
|
+
|
|
8949
|
+
if declare -F _loop_event >/dev/null 2>&1; then
|
|
8950
|
+
_loop_event "story_split_cap_hit" "${LOOP_CYCLE_ID:-$story_id}" "$story_id" "$reason" || true
|
|
8951
|
+
fi
|
|
8952
|
+
echo "[loop] StorySplitCapHit ${story_id}: chain_depth >= 2 — held for human triage"
|
|
8953
|
+
}
|
|
8954
|
+
|
|
8955
|
+
# US-SKILL-010: unified self-score note writer for roll-build /
|
|
8956
|
+
# roll-fix / roll-design. Lands under .roll/notes/ with YAML frontmatter
|
|
8957
|
+
# so subsequent stories (US-SKILL-014 trend, US-SKILL-015 docs) can
|
|
8958
|
+
# read/aggregate without parsing free text.
|
|
8959
|
+
#
|
|
8960
|
+
# Args: skill story_id score:int verdict [rationale...]
|
|
8961
|
+
_skill_write_self_score() {
|
|
8962
|
+
local skill="${1:-}"
|
|
8963
|
+
local story="${2:-}"
|
|
8964
|
+
local score="${3:-}"
|
|
8965
|
+
local verdict="${4:-}"
|
|
8966
|
+
shift 4 2>/dev/null || true
|
|
8967
|
+
local rationale="${*:-}"
|
|
8968
|
+
|
|
8969
|
+
case "$skill" in
|
|
8970
|
+
roll-build|roll-fix|roll-design) ;;
|
|
8971
|
+
*) err "_skill_write_self_score: skill must be roll-build / roll-fix / roll-design (got '$skill')"; return 1 ;;
|
|
8972
|
+
esac
|
|
8973
|
+
[ -n "$story" ] || { err "_skill_write_self_score: story id required"; return 1; }
|
|
8974
|
+
case "$score" in
|
|
8975
|
+
''|*[!0-9]*) err "_skill_write_self_score: score must be integer 1..10"; return 1 ;;
|
|
8976
|
+
esac
|
|
8977
|
+
if [ "$score" -lt 1 ] || [ "$score" -gt 10 ]; then
|
|
8978
|
+
err "_skill_write_self_score: score out of range (1..10): $score"
|
|
8979
|
+
return 1
|
|
8980
|
+
fi
|
|
8981
|
+
case "$verdict" in
|
|
8982
|
+
good|ok|regression) ;;
|
|
8983
|
+
*) err "_skill_write_self_score: verdict must be good / ok / regression (got '$verdict')"; return 1 ;;
|
|
8984
|
+
esac
|
|
8985
|
+
|
|
8986
|
+
local notes_dir=".roll/notes"
|
|
8987
|
+
mkdir -p "$notes_dir"
|
|
8988
|
+
local ts; ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
8989
|
+
local date_part="${ts%%T*}"
|
|
8990
|
+
local epoch; epoch=$(date -u +%s)
|
|
8991
|
+
local file="${notes_dir}/${date_part}-${skill}-${story}-${epoch}.md"
|
|
8992
|
+
|
|
8993
|
+
{
|
|
8994
|
+
printf -- '---\n'
|
|
8995
|
+
printf 'skill: %s\n' "$skill"
|
|
8996
|
+
printf 'story: %s\n' "$story"
|
|
8997
|
+
printf 'score: %s\n' "$score"
|
|
8998
|
+
printf 'verdict: %s\n' "$verdict"
|
|
8999
|
+
printf 'ts: %s\n' "$ts"
|
|
9000
|
+
printf -- '---\n'
|
|
9001
|
+
printf '\n'
|
|
9002
|
+
printf '%s\n' "$rationale"
|
|
9003
|
+
} > "$file"
|
|
9004
|
+
}
|
|
9005
|
+
|
|
8723
9006
|
# _loop_mark_todo <story-id> [backlog-path]
|
|
8724
9007
|
# Revert a row from "🔨 In Progress" back to "📋 Todo". Called when a
|
|
8725
9008
|
# cycle's executor fails so the next cycle can pick the story up again.
|
|
@@ -9250,6 +9533,218 @@ refs/heads/claude/*"
|
|
|
9250
9533
|
return 0
|
|
9251
9534
|
}
|
|
9252
9535
|
|
|
9536
|
+
# US-AGENT-002: agent-routes.yaml management.
|
|
9537
|
+
# `roll loop agent-routes [show|lint] [path]` — dispatch + helpers.
|
|
9538
|
+
# Default path order: $ROLL_AGENT_ROUTES (if set) → ./.roll/agent-routes.yaml
|
|
9539
|
+
# → built-in default in templates/agent-routes/default.yaml.
|
|
9540
|
+
_loop_agent_routes_path() {
|
|
9541
|
+
if [ -n "${ROLL_AGENT_ROUTES:-}" ] && [ -f "$ROLL_AGENT_ROUTES" ]; then
|
|
9542
|
+
printf '%s\n' "$ROLL_AGENT_ROUTES"
|
|
9543
|
+
return 0
|
|
9544
|
+
fi
|
|
9545
|
+
if [ -f ".roll/agent-routes.yaml" ]; then
|
|
9546
|
+
printf '%s\n' ".roll/agent-routes.yaml"
|
|
9547
|
+
return 0
|
|
9548
|
+
fi
|
|
9549
|
+
# Fallback: built-in default shipped with the package.
|
|
9550
|
+
# ROLL_INSTALL_DIR is set when installed via npm; fall back to script dir.
|
|
9551
|
+
local install_dir
|
|
9552
|
+
install_dir="${ROLL_INSTALL_DIR:-$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")}"
|
|
9553
|
+
local default_tpl="$install_dir/templates/agent-routes/default.yaml"
|
|
9554
|
+
if [ -f "$default_tpl" ]; then
|
|
9555
|
+
printf '%s\n' "$default_tpl"
|
|
9556
|
+
return 0
|
|
9557
|
+
fi
|
|
9558
|
+
return 1
|
|
9559
|
+
}
|
|
9560
|
+
|
|
9561
|
+
_loop_agent_routes_show() {
|
|
9562
|
+
local path
|
|
9563
|
+
path=$(_loop_agent_routes_path) || {
|
|
9564
|
+
echo "agent-routes: no config found (set ROLL_AGENT_ROUTES, drop .roll/agent-routes.yaml, or run roll init)" >&2
|
|
9565
|
+
return 1
|
|
9566
|
+
}
|
|
9567
|
+
echo "# source: $path"
|
|
9568
|
+
cat "$path"
|
|
9569
|
+
}
|
|
9570
|
+
|
|
9571
|
+
_loop_agent_routes_lint() {
|
|
9572
|
+
local path="${1:-}"
|
|
9573
|
+
if [ -z "$path" ]; then
|
|
9574
|
+
path=$(_loop_agent_routes_path) || {
|
|
9575
|
+
echo "agent-routes lint: no config found" >&2
|
|
9576
|
+
return 1
|
|
9577
|
+
}
|
|
9578
|
+
fi
|
|
9579
|
+
if [ ! -f "$path" ]; then
|
|
9580
|
+
echo "agent-routes lint: file not found: $path" >&2
|
|
9581
|
+
return 1
|
|
9582
|
+
fi
|
|
9583
|
+
local install_dir
|
|
9584
|
+
install_dir="${ROLL_INSTALL_DIR:-$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")}"
|
|
9585
|
+
python3 "$install_dir/lib/agent_routes_lint.py" "$path"
|
|
9586
|
+
}
|
|
9587
|
+
|
|
9588
|
+
# US-AGENT-006: pick the next eligible 📋 Todo story from .roll/backlog.md
|
|
9589
|
+
# applying the same gates as the roll-loop SKILL Step 2:
|
|
9590
|
+
# 1. Status = 📋 Todo
|
|
9591
|
+
# 2. NOT manual-only:* tagged
|
|
9592
|
+
# 3. depends-on:* (if any) all ✅ Done
|
|
9593
|
+
# 4. Priority order: FIX > US > REFACTOR
|
|
9594
|
+
#
|
|
9595
|
+
# stdout: chosen story id (single line)
|
|
9596
|
+
# exit 0 when picked, 1 when nothing eligible.
|
|
9597
|
+
_loop_pick_next_story() {
|
|
9598
|
+
local backlog="${1:-.roll/backlog.md}"
|
|
9599
|
+
[ -f "$backlog" ] || return 1
|
|
9600
|
+
|
|
9601
|
+
# Two passes over the file, once per type prefix, return first hit.
|
|
9602
|
+
local prefix
|
|
9603
|
+
for prefix in FIX US REFACTOR; do
|
|
9604
|
+
local id
|
|
9605
|
+
while IFS= read -r line; do
|
|
9606
|
+
[ -z "$line" ] && continue
|
|
9607
|
+
# Skip non-Todo rows fast
|
|
9608
|
+
case "$line" in
|
|
9609
|
+
*'📋 Todo'*) ;;
|
|
9610
|
+
*) continue ;;
|
|
9611
|
+
esac
|
|
9612
|
+
# Extract id like FIX-XXX-NNN / US-XXX-NNN / REFACTOR-XXX-NNN.
|
|
9613
|
+
id=$(printf '%s\n' "$line" | grep -oE "${prefix}-[A-Za-z0-9_-]+" | head -1)
|
|
9614
|
+
[ -n "$id" ] || continue
|
|
9615
|
+
# Gate 1: manual-only
|
|
9616
|
+
_loop_is_manual_only "$id" "$backlog" 2>/dev/null && continue
|
|
9617
|
+
# Gate 2: depends-on
|
|
9618
|
+
if ! _loop_check_depends_on "$id" "$backlog" >/dev/null 2>&1; then
|
|
9619
|
+
continue
|
|
9620
|
+
fi
|
|
9621
|
+
printf '%s\n' "$id"
|
|
9622
|
+
return 0
|
|
9623
|
+
done < "$backlog"
|
|
9624
|
+
done
|
|
9625
|
+
return 1
|
|
9626
|
+
}
|
|
9627
|
+
|
|
9628
|
+
# US-AGENT-004: pick the agent for a given backlog story by reading its
|
|
9629
|
+
# Agent profile from the linked feature md and matching against the active
|
|
9630
|
+
# agent-routes.yaml. Falls back to history.cold_start_default when no agent
|
|
9631
|
+
# matches or the story has no profile.
|
|
9632
|
+
#
|
|
9633
|
+
# stdout: "<agent> <rule_kind> <rationale...>"
|
|
9634
|
+
# exit 0 on success, 1 if the story id isn't found.
|
|
9635
|
+
_loop_pick_agent_for_story() {
|
|
9636
|
+
local story_id="${1:-}"
|
|
9637
|
+
if [ -z "$story_id" ]; then
|
|
9638
|
+
echo "_loop_pick_agent_for_story: story id required" >&2
|
|
9639
|
+
return 1
|
|
9640
|
+
fi
|
|
9641
|
+
local routes
|
|
9642
|
+
routes=$(_loop_agent_routes_path 2>/dev/null) || {
|
|
9643
|
+
echo "_loop_pick_agent_for_story: no agent-routes.yaml available" >&2
|
|
9644
|
+
return 1
|
|
9645
|
+
}
|
|
9646
|
+
local backlog=".roll/backlog.md"
|
|
9647
|
+
[ -f "$backlog" ] || {
|
|
9648
|
+
echo "_loop_pick_agent_for_story: $backlog not found" >&2
|
|
9649
|
+
return 1
|
|
9650
|
+
}
|
|
9651
|
+
local install_dir
|
|
9652
|
+
install_dir="${ROLL_INSTALL_DIR:-$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")}"
|
|
9653
|
+
# US-AGENT-005: pass runs.jsonl when available so history soft preference
|
|
9654
|
+
# can layer on top of hard rules. Project-local first, then shared.
|
|
9655
|
+
local runs_arg=""
|
|
9656
|
+
if [ -f ".roll/runs.jsonl" ]; then
|
|
9657
|
+
runs_arg="--runs .roll/runs.jsonl"
|
|
9658
|
+
elif [ -f "${HOME}/.shared/roll/loop/runs.jsonl" ]; then
|
|
9659
|
+
runs_arg="--runs ${HOME}/.shared/roll/loop/runs.jsonl"
|
|
9660
|
+
fi
|
|
9661
|
+
# shellcheck disable=SC2086
|
|
9662
|
+
python3 "$install_dir/lib/loop_pick_agent.py" \
|
|
9663
|
+
--story-id "$story_id" \
|
|
9664
|
+
--backlog "$backlog" \
|
|
9665
|
+
--routes "$routes" \
|
|
9666
|
+
$runs_arg
|
|
9667
|
+
}
|
|
9668
|
+
|
|
9669
|
+
# US-QA-012: merge-time test-quality gate. Scan bats files for ❼ + ❽
|
|
9670
|
+
# violations; loop auto-merge waits on a clean exit. PR description
|
|
9671
|
+
# `[skip-test-quality]` marker → US-QA-013 passes --skip here.
|
|
9672
|
+
#
|
|
9673
|
+
# Usage:
|
|
9674
|
+
# roll loop test-quality-check [--skip] file.bats [file.bats ...]
|
|
9675
|
+
_loop_test_quality_check() {
|
|
9676
|
+
local install_dir
|
|
9677
|
+
install_dir="${ROLL_INSTALL_DIR:-$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")}"
|
|
9678
|
+
python3 "$install_dir/lib/test_quality_gate.py" "$@"
|
|
9679
|
+
}
|
|
9680
|
+
|
|
9681
|
+
# US-QA-013: PR description marker check. Returns 0 if the body contains
|
|
9682
|
+
# `[skip-test-quality]` (case-insensitive); 1 otherwise.
|
|
9683
|
+
_loop_pr_body_has_skip_test_quality() {
|
|
9684
|
+
local body="${1:-}"
|
|
9685
|
+
[ -n "$body" ] || return 1
|
|
9686
|
+
printf '%s' "$body" | grep -qiE '\[skip-test-quality\]'
|
|
9687
|
+
}
|
|
9688
|
+
|
|
9689
|
+
# US-QA-013: gate + ALERT wrapper. Runs the test-quality gate; on
|
|
9690
|
+
# violations writes a structured ALERT-<slug>.md entry so the human (or
|
|
9691
|
+
# next brief) sees what blocked auto-merge. The wrapper is the entry
|
|
9692
|
+
# point loop calls; it accepts --skip to honor the PR bypass marker.
|
|
9693
|
+
_loop_test_quality_check_with_alert() {
|
|
9694
|
+
local skip=0
|
|
9695
|
+
if [ "${1:-}" = "--skip" ]; then
|
|
9696
|
+
skip=1; shift
|
|
9697
|
+
fi
|
|
9698
|
+
if [ "$skip" -eq 1 ]; then
|
|
9699
|
+
return 0
|
|
9700
|
+
fi
|
|
9701
|
+
local install_dir
|
|
9702
|
+
install_dir="${ROLL_INSTALL_DIR:-$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")}"
|
|
9703
|
+
local report
|
|
9704
|
+
report=$(python3 "$install_dir/lib/test_quality_gate.py" "$@" 2>&1)
|
|
9705
|
+
local rc=$?
|
|
9706
|
+
if [ "$rc" -eq 0 ]; then
|
|
9707
|
+
return 0
|
|
9708
|
+
fi
|
|
9709
|
+
|
|
9710
|
+
local main_dir="${ROLL_MAIN_PROJECT:-$PWD}"
|
|
9711
|
+
local slug; slug=$(_project_slug "$main_dir" 2>/dev/null || basename "$main_dir")
|
|
9712
|
+
local shared_root="${_SHARED_ROOT:-$HOME/.shared/roll}"
|
|
9713
|
+
local alert_file="${shared_root}/loop/ALERT-${slug}.md"
|
|
9714
|
+
mkdir -p "$(dirname "$alert_file")"
|
|
9715
|
+
{
|
|
9716
|
+
printf '[%s] test-quality gate blocked auto-merge (rubric ❼ / ❽).\n' \
|
|
9717
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
9718
|
+
printf '%s\n' "$report"
|
|
9719
|
+
printf 'Bypass: add `[skip-test-quality]` to the PR description and re-run.\n'
|
|
9720
|
+
printf '\n'
|
|
9721
|
+
} >> "$alert_file"
|
|
9722
|
+
|
|
9723
|
+
if declare -F _loop_event >/dev/null 2>&1; then
|
|
9724
|
+
_loop_event "test_quality_gate_block" "${LOOP_CYCLE_ID:-test-quality}" "${1:-}" "$report" || true
|
|
9725
|
+
fi
|
|
9726
|
+
echo "[loop] test-quality gate blocked: see $alert_file" >&2
|
|
9727
|
+
return 1
|
|
9728
|
+
}
|
|
9729
|
+
|
|
9730
|
+
_loop_agent_routes() {
|
|
9731
|
+
local sub="${1:-show}"; shift || true
|
|
9732
|
+
case "$sub" in
|
|
9733
|
+
show) _loop_agent_routes_show ;;
|
|
9734
|
+
lint) _loop_agent_routes_lint "${1:-}" ;;
|
|
9735
|
+
path) _loop_agent_routes_path ;;
|
|
9736
|
+
*)
|
|
9737
|
+
cat >&2 <<'HELP'
|
|
9738
|
+
Usage: roll loop agent-routes <show|lint|path> [path]
|
|
9739
|
+
|
|
9740
|
+
show Print active agent-routes config (source + content)
|
|
9741
|
+
lint [path] Validate schema (default: active config). Exit 1 on errors.
|
|
9742
|
+
path Print which file is currently active.
|
|
9743
|
+
HELP
|
|
9744
|
+
return 1 ;;
|
|
9745
|
+
esac
|
|
9746
|
+
}
|
|
9747
|
+
|
|
9253
9748
|
# US-AUTO-033: publish a loop cycle branch as a GitHub PR with auto-merge.
|
|
9254
9749
|
#
|
|
9255
9750
|
# _loop_publish_pr <branch> [title]
|
|
@@ -9821,6 +10316,185 @@ cmd_alert() {
|
|
|
9821
10316
|
esac
|
|
9822
10317
|
}
|
|
9823
10318
|
|
|
10319
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
10320
|
+
# FEEDBACK — one-shot GitHub issue from the CLI (US-FB-001)
|
|
10321
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
10322
|
+
|
|
10323
|
+
# Derive owner/repo from git origin. Returns "" when not a github remote.
|
|
10324
|
+
_feedback_origin_repo() {
|
|
10325
|
+
local url
|
|
10326
|
+
url=$(git remote get-url origin 2>/dev/null) || return 0
|
|
10327
|
+
case "$url" in
|
|
10328
|
+
git@github.com:*)
|
|
10329
|
+
url="${url#git@github.com:}"
|
|
10330
|
+
url="${url%.git}"
|
|
10331
|
+
printf '%s\n' "$url"
|
|
10332
|
+
;;
|
|
10333
|
+
https://github.com/*)
|
|
10334
|
+
url="${url#https://github.com/}"
|
|
10335
|
+
url="${url%.git}"
|
|
10336
|
+
printf '%s\n' "$url"
|
|
10337
|
+
;;
|
|
10338
|
+
*) printf '\n' ;;
|
|
10339
|
+
esac
|
|
10340
|
+
}
|
|
10341
|
+
|
|
10342
|
+
# US-FB-003: feedback target repo precedence:
|
|
10343
|
+
# 1. --repo flag (caller already resolved this; not part of this helper)
|
|
10344
|
+
# 2. ROLL_FEEDBACK_REPO env var
|
|
10345
|
+
# 3. .roll/local.yaml `feedback_repo:`
|
|
10346
|
+
# 4. ~/.roll/config.yaml `feedback_repo:`
|
|
10347
|
+
# 5. origin-derived github owner/repo
|
|
10348
|
+
_feedback_yaml_field() {
|
|
10349
|
+
local file="$1" field="$2"
|
|
10350
|
+
[ -f "$file" ] || return 0
|
|
10351
|
+
awk -v key="$field" '
|
|
10352
|
+
$0 ~ "^"key":" {
|
|
10353
|
+
v=$0; sub("^"key":[[:space:]]*", "", v); gsub("^[\"\x27]|[\"\x27]$", "", v); print v; exit
|
|
10354
|
+
}' "$file"
|
|
10355
|
+
}
|
|
10356
|
+
|
|
10357
|
+
_feedback_default_repo() {
|
|
10358
|
+
if [ -n "${ROLL_FEEDBACK_REPO:-}" ]; then
|
|
10359
|
+
printf '%s\n' "$ROLL_FEEDBACK_REPO"
|
|
10360
|
+
return 0
|
|
10361
|
+
fi
|
|
10362
|
+
local project_local=".roll/local.yaml"
|
|
10363
|
+
local v
|
|
10364
|
+
v=$(_feedback_yaml_field "$project_local" "feedback_repo")
|
|
10365
|
+
if [ -n "$v" ]; then
|
|
10366
|
+
printf '%s\n' "$v"
|
|
10367
|
+
return 0
|
|
10368
|
+
fi
|
|
10369
|
+
local global="${HOME}/.roll/config.yaml"
|
|
10370
|
+
v=$(_feedback_yaml_field "$global" "feedback_repo")
|
|
10371
|
+
if [ -n "$v" ]; then
|
|
10372
|
+
printf '%s\n' "$v"
|
|
10373
|
+
return 0
|
|
10374
|
+
fi
|
|
10375
|
+
_feedback_origin_repo
|
|
10376
|
+
}
|
|
10377
|
+
|
|
10378
|
+
# Map --type to GitHub label list (single, no spaces).
|
|
10379
|
+
_feedback_label_for_type() {
|
|
10380
|
+
case "${1:-}" in
|
|
10381
|
+
bug) printf 'bug,FIX\n' ;;
|
|
10382
|
+
idea) printf 'idea,enhancement,US\n' ;;
|
|
10383
|
+
ux) printf 'ux,enhancement\n' ;;
|
|
10384
|
+
*) printf 'feedback\n' ;;
|
|
10385
|
+
esac
|
|
10386
|
+
}
|
|
10387
|
+
|
|
10388
|
+
# Percent-encode for use in a GitHub issue URL query string.
|
|
10389
|
+
_feedback_urlencode() {
|
|
10390
|
+
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1"
|
|
10391
|
+
}
|
|
10392
|
+
|
|
10393
|
+
# US-FB-002: compose env info appendix attached to feedback body unless
|
|
10394
|
+
# --no-env is set. Single source of truth so future feedback paths
|
|
10395
|
+
# (web embedded, slack, etc.) can reuse.
|
|
10396
|
+
_feedback_env_block() {
|
|
10397
|
+
local roll_v os_name shell_name agent lang project
|
|
10398
|
+
roll_v="${VERSION:-unknown}"
|
|
10399
|
+
os_name="$(uname -srm 2>/dev/null || echo unknown)"
|
|
10400
|
+
shell_name="$(basename "${SHELL:-/bin/sh}")"
|
|
10401
|
+
agent=$(_project_agent 2>/dev/null || echo "unknown")
|
|
10402
|
+
lang="${LANG:-${LC_ALL:-unknown}}"
|
|
10403
|
+
project="$(basename "$(pwd -P)")"
|
|
10404
|
+
cat <<EOF
|
|
10405
|
+
|
|
10406
|
+
---
|
|
10407
|
+
|
|
10408
|
+
### Environment
|
|
10409
|
+
- roll version: $roll_v
|
|
10410
|
+
- OS: $os_name
|
|
10411
|
+
- shell: $shell_name
|
|
10412
|
+
- current agent: $agent
|
|
10413
|
+
- language: $lang
|
|
10414
|
+
- project: $project
|
|
10415
|
+
EOF
|
|
10416
|
+
}
|
|
10417
|
+
|
|
10418
|
+
cmd_feedback() {
|
|
10419
|
+
local type="" title="" body="" repo="" print_url=0 attach_env=1
|
|
10420
|
+
while [ $# -gt 0 ]; do
|
|
10421
|
+
case "$1" in
|
|
10422
|
+
--type) type="$2"; shift 2 ;;
|
|
10423
|
+
--title) title="$2"; shift 2 ;;
|
|
10424
|
+
--body) body="$2"; shift 2 ;;
|
|
10425
|
+
--repo) repo="$2"; shift 2 ;;
|
|
10426
|
+
--no-env) attach_env=0; shift ;;
|
|
10427
|
+
--print-url) print_url=1; shift ;;
|
|
10428
|
+
--help|-h)
|
|
10429
|
+
cat <<'HELP'
|
|
10430
|
+
Usage: roll feedback [options]
|
|
10431
|
+
roll feedback (一句话提反馈)
|
|
10432
|
+
|
|
10433
|
+
Open a GitHub issue from the CLI. Type auto-labels (bug → FIX label;
|
|
10434
|
+
idea → US label; ux → ux label).
|
|
10435
|
+
|
|
10436
|
+
Options:
|
|
10437
|
+
--type <bug|idea|ux> Classify the feedback (default: bug)
|
|
10438
|
+
--title <text> Issue title (required)
|
|
10439
|
+
--body <text> Issue body
|
|
10440
|
+
--repo <owner/repo> Target repo (default: derived from origin)
|
|
10441
|
+
--no-env Skip the auto-attached Environment section
|
|
10442
|
+
(roll version, OS, agent, language, project)
|
|
10443
|
+
--print-url Print the prefilled github.com URL instead of
|
|
10444
|
+
invoking `gh`. Falls back to this automatically
|
|
10445
|
+
when `gh` is not installed.
|
|
10446
|
+
HELP
|
|
10447
|
+
return 0 ;;
|
|
10448
|
+
*)
|
|
10449
|
+
err "feedback: unknown flag $1"
|
|
10450
|
+
return 1 ;;
|
|
10451
|
+
esac
|
|
10452
|
+
done
|
|
10453
|
+
|
|
10454
|
+
if [ -z "$title" ]; then
|
|
10455
|
+
err "feedback: --title is required"
|
|
10456
|
+
return 1
|
|
10457
|
+
fi
|
|
10458
|
+
if [ -z "$type" ]; then
|
|
10459
|
+
type="bug"
|
|
10460
|
+
fi
|
|
10461
|
+
case "$type" in
|
|
10462
|
+
bug|idea|ux) ;;
|
|
10463
|
+
*)
|
|
10464
|
+
err "feedback: unknown --type '$type' (expected one of: bug, idea, ux)"
|
|
10465
|
+
return 1 ;;
|
|
10466
|
+
esac
|
|
10467
|
+
|
|
10468
|
+
if [ -z "$repo" ]; then
|
|
10469
|
+
repo=$(_feedback_default_repo)
|
|
10470
|
+
fi
|
|
10471
|
+
if [ -z "$repo" ]; then
|
|
10472
|
+
err "feedback: cannot derive owner/repo from origin; pass --repo owner/repo"
|
|
10473
|
+
return 1
|
|
10474
|
+
fi
|
|
10475
|
+
|
|
10476
|
+
# US-FB-002: compose final body with optional env appendix.
|
|
10477
|
+
if [ "$attach_env" -eq 1 ]; then
|
|
10478
|
+
body="${body}$(_feedback_env_block)"
|
|
10479
|
+
fi
|
|
10480
|
+
|
|
10481
|
+
local labels; labels=$(_feedback_label_for_type "$type")
|
|
10482
|
+
|
|
10483
|
+
# Decide path: --print-url or gh missing → print URL; else use gh.
|
|
10484
|
+
if [ "$print_url" -eq 1 ] || ! command -v gh >/dev/null 2>&1; then
|
|
10485
|
+
local t_enc b_enc l_enc
|
|
10486
|
+
t_enc=$(_feedback_urlencode "$title")
|
|
10487
|
+
b_enc=$(_feedback_urlencode "$body")
|
|
10488
|
+
l_enc=$(_feedback_urlencode "$labels")
|
|
10489
|
+
printf 'https://github.com/%s/issues/new?title=%s&body=%s&labels=%s\n' \
|
|
10490
|
+
"$repo" "$t_enc" "$b_enc" "$l_enc"
|
|
10491
|
+
return 0
|
|
10492
|
+
fi
|
|
10493
|
+
|
|
10494
|
+
# Real path: gh issue create
|
|
10495
|
+
gh issue create --repo "$repo" --title "$title" --body "$body" --label "$labels"
|
|
10496
|
+
}
|
|
10497
|
+
|
|
9824
10498
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
9825
10499
|
# LANG — switch / inspect Roll's UI language (US-I18N-001)
|
|
9826
10500
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -10681,6 +11355,7 @@ main() {
|
|
|
10681
11355
|
brief) cmd_brief "$@" ;;
|
|
10682
11356
|
backlog) cmd_backlog "$@" ;;
|
|
10683
11357
|
alert) cmd_alert "$@" ;;
|
|
11358
|
+
feedback) cmd_feedback "$@" ;;
|
|
10684
11359
|
lang) cmd_lang "$@" ;;
|
|
10685
11360
|
agent) cmd_agent "$@" ;;
|
|
10686
11361
|
ci) cmd_ci "$@" ;;
|