@seanyao/roll 2026.511.3 → 2026.511.5

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## v2026.511.5
4
+ - **Fixed**: launchd plist 自动 reload — plist 内容变更且服务已加载时自动 unload + reload,升级 roll 后 loop 服务立即生效,无需手动重启
5
+ - **Improved**: roll loop status/monitor 三态展示 — 区分 ● 运行中 / ⚠ 已安装未加载 / ○ 未安装,并给出对应的自愈操作提示
6
+
7
+ ## v2026.511.4
8
+ - **Fixed**: roll init 自动重建 launchd runner scripts — 升级 roll 后直接跑 `roll init` 即可迁移到独立 runner,无需手动执行 roll setup 或 roll loop on
9
+
3
10
  ## v2026.511.3
4
11
  - **Fixed**: loop/dream/brief 多项目运行隔离 — 共享 run.sh 导致所有项目的 loop 在同一目录执行,改为每个项目独立 runner 脚本(run-{slug}.sh),彻底隔离多项目并发执行环境
5
12
  - **Fixed**: roll release 自发版拦截 — 在 roll 自身项目执行 `roll release` 时自动拦截并提示改用 scripts/release.sh,防止误操作绕过 2FA
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.511.3"
7
+ VERSION="2026.511.5"
8
8
  ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
9
9
  ROLL_CONFIG="${ROLL_HOME}/config.yaml"
10
10
  ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
@@ -352,6 +352,17 @@ ai_deepseek: ~/.deepseek|AGENTS.md|AGENTS.md
352
352
  default_language: zh
353
353
  default_project_type: fullstack
354
354
  editor: ${EDITOR:-vim}
355
+
356
+ # Loop schedule (24h format, machine local timezone)
357
+ # Minute fields auto-derive from project path hash when omitted — avoids contention across projects.
358
+ loop_active_start: 10 # loop only fires inside this window (after human reviews brief)
359
+ loop_active_end: 18
360
+ # loop_minute: 5 # omit to auto-derive from project hash
361
+ loop_dream_hour: 3
362
+ # loop_dream_minute: 10 # omit to auto-derive
363
+ loop_brief_hour: 9
364
+ # loop_brief_minute: 15 # omit to auto-derive
365
+ primary_agent: claude
355
366
  YAML
356
367
  ok "Created: ~/.roll/config.yaml 已创建: ~/.roll/config.yaml"
357
368
  fi
