@seanyao/roll 2026.511.4 → 2026.511.6
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 +17 -0
- package/bin/roll +200 -44
- package/package.json +1 -1
- package/skills/roll-build/SKILL.md +1 -1
- package/skills/roll-design/SKILL.md +3 -3
- package/skills/roll-fix/SKILL.md +1 -1
- package/skills/roll-loop/SKILL.md +43 -2
- package/skills/roll-propose/SKILL.md +153 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2026.511.8
|
|
4
|
+
- **Fixed**: 集成测试 launchd ghost 泄漏 — `integration_teardown` 在删除 TEST_TMP 之前,先 `launchctl bootout` 该沙箱里被 `roll loop on` 注册到 user gui domain 的所有 `com.roll.*` 服务,避免删 plist 后 launchd 仍保留指向不存在路径的 ghost 注册。
|
|
5
|
+
|
|
6
|
+
## v2026.511.7
|
|
7
|
+
- **Added**: loop 执行 story 前显式标记 🔨 In Progress — roll-loop SKILL 在调用 executor 之前先把 BACKLOG 中的故事状态从 📋 Todo 改为 🔨 In Progress 并提交 `chore: mark US-XXX in progress`,brief 简报和 peer agent 都能即时感知正在进行的工作,tcr 微提交不再"对 brief 不可见"。
|
|
8
|
+
- **Added**: loop 启动时孤儿 🔨 自愈 — 扫描 BACKLOG 中无对应 state.yaml running item 的 🔨 条目,视为上次崩溃残留,自动 revert 回 📋 Todo 并写 ALERT,避免被"卡"在错误的中间状态里。
|
|
9
|
+
- **Improved**: roll-build / roll-fix SKILL 状态转换段更新 — 显式接受 📋 Todo 或 🔨 In Progress 作为 ✅ Done 前置状态,loop 触发链路状态过渡更稳健。
|
|
10
|
+
|
|
11
|
+
## v2026.511.6
|
|
12
|
+
- **Added**: Loop 并发安全 — runner script 启动时写入 per-project LOCK 文件并检测重入;活跃 PID 已存在则跳过本次,残留死 LOCK 自动清理;正常/异常退出均通过 trap 清掉 LOCK。彻底防止两个 loop 实例同时启动造成的 BACKLOG/git 冲突。
|
|
13
|
+
- **Added**: roll-loop SKILL 显式声明 skip-🔨 In Progress 语义 — claude 扫 BACKLOG 时跳过已被人工或 peer agent 标记的执行中条目,为人机协同和多 agent 协作奠定基础。
|
|
14
|
+
- **Fixed**: 5 个 pre-existing 测试失败 — `run_roll` helper 切换到 TEST_TMP 作为 cwd 避免 slug 冲突;loop status 测试匹配三态显示新文案;dashboard 测试匹配 `_launchd_svc_state` + array 派生 schedule 的新结构。
|
|
15
|
+
|
|
16
|
+
## v2026.511.5
|
|
17
|
+
- **Fixed**: launchd plist 自动 reload — plist 内容变更且服务已加载时自动 unload + reload,升级 roll 后 loop 服务立即生效,无需手动重启
|
|
18
|
+
- **Improved**: roll loop status/monitor 三态展示 — 区分 ● 运行中 / ⚠ 已安装未加载 / ○ 未安装,并给出对应的自愈操作提示
|
|
19
|
+
|
|
3
20
|
## v2026.511.4
|
|
4
21
|
- **Fixed**: roll init 自动重建 launchd runner scripts — 升级 roll 后直接跑 `roll init` 即可迁移到独立 runner,无需手动执行 roll setup 或 roll loop on
|
|
5
22
|
|
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.
|
|
7
|
+
VERSION="2026.511.6"
|
|
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
|
|
@@ -1757,6 +1768,28 @@ _project_slug() {
|
|
|
1757
1768
|
printf '%s' "${base}-${hash}"
|
|
1758
1769
|
}
|
|
1759
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
|
+
|
|
1760
1793
|
_launchd_label() {
|
|
1761
1794
|
local service="$1" project_path="$2"
|
|
1762
1795
|
printf 'com.roll.%s.%s' "$service" "$(_project_slug "$project_path")"
|
|
@@ -1812,12 +1845,53 @@ _write_runner_script() {
|
|
|
1812
1845
|
chmod +x "$script_path"
|
|
1813
1846
|
}
|
|
1814
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
|
+
LOCK="\$(dirname "\$0")/.LOCK-\$(basename "\$0" .sh | sed 's/^run-//')"
|
|
1859
|
+
if [ -f "\$LOCK" ]; then
|
|
1860
|
+
prev_pid=\$(head -1 "\$LOCK" 2>/dev/null || echo "")
|
|
1861
|
+
if [ -n "\$prev_pid" ] && kill -0 "\$prev_pid" 2>/dev/null; then
|
|
1862
|
+
echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] loop already running (PID \$prev_pid), skipping" >> "${log_path}"
|
|
1863
|
+
exit 0
|
|
1864
|
+
fi
|
|
1865
|
+
rm -f "\$LOCK"
|
|
1866
|
+
fi
|
|
1867
|
+
echo "\$\$" > "\$LOCK"
|
|
1868
|
+
trap 'rm -f "\$LOCK"' EXIT
|
|
1869
|
+
cd "${project_path}" && ${cmd} >> "${log_path}" 2>&1
|
|
1870
|
+
SCRIPT
|
|
1871
|
+
chmod +x "$script_path"
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1815
1874
|
_launchd_is_loaded() {
|
|
1816
|
-
launchctl
|
|
1875
|
+
launchctl print-disabled "gui/$(id -u)" 2>/dev/null | grep -qF "\"$1\" => enabled"
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
_launchd_svc_state() {
|
|
1879
|
+
local svc="$1" project_path="$2"
|
|
1880
|
+
local label; label=$(_launchd_label "$svc" "$project_path")
|
|
1881
|
+
local plist; plist=$(_launchd_plist_path "$svc" "$project_path")
|
|
1882
|
+
if _launchd_is_loaded "$label"; then
|
|
1883
|
+
echo "enabled"
|
|
1884
|
+
elif [[ -f "$plist" ]]; then
|
|
1885
|
+
echo "installed-off"
|
|
1886
|
+
else
|
|
1887
|
+
echo "not-installed"
|
|
1888
|
+
fi
|
|
1817
1889
|
}
|
|
1818
1890
|
|
|
1819
1891
|
# Install launchd plist files (disabled by default) and runner scripts for
|
|
1820
1892
|
# a given project path. Idempotent — skips unchanged files. Does NOT load.
|
|
1893
|
+
# Schedule times are read from ~/.roll/config.yaml; missing fields are
|
|
1894
|
+
# auto-derived from the project path hash so different projects don't contend.
|
|
1821
1895
|
_install_launchd_plists() {
|
|
1822
1896
|
local project_path="$1"
|
|
1823
1897
|
local sd="${ROLL_HOME}/skills"
|
|
@@ -1826,12 +1900,22 @@ _install_launchd_plists() {
|
|
|
1826
1900
|
mkdir -p "$_LAUNCHD_DIR"
|
|
1827
1901
|
mkdir -p "${shared}/loop" "${shared}/dream" "${shared}/brief"
|
|
1828
1902
|
|
|
1903
|
+
local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
|
|
1904
|
+
active_start=$(_config_read_int "loop_active_start" "10")
|
|
1905
|
+
active_end=$(_config_read_int "loop_active_end" "18")
|
|
1906
|
+
loop_minute=$(_config_read_int "loop_minute" "$(_loop_derive_minute "$project_path" 0)")
|
|
1907
|
+
dream_hour=$(_config_read_int "loop_dream_hour" "3")
|
|
1908
|
+
dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
|
|
1909
|
+
brief_hour=$(_config_read_int "loop_brief_hour" "9")
|
|
1910
|
+
brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
|
|
1911
|
+
|
|
1829
1912
|
local services=("loop" "dream" "brief")
|
|
1830
1913
|
local skill_names=("roll-loop" "roll-.dream" "roll-brief")
|
|
1831
|
-
local minutes=("
|
|
1832
|
-
local hours=("" "
|
|
1914
|
+
local minutes=("$loop_minute" "$dream_minute" "$brief_minute")
|
|
1915
|
+
local hours=("" "$dream_hour" "$brief_hour")
|
|
1833
1916
|
|
|
1834
1917
|
local updated=0
|
|
1918
|
+
local slug; slug=$(_project_slug "$project_path")
|
|
1835
1919
|
for i in "${!services[@]}"; do
|
|
1836
1920
|
local svc="${services[$i]}"
|
|
1837
1921
|
local skill="${skill_names[$i]}"
|
|
@@ -1839,18 +1923,27 @@ _install_launchd_plists() {
|
|
|
1839
1923
|
local hour="${hours[$i]}"
|
|
1840
1924
|
local label; label=$(_launchd_label "$svc" "$project_path")
|
|
1841
1925
|
local plist; plist=$(_launchd_plist_path "$svc" "$project_path")
|
|
1842
|
-
local slug; slug=$(_project_slug "$project_path")
|
|
1843
1926
|
local runner="${shared}/${svc}/run-${slug}.sh"
|
|
1844
1927
|
local log="${shared}/${svc}/cron.log"
|
|
1845
1928
|
local cmd; cmd=$(_agent_skill_cmd "${sd}/${skill}/SKILL.md" 2>/dev/null || echo "roll loop now")
|
|
1846
1929
|
|
|
1847
|
-
|
|
1930
|
+
if [[ "$svc" == "loop" ]]; then
|
|
1931
|
+
_write_loop_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log" "$active_start" "$active_end"
|
|
1932
|
+
else
|
|
1933
|
+
_write_runner_script "$runner" "$project_path" "cd \"${project_path}\" && ${cmd}" "$log"
|
|
1934
|
+
fi
|
|
1848
1935
|
|
|
1849
1936
|
local before=""
|
|
1850
1937
|
[[ -f "$plist" ]] && before=$(cat "$plist")
|
|
1851
1938
|
_write_launchd_plist "$plist" "$label" "$project_path" "$minute" "$hour" "$runner"
|
|
1852
1939
|
local after; after=$(cat "$plist")
|
|
1853
|
-
[[ "$before" != "$after" ]]
|
|
1940
|
+
if [[ "$before" != "$after" ]]; then
|
|
1941
|
+
updated=$((updated + 1))
|
|
1942
|
+
if _launchd_is_loaded "$label"; then
|
|
1943
|
+
launchctl unload "$plist" 2>/dev/null || true
|
|
1944
|
+
launchctl load "$plist" 2>/dev/null || true
|
|
1945
|
+
fi
|
|
1946
|
+
fi
|
|
1854
1947
|
done
|
|
1855
1948
|
|
|
1856
1949
|
if [[ $updated -gt 0 ]]; then
|
|
@@ -1895,6 +1988,15 @@ _loop_on() {
|
|
|
1895
1988
|
local project_path; project_path=$(pwd -P)
|
|
1896
1989
|
local agent; agent=$(_project_agent)
|
|
1897
1990
|
|
|
1991
|
+
local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
|
|
1992
|
+
active_start=$(_config_read_int "loop_active_start" "10")
|
|
1993
|
+
active_end=$(_config_read_int "loop_active_end" "18")
|
|
1994
|
+
loop_minute=$(_config_read_int "loop_minute" "$(_loop_derive_minute "$project_path" 0)")
|
|
1995
|
+
dream_hour=$(_config_read_int "loop_dream_hour" "3")
|
|
1996
|
+
dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
|
|
1997
|
+
brief_hour=$(_config_read_int "loop_brief_hour" "9")
|
|
1998
|
+
brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
|
|
1999
|
+
|
|
1898
2000
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
1899
2001
|
_install_launchd_plists "$project_path" >/dev/null
|
|
1900
2002
|
|
|
@@ -1903,7 +2005,7 @@ _loop_on() {
|
|
|
1903
2005
|
local label; label=$(_launchd_label "$svc" "$project_path")
|
|
1904
2006
|
if ! _launchd_is_loaded "$label"; then
|
|
1905
2007
|
all_loaded=false
|
|
1906
|
-
launchctl load "$(_launchd_plist_path "$svc" "$project_path")" 2>/dev/null || true
|
|
2008
|
+
launchctl load -w "$(_launchd_plist_path "$svc" "$project_path")" 2>/dev/null || true
|
|
1907
2009
|
fi
|
|
1908
2010
|
done
|
|
1909
2011
|
|
|
@@ -1912,9 +2014,10 @@ _loop_on() {
|
|
|
1912
2014
|
fi
|
|
1913
2015
|
|
|
1914
2016
|
ok "Loop enabled 已启用"
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2017
|
+
printf " • roll-loop every hour :%02d active %02d:00–%02d:00 每小时 :%02d(窗口 %02d:00–%02d:00)\n" \
|
|
2018
|
+
"$loop_minute" "$active_start" "$active_end" "$loop_minute" "$active_start" "$active_end"
|
|
2019
|
+
printf " • roll-.dream daily at %02d:%02d 每天 %02d:%02d\n" "$dream_hour" "$dream_minute" "$dream_hour" "$dream_minute"
|
|
2020
|
+
printf " • roll-brief daily at %02d:%02d 每天 %02d:%02d\n" "$brief_hour" "$brief_minute" "$brief_hour" "$brief_minute"
|
|
1918
2021
|
echo " • Agent: ${agent} (change: roll agent use <name>)"
|
|
1919
2022
|
return 0
|
|
1920
2023
|
fi
|
|
@@ -1934,15 +2037,16 @@ _loop_on() {
|
|
|
1934
2037
|
|
|
1935
2038
|
(
|
|
1936
2039
|
crontab -l 2>/dev/null
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2040
|
+
printf "%d * * * * %s %s:%s\n" "$loop_minute" "$loop_cmd" "$_LOOP_TAG" "$project_path"
|
|
2041
|
+
printf "%d %d * * * %s %s:%s\n" "$dream_minute" "$dream_hour" "$dream_cmd" "$_LOOP_TAG" "$project_path"
|
|
2042
|
+
printf "%d %d * * * %s %s:%s\n" "$brief_minute" "$brief_hour" "$brief_cmd" "$_LOOP_TAG" "$project_path"
|
|
1940
2043
|
) | crontab -
|
|
1941
2044
|
|
|
1942
2045
|
ok "Loop enabled 已启用"
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
2046
|
+
printf " • roll-loop every hour :%02d active %02d:00–%02d:00 每小时 :%02d(窗口 %02d:00–%02d:00)\n" \
|
|
2047
|
+
"$loop_minute" "$active_start" "$active_end" "$loop_minute" "$active_start" "$active_end"
|
|
2048
|
+
printf " • roll-.dream daily at %02d:%02d 每天 %02d:%02d\n" "$dream_hour" "$dream_minute" "$dream_hour" "$dream_minute"
|
|
2049
|
+
printf " • roll-brief daily at %02d:%02d 每天 %02d:%02d\n" "$brief_hour" "$brief_minute" "$brief_hour" "$brief_minute"
|
|
1946
2050
|
echo " • Agent: ${agent} (change: roll agent use <name>)"
|
|
1947
2051
|
}
|
|
1948
2052
|
|
|
@@ -1955,7 +2059,7 @@ _loop_off() {
|
|
|
1955
2059
|
local label; label=$(_launchd_label "$svc" "$project_path")
|
|
1956
2060
|
if _launchd_is_loaded "$label"; then
|
|
1957
2061
|
any_loaded=true
|
|
1958
|
-
launchctl unload "$(_launchd_plist_path "$svc" "$project_path")" 2>/dev/null || true
|
|
2062
|
+
launchctl unload -w "$(_launchd_plist_path "$svc" "$project_path")" 2>/dev/null || true
|
|
1959
2063
|
fi
|
|
1960
2064
|
done
|
|
1961
2065
|
if ! $any_loaded; then
|
|
@@ -1995,16 +2099,24 @@ _loop_status() {
|
|
|
1995
2099
|
local project_path; project_path=$(pwd -P)
|
|
1996
2100
|
local agent; agent=$(_project_agent)
|
|
1997
2101
|
echo ""
|
|
1998
|
-
local loop_enabled=false
|
|
1999
2102
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
2000
|
-
|
|
2103
|
+
echo -e " Services Agent: ${CYAN}${agent}${NC}"
|
|
2104
|
+
for svc in loop dream brief; do
|
|
2105
|
+
local state; state=$(_launchd_svc_state "$svc" "$project_path")
|
|
2106
|
+
case "$state" in
|
|
2107
|
+
enabled) echo -e " ${GREEN}${svc} ● enabled${NC}" ;;
|
|
2108
|
+
installed-off) echo -e " ${YELLOW}${svc} ⚠ installed/off${NC} run: roll loop on" ;;
|
|
2109
|
+
not-installed) echo -e " ${RED}${svc} ○ not installed${NC} run: roll setup" ;;
|
|
2110
|
+
esac
|
|
2111
|
+
done
|
|
2001
2112
|
else
|
|
2113
|
+
local loop_enabled=false
|
|
2002
2114
|
crontab -l 2>/dev/null | grep -q "${_LOOP_TAG}:${project_path}" && loop_enabled=true
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2115
|
+
if $loop_enabled; then
|
|
2116
|
+
echo -e " Scheduler ${GREEN}● enabled${NC} Agent: ${CYAN}${agent}${NC}"
|
|
2117
|
+
else
|
|
2118
|
+
echo -e " Scheduler ${YELLOW}○ disabled${NC} run: roll loop on"
|
|
2119
|
+
fi
|
|
2008
2120
|
fi
|
|
2009
2121
|
[[ -f "$_LOOP_ALERT" ]] && { echo ""; echo -e " ${RED}⚠ ALERT:${NC}"; sed 's/^/ /' "$_LOOP_ALERT"; }
|
|
2010
2122
|
[[ -f "$_LOOP_STATE" ]] && { echo ""; echo " State:"; sed 's/^/ /' "$_LOOP_STATE"; }
|
|
@@ -2095,15 +2207,30 @@ _loop_monitor() {
|
|
|
2095
2207
|
# Services status (three services on macOS, single on Linux)
|
|
2096
2208
|
echo -e " ${BOLD}Services 服务状态${NC} Agent: ${CYAN}${agent}${NC}"
|
|
2097
2209
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
2098
|
-
local
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2210
|
+
local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
|
|
2211
|
+
active_start=$(_config_read_int "loop_active_start" "10")
|
|
2212
|
+
active_end=$(_config_read_int "loop_active_end" "18")
|
|
2213
|
+
loop_minute=$(_config_read_int "loop_minute" "$(_loop_derive_minute "$project_path" 0)")
|
|
2214
|
+
dream_hour=$(_config_read_int "loop_dream_hour" "3")
|
|
2215
|
+
dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
|
|
2216
|
+
brief_hour=$(_config_read_int "loop_brief_hour" "9")
|
|
2217
|
+
brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
|
|
2218
|
+
|
|
2219
|
+
local loop_sched dream_sched brief_sched
|
|
2220
|
+
loop_sched=$(printf "every hour :%02d active %02d:00–%02d:00" "$loop_minute" "$active_start" "$active_end")
|
|
2221
|
+
dream_sched=$(printf "%02d:%02d" "$dream_hour" "$dream_minute")
|
|
2222
|
+
brief_sched=$(printf "%02d:%02d" "$brief_hour" "$brief_minute")
|
|
2223
|
+
|
|
2224
|
+
local svcs=("loop" "dream" "brief")
|
|
2225
|
+
local scheds=("$loop_sched" "$dream_sched" "$brief_sched")
|
|
2226
|
+
for i in "${!svcs[@]}"; do
|
|
2227
|
+
local svc="${svcs[$i]}" schedule="${scheds[$i]}"
|
|
2228
|
+
local state; state=$(_launchd_svc_state "$svc" "$project_path")
|
|
2229
|
+
case "$state" in
|
|
2230
|
+
enabled) printf " ${GREEN}%-8s ● enabled${NC} (%s)\n" "$svc" "$schedule" ;;
|
|
2231
|
+
installed-off) printf " ${YELLOW}%-8s ⚠ installed/off${NC} (%s) run: roll loop on\n" "$svc" "$schedule" ;;
|
|
2232
|
+
not-installed) printf " ${RED}%-8s ○ not installed${NC} (%s) run: roll setup\n" "$svc" "$schedule" ;;
|
|
2233
|
+
esac
|
|
2107
2234
|
done
|
|
2108
2235
|
else
|
|
2109
2236
|
if crontab -l 2>/dev/null | grep -q "${_LOOP_TAG}:${project_path}"; then
|
|
@@ -2248,20 +2375,23 @@ cmd_backlog() {
|
|
|
2248
2375
|
return 1
|
|
2249
2376
|
fi
|
|
2250
2377
|
|
|
2251
|
-
local us_items fix_items refactor_items total=0
|
|
2378
|
+
local us_items fix_items refactor_items idea_items total=0
|
|
2252
2379
|
|
|
2253
2380
|
us_items=$(grep -E '^\| \[US-' "$backlog" | grep -F '| 📋 Todo |' || true)
|
|
2254
2381
|
fix_items=$(grep -E '^\| FIX-' "$backlog" | grep -F '| 📋 Todo |' || true)
|
|
2255
2382
|
refactor_items=$(grep -E '^\| REFACTOR-' "$backlog" | grep -F '| 📋 Todo |' || true)
|
|
2383
|
+
idea_items=$(grep -E '^\| IDEA-' "$backlog" | grep -F '| 📋 Todo |' || true)
|
|
2256
2384
|
|
|
2257
|
-
local us_count fix_count refactor_count
|
|
2385
|
+
local us_count fix_count refactor_count idea_count
|
|
2258
2386
|
us_count=$(echo "$us_items" | grep -c . || true)
|
|
2259
2387
|
fix_count=$(echo "$fix_items" | grep -c . || true)
|
|
2260
2388
|
refactor_count=$(echo "$refactor_items" | grep -c . || true)
|
|
2389
|
+
idea_count=$(echo "$idea_items" | grep -c . || true)
|
|
2261
2390
|
[[ -z "$us_items" ]] && us_count=0
|
|
2262
2391
|
[[ -z "$fix_items" ]] && fix_count=0
|
|
2263
2392
|
[[ -z "$refactor_items" ]] && refactor_count=0
|
|
2264
|
-
|
|
2393
|
+
[[ -z "$idea_items" ]] && idea_count=0
|
|
2394
|
+
total=$(( us_count + fix_count + refactor_count + idea_count ))
|
|
2265
2395
|
|
|
2266
2396
|
echo ""
|
|
2267
2397
|
echo -e " ${BOLD}Pending Backlog 待处理任务${NC} (${total} items)"
|
|
@@ -2300,6 +2430,17 @@ cmd_backlog() {
|
|
|
2300
2430
|
echo ""
|
|
2301
2431
|
fi
|
|
2302
2432
|
|
|
2433
|
+
if [[ $idea_count -gt 0 ]]; then
|
|
2434
|
+
echo -e " ${NC}Ideas 创意 (${idea_count})"
|
|
2435
|
+
while IFS= read -r line; do
|
|
2436
|
+
local id desc
|
|
2437
|
+
id=$(echo "$line" | awk -F'|' '{print $2}' | tr -d ' ')
|
|
2438
|
+
desc=$(echo "$line" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//')
|
|
2439
|
+
printf " %-14s %s\n" "$id" "$desc"
|
|
2440
|
+
done <<< "$idea_items"
|
|
2441
|
+
echo ""
|
|
2442
|
+
fi
|
|
2443
|
+
|
|
2303
2444
|
if [[ $total -eq 0 ]]; then
|
|
2304
2445
|
echo -e " ${GREEN}✓ Nothing pending — backlog is clear 暂无待处理任务${NC}"
|
|
2305
2446
|
echo ""
|
|
@@ -2324,13 +2465,28 @@ _dashboard() {
|
|
|
2324
2465
|
if $_dash_loop_enabled; then
|
|
2325
2466
|
echo -e " Loop ${GREEN}● on${NC} Agent: ${CYAN}${agent}${NC}"
|
|
2326
2467
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2468
|
+
local active_start active_end loop_minute dream_hour dream_minute brief_hour brief_minute
|
|
2469
|
+
active_start=$(_config_read_int "loop_active_start" "10")
|
|
2470
|
+
active_end=$(_config_read_int "loop_active_end" "18")
|
|
2471
|
+
loop_minute=$(_config_read_int "loop_minute" "$(_loop_derive_minute "$project_path" 0)")
|
|
2472
|
+
dream_hour=$(_config_read_int "loop_dream_hour" "3")
|
|
2473
|
+
dream_minute=$(_config_read_int "loop_dream_minute" "$(_loop_derive_minute "$project_path" 2)")
|
|
2474
|
+
brief_hour=$(_config_read_int "loop_brief_hour" "9")
|
|
2475
|
+
brief_minute=$(_config_read_int "loop_brief_minute" "$(_loop_derive_minute "$project_path" 4)")
|
|
2476
|
+
local loop_sched dream_sched brief_sched
|
|
2477
|
+
loop_sched=$(printf "every hour :%02d active %02d:00–%02d:00" "$loop_minute" "$active_start" "$active_end")
|
|
2478
|
+
dream_sched=$(printf "%02d:%02d" "$dream_hour" "$dream_minute")
|
|
2479
|
+
brief_sched=$(printf "%02d:%02d" "$brief_hour" "$brief_minute")
|
|
2480
|
+
local svcs=("loop" "dream" "brief")
|
|
2481
|
+
local scheds=("$loop_sched" "$dream_sched" "$brief_sched")
|
|
2482
|
+
for i in "${!svcs[@]}"; do
|
|
2483
|
+
local svc="${svcs[$i]}" schedule="${scheds[$i]}"
|
|
2484
|
+
local state; state=$(_launchd_svc_state "$svc" "$project_path")
|
|
2485
|
+
case "$state" in
|
|
2486
|
+
enabled) printf " ${GREEN}%-8s ● enabled${NC} (%s)\n" "$svc" "$schedule" ;;
|
|
2487
|
+
installed-off) printf " ${YELLOW}%-8s ⚠ off${NC} (%s) run: roll loop on\n" "$svc" "$schedule" ;;
|
|
2488
|
+
not-installed) printf " ${RED}%-8s ○ missing${NC} (%s) run: roll setup\n" "$svc" "$schedule" ;;
|
|
2489
|
+
esac
|
|
2334
2490
|
done
|
|
2335
2491
|
fi
|
|
2336
2492
|
else
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seanyao/roll",
|
|
3
|
-
"version": "2026.511.
|
|
3
|
+
"version": "2026.511.6",
|
|
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"
|
|
@@ -483,7 +483,7 @@ Both locations must be updated — neither can be skipped:
|
|
|
483
483
|
| [US-{ID}](docs/features/<feature>.md#us-{id}) | {Title} | ✅ Done |
|
|
484
484
|
```
|
|
485
485
|
|
|
486
|
-
Change the Status from `📋 Todo` to `✅ Done`.
|
|
486
|
+
Change the Status from `📋 Todo` or `🔨 In Progress` (whichever the row currently shows) to `✅ Done`. When invoked by `roll-loop`, the row will already be `🔨 In Progress` — that is the expected starting state, and the transition is the same Edit operation.
|
|
487
487
|
For Fly mode: first append an index row under the appropriate Epic > Feature group, then mark it done.
|
|
488
488
|
|
|
489
489
|
**② Update `docs/features/<feature>.md` US section:**
|
|
@@ -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
|
|
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
|
|
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
|
|
package/skills/roll-fix/SKILL.md
CHANGED
|
@@ -296,7 +296,7 @@ Both locations must be updated — neither can be skipped:
|
|
|
296
296
|
| [FIX-{ID}](docs/features/<feature>.md#fix-{id}) | {Title} | ✅ Done |
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
-
Change the Status of the corresponding row from `📋 Todo` to `✅ Done`.
|
|
299
|
+
Change the Status of the corresponding row from `📋 Todo` or `🔨 In Progress` (whichever the row currently shows) to `✅ Done`. When invoked by `roll-loop`, the row will already be `🔨 In Progress` — that is the expected starting state.
|
|
300
300
|
|
|
301
301
|
**② Update `docs/features/<feature>.md` FIX section:**
|
|
302
302
|
|
|
@@ -57,17 +57,56 @@ if [ -f "$STATE_FILE" ] && grep -q "status: interrupted" "$STATE_FILE"; then
|
|
|
57
57
|
fi
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
**Orphan 🔨 recovery** — clean up stories left in `🔨 In Progress` by a crashed previous run:
|
|
61
|
+
|
|
62
|
+
1. Scan BACKLOG.md for all rows whose Status column contains `🔨 In Progress`.
|
|
63
|
+
2. For each such story, check `state.yaml`:
|
|
64
|
+
- If `current_item` matches the story id AND `status: running` → this is the resume case (handled above), leave it.
|
|
65
|
+
- Otherwise → this is an **orphan 🔨** (the loop that marked it crashed before finishing). Revert the status back to `📋 Todo`, commit `chore: revert orphan 🔨 US-XXX to 📋`, and append a line to `~/.shared/roll/loop/ALERT.md` recording the orphan id and time so the next brief surfaces it.
|
|
66
|
+
3. After orphan sweep, proceed to Step 2 (Scan BACKLOG) — the reverted stories will be picked up normally if they're top of the queue.
|
|
67
|
+
|
|
60
68
|
### Step 2 — Scan BACKLOG
|
|
61
69
|
|
|
62
70
|
Read `BACKLOG.md`. Collect all rows where Status = `📋 Todo`, in order:
|
|
63
71
|
|
|
64
72
|
Priority: FIX-XXX first (bugs block progress), then US-XXX, then REFACTOR-XXX.
|
|
65
73
|
|
|
74
|
+
**Skip rows with Status = `🔨 In Progress`**. These are currently being executed by:
|
|
75
|
+
- Another concurrent executor (human via `$roll-build`, peer agent)
|
|
76
|
+
- An earlier loop iteration that hasn't finished yet (rare; should be guarded by LOCK)
|
|
77
|
+
- A previous interrupted run (the resume logic in Step 1 will pick these up)
|
|
78
|
+
|
|
66
79
|
Cap at `max_items_per_run` to limit blast radius per cycle.
|
|
67
80
|
|
|
81
|
+
### Concurrency Safety
|
|
82
|
+
|
|
83
|
+
Loop has two layers of concurrency protection:
|
|
84
|
+
|
|
85
|
+
1. **Per-project LOCK** (enforced by runner script, see `bin/roll:_write_loop_runner_script`):
|
|
86
|
+
- LOCK file path: `~/.shared/roll/loop/.LOCK-<project-slug>`
|
|
87
|
+
- On launch: if LOCK exists and the PID inside is alive → exit 0 (previous loop still running)
|
|
88
|
+
- On launch: if LOCK exists but PID is dead → clean up stale LOCK and continue
|
|
89
|
+
- On exit (normal or via trap): LOCK is removed
|
|
90
|
+
- One LOCK per project — different projects' loops run independently
|
|
91
|
+
|
|
92
|
+
2. **🔨 In Progress story status** (enforced here):
|
|
93
|
+
- Before picking a story, check its status is `📋 Todo`
|
|
94
|
+
- Skip any `🔨 In Progress` row (someone else is on it)
|
|
95
|
+
- Mark each story `🔨 In Progress` BEFORE invoking the executor skill (see Step 3)
|
|
96
|
+
- On completion: update to `✅ Done`; on TCR failure: revert to `📋 Todo`
|
|
97
|
+
|
|
98
|
+
Together these mean: only one loop runs at a time per project (LOCK), and within a loop, stories already claimed by humans or peer agents are skipped (status check).
|
|
99
|
+
|
|
68
100
|
### Step 3 — Route and Execute
|
|
69
101
|
|
|
70
|
-
For each item:
|
|
102
|
+
For each item, **before invoking the executor skill**, mark the story 🔨 In Progress in BACKLOG.md so brief and peer agents can see it's being worked on:
|
|
103
|
+
|
|
104
|
+
1. Edit BACKLOG.md: change the row's Status column from `📋 Todo` to `🔨 In Progress`.
|
|
105
|
+
2. Commit: `git commit -am "chore: mark US-XXX in progress"` (use the actual story id).
|
|
106
|
+
|
|
107
|
+
This commit is what makes the work visible — without it, tcr micro-commits during execution are invisible to `roll-brief`.
|
|
108
|
+
|
|
109
|
+
Then invoke the executor:
|
|
71
110
|
|
|
72
111
|
```
|
|
73
112
|
Item type → Skill invoked
|
|
@@ -77,7 +116,9 @@ FIX-XXX → Skill("roll-fix", "FIX-XXX")
|
|
|
77
116
|
REFACTOR-XXX → Skill("roll-build", "REFACTOR-XXX")
|
|
78
117
|
```
|
|
79
118
|
|
|
80
|
-
|
|
119
|
+
The executor will update the row to `✅ Done` on success (it transitions from `🔨 In Progress` → `✅ Done`, same Edit logic as from `📋 Todo`).
|
|
120
|
+
|
|
121
|
+
Before invoking, also write current item to state file:
|
|
81
122
|
|
|
82
123
|
```yaml
|
|
83
124
|
# ~/.shared/roll/loop/state.yaml
|
|
@@ -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
|
+
```
|