@@ -758,6 +769,8 @@ cmd_init() {
758
769
  _sync_conventions
759
770
  echo ""
760
771
 
772
+ _install_launchd_plists "$project_dir"
773
+
761
774
  if [[ "$has_agents" == "true" ]]; then
762
775
  ok "Done. 完成。"
763
776
  else
@@ -1755,6 +1768,28 @@ _project_slug() {
1755
1768
  printf '%s' "${base}-${hash}"
1756
1769
  }
1757
1770
 
1771
+ _config_read_int() {
1772
+ local key="$1" default="$2"
1773
+ local val
1774
+ val=$(grep "^${key}:" "$ROLL_CONFIG" 2>/dev/null | awk '{print $2}' | tr -d '"' | head -1)
1775
+ if [[ "$val" =~ ^[0-9]+$ ]]; then echo "$val"; else echo "$default"; fi
1776
+ }
1777
+
1778
+ # Derive a minute in [1,55] from project path hash + offset so different projects
1779
+ # and different services within a project don't fire at the same time.
1780
+ # Offsets used: loop=0, dream=2, brief=4 → always three distinct values (2<55).
1781
+ _loop_derive_minute() {
1782
+ local project_path="$1" offset="${2:-0}"
1783
+ local hash_hex
1784
+ if command -v md5 &>/dev/null; then
1785
+ hash_hex=$(printf '%s' "$project_path" | md5 | cut -c1-6)
1786
+ else
1787
+ hash_hex=$(printf '%s' "$project_path" | md5sum | cut -c1-6)
1788
+ fi
1789
+ local hash_dec; hash_dec=$(printf '%d' "0x${hash_hex}")
1790
+ echo $(( (hash_dec + offset) % 55 + 1 ))
1791
+ }
1792
+
1758
1793
  _launchd_label() {
1759
1794
  local service="$1" project_path="$2"
1760
1795
  printf 'com.roll.%s.%s' "$service" "$(_project_slug "$project_path")"
@@ -1810,12 +1845,42 @@ _write_runner_script() {
1810
1845
  chmod +x "$script_path"
1811
1846
  }
1812
1847
 
1848
+ # Like _write_runner_script but prepends an active window guard.
1849
+ # Silently exits when current hour is outside [active_start, active_end).
1850
+ _write_loop_runner_script() {
1851
+ local script_path="$1" project_path="$2" cmd="$3" log_path="$4"
1852
+ local active_start="${5:-10}" active_end="${6:-18}"
1853
+ mkdir -p "$(dirname "$script_path")"
1854
+ cat > "$script_path" << SCRIPT
1855
+ #!/bin/bash -l
1856
+ h=\$(printf '%d' "\$(date +%H)")
1857
+ if [ "\$h" -lt ${active_start} ] || [ "\$h" -ge ${active_end} ]; then exit 0; fi
1858
+ cd "${project_path}" && ${cmd} >> "${log_path}" 2>&1
1859
+ SCRIPT
1860
+ chmod +x "$script_path"
1861
+ }
1862
+
1813
1863
  _launchd_is_loaded() {
1814
- launchctl list "$1" &>/dev/null 2>&1
1864
+ launchctl print-disabled "gui/$(id -u)" 2>/dev/null | grep -qF "\"$1\" => enabled"
1865
+ }
1866
+
1867
+ _launchd_svc_state() {
1868
+ local svc="$1" project_path="$2"
1869
+ local label; label=$(_launchd_label "$svc" "$project_path")
1870
+ local plist; plist=$(_launchd_plist_path "$svc" "$project_path")
1871
+ if _launchd_is_loaded "$label"; then
1872
+ echo "enabled"
1873
+ elif [[ -f "$plist" ]]; then
1874
+ echo "installed-off"
1875
+ else
1876
+ echo "not-installed"
1877
+ fi
1815
1878
  }
1816
1879
 
1817
1880
  # Install launchd plist files (disabled by default) and runner scripts for
1818
1881
  # a given project path. Idempotent — skips unchanged files. Does NOT load.
1882
+ # Schedule times are read from ~/.roll/config.yaml; missing fields are
1883
+ # auto-derived from the project path hash so different projects don't contend.
1819
1884
  _install_launchd_plists() {
1820
1885
  local project_path="$1"
1821
1886
  local sd="${ROLL_HOME}/skills"
@@ -1824,12 +1889,22 @@ _install_launchd_plists() {
1824
1889
  mkdir -p "$_LAUNCHD_DIR"
1825
1890
  mkdir -p "${shared}/loop" "${shared}/dream" "${shared}/brief"
1826
1891
 
1892
+ local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
1893
+ active_start=$(_config_read_int "loop_active_start" "10")
1894
+ active_end=$(_config_read_int "loop_active_end" "18")
1895
+ loop_minute=$(_config_read_int "loop_minute" "$(_loop_derive_minute "$project_path" 0)")
1896
+ dream_hour=$(_config_read_int "loop_dream_hour" "3")
1897
+ dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
1898
+ brief_hour=$(_config_read_int "loop_brief_hour" "9")
1899
+ brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
1900
+
1827
1901
  local services=("loop" "dream" "brief")
1828
1902
  local skill_names=("roll-loop" "roll-.dream" "roll-brief")
1829
- local minutes=("0" "0" "0")
1830
- local hours=("" "3" "9")
1903
+ local minutes=("$loop_minute" "$dream_minute" "$brief_minute")
1904
+ local hours=("" "$dream_hour" "$brief_hour")
1831
1905
 
1832
1906
  local updated=0
1907
+ local slug; slug=$(_project_slug "$project_path")
1833
1908
  for i in "${!services[@]}"; do
1834
1909
  local svc="${services[$i]}"
1835
1910
  local skill="${skill_names[$i]}"
@@ -1837,18 +1912,27 @@ _install_launchd_plists() {
1837
1912
  local hour="${hours[$i]}"
1838
1913
  local label; label=$(_launchd_label "$svc" "$project_path")
1839
1914
  local plist; plist=$(_launchd_plist_path "$svc" "$project_path")
1840
- local slug; slug=$(_project_slug "$project_path")
1841
1915
  local runner="${shared}/${svc}/run-${slug}.sh"
1842
1916
  local log="${shared}/${svc}/cron.log"
1843
1917
  local cmd; cmd=$(_agent_skill_cmd "${sd}/${skill}/SKILL.md" 2>/dev/null || echo "roll loop now")
1844
1918
 
1845
- _write_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log"
1919
+ if [[ "$svc" == "loop" ]]; then
1920
+ _write_loop_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log" "$active_start" "$active_end"
1921
+ else
1922
+ _write_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log"
1923
+ fi
1846
1924
 
1847
1925
  local before=""
1848
1926
  [[ -f "$plist" ]] && before=$(cat "$plist")
1849
1927
  _write_launchd_plist "$plist" "$label" "$project_path" "$minute" "$hour" "$runner"
1850
1928
  local after; after=$(cat "$plist")
1851
- [[ "$before" != "$after" ]] && updated=$((updated + 1))
1929
+ if [[ "$before" != "$after" ]]; then
1930
+ updated=$((updated + 1))
1931
+ if _launchd_is_loaded "$label"; then
1932
+ launchctl unload "$plist" 2>/dev/null || true
1933
+ launchctl load "$plist" 2>/dev/null || true
1934
+ fi
1935
+ fi
1852
1936
  done
1853
1937
 
1854
1938
  if [[ $updated -gt 0 ]]; then
@@ -1893,6 +1977,15 @@ _loop_on() {
1893
1977
  local project_path; project_path=$(pwd -P)
1894
1978
  local agent; agent=$(_project_agent)
1895
1979
 
1980
+ local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
1981
+ active_start=$(_config_read_int "loop_active_start" "10")
1982
+ active_end=$(_config_read_int "loop_active_end" "18")
1983
+ loop_minute=$(_config_read_int "loop_minute" "$(_loop_derive_minute "$project_path" 0)")
1984
+ dream_hour=$(_config_read_int "loop_dream_hour" "3")
1985
+ dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
1986
+ brief_hour=$(_config_read_int "loop_brief_hour" "9")
1987
+ brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
1988
+
1896
1989
  if [[ "$(uname)" == "Darwin" ]]; then
1897
1990
  _install_launchd_plists "$project_path" >/dev/null
1898
1991
 
@@ -1901,7 +1994,7 @@ _loop_on() {
1901
1994
  local label; label=$(_launchd_label "$svc" "$project_path")
1902
1995
  if ! _launchd_is_loaded "$label"; then
1903
1996
  all_loaded=false
1904
- launchctl load "$(_launchd_plist_path "$svc" "$project_path")" 2>/dev/null || true
1997
+ launchctl load -w "$(_launchd_plist_path "$svc" "$project_path")" 2>/dev/null || true
1905
1998
  fi
1906
1999
  done
1907
2000
 
@@ -1910,9 +2003,10 @@ _loop_on() {
1910
2003
  fi
1911
2004
 
1912
2005
  ok "Loop enabled 已启用"
1913
- echo " • roll-loop every hour :00 每小时整点"
1914
- echo " roll-.dream nightly at 03:00 每晚 03:00"
1915
- echo " • roll-brief daily at 09:00 每天 09:00"
2006
+ printf " • roll-loop every hour :%02d active %02d:00–%02d:00 每小时 :%02d(窗口 %02d:00–%02d:00)\n" \
2007
+ "$loop_minute" "$active_start" "$active_end" "$loop_minute" "$active_start" "$active_end"
2008
+ printf " • roll-.dream daily at %02d:%02d 每天 %02d:%02d\n" "$dream_hour" "$dream_minute" "$dream_hour" "$dream_minute"
2009
+ printf " • roll-brief daily at %02d:%02d 每天 %02d:%02d\n" "$brief_hour" "$brief_minute" "$brief_hour" "$brief_minute"
1916
2010
  echo " • Agent: ${agent} (change: roll agent use <name>)"
1917
2011
  return 0
1918
2012
  fi
@@ -1932,15 +2026,16 @@ _loop_on() {
1932
2026
 
1933
2027
  (
1934
2028
  crontab -l 2>/dev/null
1935
- echo "0 * * * * ${loop_cmd} ${_LOOP_TAG}:${project_path}"
1936
- echo "0 1 * * * ${dream_cmd} ${_LOOP_TAG}:${project_path}"
1937
- echo "0 8 * * * ${brief_cmd} ${_LOOP_TAG}:${project_path}"
2029
+ printf "%d * * * * %s %s:%s\n" "$loop_minute" "$loop_cmd" "$_LOOP_TAG" "$project_path"
2030
+ printf "%d %d * * * %s %s:%s\n" "$dream_minute" "$dream_hour" "$dream_cmd" "$_LOOP_TAG" "$project_path"
2031
+ printf "%d %d * * * %s %s:%s\n" "$brief_minute" "$brief_hour" "$brief_cmd" "$_LOOP_TAG" "$project_path"
1938
2032
  ) | crontab -
1939
2033
 
1940
2034
  ok "Loop enabled 已启用"
1941
- echo " • roll-loop every hour :00 每小时整点"
1942
- echo " roll-.dream nightly at 03:00 每晚 03:00"
1943
- echo " • roll-brief daily at 09:00 每天 09:00"
2035
+ printf " • roll-loop every hour :%02d active %02d:00–%02d:00 每小时 :%02d(窗口 %02d:00–%02d:00)\n" \
2036
+ "$loop_minute" "$active_start" "$active_end" "$loop_minute" "$active_start" "$active_end"
2037
+ printf " • roll-.dream daily at %02d:%02d 每天 %02d:%02d\n" "$dream_hour" "$dream_minute" "$dream_hour" "$dream_minute"
2038
+ printf " • roll-brief daily at %02d:%02d 每天 %02d:%02d\n" "$brief_hour" "$brief_minute" "$brief_hour" "$brief_minute"
1944
2039
  echo " • Agent: ${agent} (change: roll agent use <name>)"
1945
2040
  }
1946
2041
 
@@ -1953,7 +2048,7 @@ _loop_off() {
1953
2048
  local label; label=$(_launchd_label "$svc" "$project_path")
1954
2049
  if _launchd_is_loaded "$label"; then
1955
2050
  any_loaded=true
1956
- launchctl unload "$(_launchd_plist_path "$svc" "$project_path")" 2>/dev/null || true
2051
+ launchctl unload -w "$(_launchd_plist_path "$svc" "$project_path")" 2>/dev/null || true
1957
2052
  fi
1958
2053
  done
1959
2054
  if ! $any_loaded; then
@@ -1993,16 +2088,24 @@ _loop_status() {
1993
2088
  local project_path; project_path=$(pwd -P)
1994
2089
  local agent; agent=$(_project_agent)
1995
2090
  echo ""
1996
- local loop_enabled=false
1997
2091
  if [[ "$(uname)" == "Darwin" ]]; then
1998
- _launchd_is_loaded "$(_launchd_label "loop" "$project_path")" && loop_enabled=true
2092
+ echo -e " Services Agent: ${CYAN}${agent}${NC}"
2093
+ for svc in loop dream brief; do
2094
+ local state; state=$(_launchd_svc_state "$svc" "$project_path")
2095
+ case "$state" in
2096
+ enabled) echo -e " ${GREEN}${svc} ● enabled${NC}" ;;
2097
+ installed-off) echo -e " ${YELLOW}${svc} ⚠ installed/off${NC} run: roll loop on" ;;
2098
+ not-installed) echo -e " ${RED}${svc} ○ not installed${NC} run: roll setup" ;;
2099
+ esac
2100
+ done
1999
2101
  else
2102
+ local loop_enabled=false
2000
2103
  crontab -l 2>/dev/null | grep -q "${_LOOP_TAG}:${project_path}" && loop_enabled=true
2001
- fi
2002
- if $loop_enabled; then
2003
- echo -e " Scheduler ${GREEN}● enabled${NC} Agent: ${CYAN}${agent}${NC}"
2004
- else
2005
- echo -e " Scheduler ${YELLOW}○ disabled${NC} run: roll loop on"
2104
+ if $loop_enabled; then
2105
+ echo -e " Scheduler ${GREEN}● enabled${NC} Agent: ${CYAN}${agent}${NC}"
2106
+ else
2107
+ echo -e " Scheduler ${YELLOW}○ disabled${NC} run: roll loop on"
2108
+ fi
2006
2109
  fi
2007
2110
  [[ -f "$_LOOP_ALERT" ]] && { echo ""; echo -e " ${RED}⚠ ALERT:${NC}"; sed 's/^/ /' "$_LOOP_ALERT"; }
2008
2111
  [[ -f "$_LOOP_STATE" ]] && { echo ""; echo " State:"; sed 's/^/ /' "$_LOOP_STATE"; }
@@ -2093,15 +2196,30 @@ _loop_monitor() {
2093
2196
  # Services status (three services on macOS, single on Linux)
2094
2197
  echo -e " ${BOLD}Services 服务状态${NC} Agent: ${CYAN}${agent}${NC}"
2095
2198
  if [[ "$(uname)" == "Darwin" ]]; then
2096
- local svc_info svc schedule
2097
- for svc_info in "loop:hourly" "dream:03:00" "brief:09:00"; do
2098
- svc="${svc_info%%:*}"
2099
- schedule="${svc_info#*:}"
2100
- if _launchd_is_loaded "$(_launchd_label "$svc" "$project_path")"; then
2101
- printf " ${GREEN}%-8s ● enabled${NC} (%s)\n" "$svc" "$schedule"
2102
- else
2103
- printf " ${YELLOW}%-8s ○ disabled${NC} (%s)\n" "$svc" "$schedule"
2104
- fi
2199
+ local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
2200
+ active_start=$(_config_read_int "loop_active_start" "10")
2201
+ active_end=$(_config_read_int "loop_active_end" "18")
2202
+ loop_minute=$(_config_read_int "loop_minute" "$(_loop_derive_minute "$project_path" 0)")
2203
+ dream_hour=$(_config_read_int "loop_dream_hour" "3")
2204
+ dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
2205
+ brief_hour=$(_config_read_int "loop_brief_hour" "9")
2206
+ brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
2207
+
2208
+ local loop_sched dream_sched brief_sched
2209
+ loop_sched=$(printf "every hour :%02d active %02d:00–%02d:00" "$loop_minute" "$active_start" "$active_end")
2210
+ dream_sched=$(printf "%02d:%02d" "$dream_hour" "$dream_minute")
2211
+ brief_sched=$(printf "%02d:%02d" "$brief_hour" "$brief_minute")
2212
+
2213
+ local svcs=("loop" "dream" "brief")
2214
+ local scheds=("$loop_sched" "$dream_sched" "$brief_sched")
2215
+ for i in "${!svcs[@]}"; do
2216
+ local svc="${svcs[$i]}" schedule="${scheds[$i]}"
2217
+ local state; state=$(_launchd_svc_state "$svc" "$project_path")
2218
+ case "$state" in
2219
+ enabled) printf " ${GREEN}%-8s ● enabled${NC} (%s)\n" "$svc" "$schedule" ;;
2220
+ installed-off) printf " ${YELLOW}%-8s ⚠ installed/off${NC} (%s) run: roll loop on\n" "$svc" "$schedule" ;;
2221
+ not-installed) printf " ${RED}%-8s ○ not installed${NC} (%s) run: roll setup\n" "$svc" "$schedule" ;;
2222
+ esac
2105
2223
  done
2106
2224
  else
2107
2225
  if crontab -l 2>/dev/null | grep -q "${_LOOP_TAG}:${project_path}"; then
@@ -2246,20 +2364,23 @@ cmd_backlog() {
2246
2364
  return 1
2247
2365
  fi
2248
2366
 
2249
- local us_items fix_items refactor_items total=0
2367
+ local us_items fix_items refactor_items idea_items total=0
2250
2368
 
2251
2369
  us_items=$(grep -E '^\| \[US-' "$backlog" | grep -F '| 📋 Todo |' || true)
2252
2370
  fix_items=$(grep -E '^\| FIX-' "$backlog" | grep -F '| 📋 Todo |' || true)
2253
2371
  refactor_items=$(grep -E '^\| REFACTOR-' "$backlog" | grep -F '| 📋 Todo |' || true)
2372
+ idea_items=$(grep -E '^\| IDEA-' "$backlog" | grep -F '| 📋 Todo |' || true)
2254
2373
 
2255
- local us_count fix_count refactor_count
2374
+ local us_count fix_count refactor_count idea_count
2256
2375
  us_count=$(echo "$us_items" | grep -c . || true)
2257
2376
  fix_count=$(echo "$fix_items" | grep -c . || true)
2258
2377
  refactor_count=$(echo "$refactor_items" | grep -c . || true)
2378
+ idea_count=$(echo "$idea_items" | grep -c . || true)
2259
2379
  [[ -z "$us_items" ]] && us_count=0
2260
2380
  [[ -z "$fix_items" ]] && fix_count=0
2261
2381
  [[ -z "$refactor_items" ]] && refactor_count=0
2262
- total=$(( us_count + fix_count + refactor_count ))
2382
+ [[ -z "$idea_items" ]] && idea_count=0
2383
+ total=$(( us_count + fix_count + refactor_count + idea_count ))
2263
2384
 
2264
2385
  echo ""
2265
2386
  echo -e " ${BOLD}Pending Backlog 待处理任务${NC} (${total} items)"
@@ -2298,6 +2419,17 @@ cmd_backlog() {
2298
2419
  echo ""
2299
2420
  fi
2300
2421
 
2422
+ if [[ $idea_count -gt 0 ]]; then
2423
+ echo -e " ${NC}Ideas 创意 (${idea_count})"
2424
+ while IFS= read -r line; do
2425
+ local id desc
2426
+ id=$(echo "$line" | awk -F'|' '{print $2}' | tr -d ' ')
2427
+ desc=$(echo "$line" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//')
2428
+ printf " %-14s %s\n" "$id" "$desc"
2429
+ done <<< "$idea_items"
2430
+ echo ""
2431
+ fi
2432
+
2301
2433
  if [[ $total -eq 0 ]]; then
2302
2434
  echo -e " ${GREEN}✓ Nothing pending — backlog is clear 暂无待处理任务${NC}"
2303
2435
  echo ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "2026.511.3",
3
+ "version": "2026.511.5",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "scripts": {
6
6
  "test": "find tests/unit tests/integration -name '*.bats' | sort | xargs ./tests/helpers/bats-core/bin/bats"
@@ -48,8 +48,8 @@ $roll-design "user system design"
48
48
  # Split Stories from an existing Plan
49
49
  $roll-design --from-plan docs/features/auth-plan.md
50
50
 
51
- # Directly create a Story
52
- $roll-design --story "user login feature"
51
+ # Directly create a Story (auto-detected as User Story → Slice DDD)
52
+ $roll-design "user login feature"
53
53
  ```
54
54
 
55
55
  ## DDD Depth Scale
@@ -622,7 +622,7 @@ $roll-build US-AUTH-001 → TCR → CI/CD → Deploy
622
622
 
623
623
  ```
624
624
  $roll-debug discovers issue → Suggest creating FIX
625
- $roll-design --fix "fix login API 404" → Create FIX-AUTH-001
625
+ $roll-design "fix login API 404" → Create FIX-AUTH-001 ← auto-detected as Bug Fix
626
626
  $roll-fix FIX-AUTH-001 → Quick fix
627
627
  ```
628
628
 
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: roll-propose
3
+ license: MIT
4
+ allowed-tools: "Read, Glob, Grep, Write, Bash(git:*)"
5
+ description: |
6
+ Human-triggered product proposal generator. Reads project context (BACKLOG,
7
+ recent commits, existing skills) and generates 1–3 structured US drafts from
8
+ a user-facing perspective. Writes to PROPOSALS.md for human review — never
9
+ directly to BACKLOG. Distinct from roll-.dream (which surfaces technical debt
10
+ from execution experience); roll-propose thinks in user scenarios and feature
11
+ gaps.
12
+ ---
13
+
14
+ # roll-propose
15
+
16
+ > Follows the Architecture Constraints, Development Discipline, and Engineering
17
+ > Common Sense defined in the project AGENTS.md.
18
+
19
+ Human-triggered skill for product-level feature ideation. Generates structured
20
+ User Story drafts from a product/user perspective and queues them in
21
+ PROPOSALS.md for human approval before entering BACKLOG.
22
+
23
+ ## Distinct from roll-.dream
24
+
25
+ | | roll-propose | roll-.dream |
26
+ |---|---|---|
27
+ | Triggered by | Human explicitly | Nightly schedule |
28
+ | Perspective | User-facing / product scenarios | Code health / technical debt |
29
+ | Output | PROPOSALS.md (pending approval) | BACKLOG (REFACTOR-XXX) |
30
+ | Thinking style | "What would users want next?" | "What is the code telling us?" |
31
+
32
+ ## When to Use
33
+
34
+ ```
35
+ $roll-propose # generate proposals from full context
36
+ $roll-propose 用户反馈里提到了XX # provide a focus hint
37
+ ```
38
+
39
+ ## When Not to Use
40
+
41
+ - Describing a known defect or broken behavior (use `$roll-idea`)
42
+ - A story is already well-defined and ready to build (use `$roll-build`)
43
+ - Exploring technical architecture or design (use `$roll-design`)
44
+ - Surfacing code-level technical debt (use `$roll-.dream`)
45
+
46
+ ## Behavior
47
+
48
+ ### Step 1 — Gather Context
49
+
50
+ Read in parallel:
51
+
52
+ 1. `BACKLOG.md` — all existing US-XXX, FIX-XXX, REFACTOR-XXX, IDEA-XXX entries (both Todo and Done) to avoid proposing duplicates
53
+ 2. `PROPOSALS.md` (if exists) — already-proposed items (avoid re-proposing rejected or pending ones)
54
+ 3. Recent 20 commits via `git log --oneline -20` — what has recently shipped
55
+ 4. `skills/` directory listing — what capabilities roll already has
56
+ 5. Optional: any focus hint passed by the user
57
+
58
+ ### Step 2 — Think from User Perspective
59
+
60
+ Frame proposals from the **product engineer / end user** point of view:
61
+
62
+ - What recurring friction do users of roll face that no current skill addresses?
63
+ - What workflow is partially covered but has visible gaps?
64
+ - What would make the autonomous loop more legible, controllable, or trustworthy to its human owner?
65
+
66
+ Avoid technical-debt reasoning (that is roll-.dream's domain). Focus on:
67
+ - New user-visible commands or behaviors
68
+ - Improvements to existing UX (output clarity, discoverability, onboarding)
69
+ - Integrations that extend reach (new AI tools, editor support, CI patterns)
70
+
71
+ ### Step 3 — Draft 1–3 Proposals
72
+
73
+ Generate between 1 and 3 proposals. For each:
74
+
75
+ ```
76
+ ## PROPOSAL: {Short title}
77
+
78
+ **Motivation (why):**
79
+ One to two sentences from the user's perspective explaining the pain or opportunity.
80
+
81
+ **Target scenario:**
82
+ Concrete usage example — what the user does, what they see, what they gain.
83
+
84
+ **Acceptance Criteria (draft):**
85
+ - [ ] AC 1
86
+ - [ ] AC 2
87
+ - [ ] AC 3
88
+
89
+ **Suggested ID:** US-{EPIC}-{NNN} (best-guess prefix; human assigns final ID)
90
+ **Suggested Epic / Feature:** {name}
91
+ **Estimated complexity:** {S | M | L}
92
+ ```
93
+
94
+ Complexity guide: S = one skill file or small bin/roll change, M = skill + bin/roll + tests, L = multi-file + new infrastructure.
95
+
96
+ ### Step 4 — Write to PROPOSALS.md
97
+
98
+ Append to `PROPOSALS.md` in the project root (create if absent):
99
+
100
+ ```markdown
101
+ ---
102
+ proposed: {YYYY-MM-DD HH:MM}
103
+ status: pending
104
+ ---
105
+
106
+ {proposals from Step 3}
107
+ ```
108
+
109
+ Use `---` as separator between proposal batches. Never overwrite existing content.
110
+
111
+ ### Step 5 — Report
112
+
113
+ ```
114
+ ✅ roll-propose: {N} proposal(s) written to PROPOSALS.md
115
+
116
+ To approve: move the entry to BACKLOG.md and assign a US-XXX ID.
117
+ To reject: annotate with "Rejected: {reason}" to suppress future re-proposals.
118
+ ```
119
+
120
+ ## Output Rules
121
+
122
+ - Write proposals in the same language as the project's primary documentation (Chinese for this project).
123
+ - Never write directly to BACKLOG.md — PROPOSALS.md is the staging area.
124
+ - If a similar proposal already exists in PROPOSALS.md (pending or rejected), note similarity and skip or merge rather than creating a duplicate.
125
+ - Aim for 2 proposals by default; generate 1 if context is thin, 3 if focus hint suggests a rich area.
126
+
127
+ ## PROPOSALS.md Format
128
+
129
+ ```markdown
130
+ # Roll Proposals
131
+
132
+ > 待审批提案。批准后手工移入 BACKLOG.md 并分配 US-XXX 编号。
133
+ > 拒绝时在条目末尾注明拒绝原因,防止 Agent 重复提出相似提案。
134
+
135
+ ---
136
+ proposed: 2026-05-11 11:30
137
+ status: pending
138
+ ---
139
+
140
+ ## PROPOSAL: ...
141
+
142
+ ...
143
+
144
+ ---
145
+ proposed: 2026-05-08 09:00
146
+ status: rejected
147
+ rejected_reason: 与现有 roll-design 功能重叠,不需要单独技能
148
+ ---
149
+
150
+ ## PROPOSAL: ...
151
+
152
+ ...
153
+ ```