@seanyao/roll 2026.519.2 → 2026.519.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,11 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## v2026.519.3
4
+
5
+ ### Major
6
+
7
+ - **`.roll/` 拆为独立私有仓库** — 过程文件(backlog / brief / features / dream / domain / briefs)整体迁入嵌套私有 repo `roll-meta`,主仓只留产品文档。AGENTS.md 新增 §9 说明双 repo 协作约定 `[meta]`
8
+
9
+ ### Improved
10
+
11
+ - **`.roll/features/` 按 Epic 分组** — features 目录由扁平改为 Epic 子目录,附带 loop 架构经验文档 `[docs]`
12
+ - **站点登陆页对齐 v2.0 架构** — `docs/site/` 文案与 2.0 实际产物对齐,去掉过期描述 `[docs]`
13
+
14
+ ### Fixed
15
+
16
+ - **loop 异常退出** — dashboard 不再卡在"运行中",崩溃 / 被 kill / 超时退出都会写下结束标记 `[loop]`
17
+ - **`FIX-065` loop 共享状态隔离** — 测试运行的 loop 不再污染生产 backlog/brief,sandbox 化共享路径 `[loop]`
18
+
3
19
  ## v2026.519.2
4
20
 
5
21
  ### Improved
6
22
 
7
23
  - **`roll init`** — 初始化流程现在显示 6 步编号进度,新建文件用绿色 `+`、合并已有用琥珀 `~`,结尾给三步上手指南 `[loop]`
8
24
 
25
+ ### Fixed
26
+
27
+ - **`roll --help` 中 `init` 描述** — 文案从 `+ docs/` 改为 `+ .roll/features/`,对齐 2.0 实际产物和 README;v2 (`lib/roll-help.py`) 与 legacy (`bin/roll`) 两份 help 同步修正 `[FIX-064]`
28
+
9
29
  ## v2026.519.1
10
30
 
11
31
  ### Major(大版本重构)
package/README.md CHANGED
@@ -37,9 +37,6 @@ Roll 2.0 introduces a **process/product split** to keep open-source projects cle
37
37
  - **`$roll-onboard`** — interactive skill for adopting Roll on legacy codebases without rewrites. AI reads your code, asks 9 questions in ≤ 3 minutes, produces a plan; `roll init --apply` executes it.
38
38
  - **Three adoption patterns** — `seed` (new project), `graft` (legacy, non-invasive), `replant` (legacy, clean rebuild). See [guide/en/patterns/](guide/en/patterns/).
39
39
 
40
- 📖 Upgrading from 1.x? Read [guide/en/migration-2.0.md](guide/en/migration-2.0.md).
41
- 📖 New legacy project? Read [guide/en/legacy-onboarding.md](guide/en/legacy-onboarding.md).
42
-
43
40
  ## Evolution
44
41
 
45
42
  Roll didn't start as a framework. It started as a question: *what if the AI didn't just write code, but actually shipped it?*
@@ -66,6 +63,20 @@ roll loop on # optional: let the agent work unattended
66
63
 
67
64
  ---
68
65
 
66
+ ## Adoption Paths
67
+
68
+ Three ways to bring Roll into a project. Not sure which fits? Just run `roll init` — it detects legacy code and routes you to `$roll-onboard` automatically.
69
+
70
+ | Path | When to use | How to start |
71
+ |------|-------------|--------------|
72
+ | **Seed** | Brand-new project, starting from zero | `roll init` (the Quick Start path above) |
73
+ | **Graft** | Existing codebase, keep current workflow intact | `roll init` → AI prompts `$roll-onboard` → 9 questions in ≤ 3 min → `roll init --apply` |
74
+ | **Replant** | Existing codebase, ready to realign on Roll conventions | Same as Graft, choose "clean rebuild" during onboarding |
75
+
76
+ Details: [seed](guide/en/patterns/seed-pattern.md) · [graft](guide/en/patterns/graft-pattern.md) · [replant](guide/en/patterns/replant-pattern.md)
77
+
78
+ ---
79
+
69
80
  ## Documentation Index
70
81
 
71
82
  | Topic | English | 中文 |
@@ -78,7 +89,7 @@ roll loop on # optional: let the agent work unattended
78
89
  | Configuration (env vars) | [guide/en/configuration.md](guide/en/configuration.md) | [guide/zh/configuration.md](guide/zh/configuration.md) |
79
90
  | Skill selection guide | [guide/en/skills.md](guide/en/skills.md) | [guide/zh/skills.md](guide/zh/skills.md) |
80
91
  | FAQ (troubleshooting) | [guide/en/faq.md](guide/en/faq.md) | [guide/zh/faq.md](guide/zh/faq.md) |
81
- | Domain model (DDD) | [domain/context-map.md](.roll/domain/context-map.md) | — |
92
+ | Adoption patterns | [guide/en/patterns/](guide/en/patterns/) | [guide/zh/patterns/](guide/zh/patterns/) |
82
93
  | Engineering common sense | [practices/engineering-common-sense.md](guide/en/practices/engineering-common-sense.md) | — |
83
94
 
84
95
  ---
@@ -87,15 +98,23 @@ roll loop on # optional: let the agent work unattended
87
98
 
88
99
  | Command | Description |
89
100
  |---------|-------------|
101
+ | **Autonomy · daily use** | |
102
+ | `roll loop <on\|off\|now\|status\|monitor>` | 🤖 Manage the autonomous BACKLOG executor |
103
+ | `roll brief` | 🤖 Show latest owner brief |
104
+ | `roll backlog [block\|defer\|…]` | View and manage pending tasks |
105
+ | `roll peer` | 🤖 Cross-agent negotiation & review |
106
+ | `roll alert` | View / clear loop alerts |
107
+ | **Project · per repo** | |
108
+ | `roll init` | Create AGENTS.md + .roll/backlog.md + .roll/features/ |
109
+ | `roll status` | Show current state and drift |
110
+ | `roll agent [use <name>]` | Per-project agent selection (Claude / Cursor / Codex / Kimi / …) |
111
+ | `roll ci [--wait]` | Show or wait for current commit's CI status |
112
+ | `roll release` | 🤖 Run the release script (human-only) |
113
+ | `roll review-pr <number>` | 🤖 AI-powered code review for a PR |
114
+ | **Machine · global** | |
90
115
  | `roll setup [-f]` | First-time install or re-sync conventions to all AI clients |
91
- | `roll update` | Upgrade to latest version |
92
- | `roll init` | Initialize project: AGENTS.md + .roll/backlog.md + .roll/features/ |
93
- | `roll status` | Show sync state, skill links, detected AI tools |
94
- | `roll backlog` | Show pending tasks from .roll/backlog.md |
95
- | `roll loop <on\|off\|now\|status\|monitor>` | 🤖 Manage autonomous executor |
96
- | `roll brief` | 🤖 Show latest owner digest |
97
- | `roll peer` | 🤖 Cross-agent code review |
98
- | `roll release` | 🤖 Version + tag + npm publish + GitHub Release |
116
+ | `roll update` | Upgrade to latest + re-sync |
117
+ | `roll version` | Print installed roll version |
99
118
 
100
119
  ---
101
120
 
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.519.2"
7
+ VERSION="2026.519.3"
8
8
  ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
9
9
  ROLL_CONFIG="${ROLL_HOME}/config.yaml"
10
10
  ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
@@ -2343,7 +2343,13 @@ cmd_review_pr() {
2343
2343
  local output
2344
2344
  info "Reviewing PR #${pr_number} with ${agent}..."
2345
2345
  _agent_argv "$agent" text "$prompt" || { err "Unknown agent '${agent}'"; return 1; }
2346
- output=$("${_AGENT_ARGV[@]}" 2>/dev/null)
2346
+ local _stderr_log; _stderr_log=$(mktemp)
2347
+ output=$("${_AGENT_ARGV[@]}" 2>"$_stderr_log")
2348
+ if [[ -z "$output" && -s "$_stderr_log" ]]; then
2349
+ err "agent ${agent} produced no output. stderr (first 5 lines):"
2350
+ head -5 "$_stderr_log" | sed 's/^/ /' >&2
2351
+ fi
2352
+ rm -f "$_stderr_log"
2347
2353
 
2348
2354
  echo "$output"
2349
2355
 
@@ -2545,6 +2551,35 @@ PYEOF
2545
2551
  }
2546
2552
 
2547
2553
  _LOOP_TAG="# roll-loop"
2554
+ # FIX-065: when sourced in a test context with no explicit override, route
2555
+ # shared state into a per-process /tmp path instead of falling back to
2556
+ # production ~/.shared/roll/. Without this safety net, tests that source
2557
+ # bin/roll (directly or via a generated inner runner under /var/folders/)
2558
+ # would write ALERT / state / events / runs.jsonl into the live loop
2559
+ # daemon's monitored directory and trigger false aborts.
2560
+ #
2561
+ # Test context is detected via three signals (any one is enough):
2562
+ # 1. BATS_TEST_FILENAME is set (works for direct test invocations)
2563
+ # 2. The caller's file path lives under /tmp or /var/folders (catches the
2564
+ # generated runner-inner.sh path that bats subprocesses spawn —
2565
+ # BATS_* env can be lost across `bash -l` + nested forks)
2566
+ # 3. PWD is under /tmp or /var/folders (catches helpers that cd'd in)
2567
+ if [ -z "${_SHARED_ROOT:-}" ]; then
2568
+ _roll_in_test_ctx=0
2569
+ if [ -n "${BATS_TEST_FILENAME:-}" ]; then
2570
+ _roll_in_test_ctx=1
2571
+ else
2572
+ _roll_caller="${BASH_SOURCE[1]:-}"
2573
+ case "$_roll_caller" in /tmp/*|/private/tmp/*|/var/folders/*) _roll_in_test_ctx=1 ;; esac
2574
+ case "$PWD" in /tmp/*|/private/tmp/*|/var/folders/*) _roll_in_test_ctx=1 ;; esac
2575
+ fi
2576
+ if [ "$_roll_in_test_ctx" = 1 ]; then
2577
+ _SHARED_ROOT="${TMPDIR:-/tmp}/roll-test-shared.$$"
2578
+ mkdir -p "${_SHARED_ROOT}/loop"
2579
+ export _SHARED_ROOT
2580
+ fi
2581
+ unset _roll_in_test_ctx _roll_caller
2582
+ fi
2548
2583
  : "${_SHARED_ROOT:=${HOME}/.shared/roll}"
2549
2584
  # FIX-052: per-project loop state — ALERT/state/mute were globally shared,
2550
2585
  # causing one project's alerts to surface in another project's session and
@@ -2553,7 +2588,9 @@ _LOOP_TAG="# roll-loop"
2553
2588
  : "${_LOOP_PROJ_SLUG:=$(_project_slug 2>/dev/null || echo default)}"
2554
2589
  : "${_LOOP_STATE:=${_SHARED_ROOT}/loop/state-${_LOOP_PROJ_SLUG}.yaml}"
2555
2590
  : "${_LOOP_ALERT:=${_SHARED_ROOT}/loop/ALERT-${_LOOP_PROJ_SLUG}.md}"
2556
- _LOOP_RUNS="${HOME}/.shared/roll/loop/runs.jsonl"
2591
+ # FIX-065: was hardcoded to ${HOME}/.shared/roll/loop/runs.jsonl which ignored
2592
+ # _SHARED_ROOT overrides and silently leaked test runs.jsonl writes into prod.
2593
+ _LOOP_RUNS="${_SHARED_ROOT}/loop/runs.jsonl"
2557
2594
  : "${_LOOP_MUTE_FILE:=${_SHARED_ROOT}/loop/mute-${_LOOP_PROJ_SLUG}}"
2558
2595
  _LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
2559
2596
 
@@ -2564,6 +2601,13 @@ _config_read_int() {
2564
2601
  if [[ "$val" =~ ^[0-9]+$ ]]; then echo "$val"; else echo "$default"; fi
2565
2602
  }
2566
2603
 
2604
+ # REFACTOR-031: cross-platform file mtime in epoch seconds.
2605
+ # Replaces the `stat -c %Y ... || stat -f %m ... || echo 0` pattern that was
2606
+ # copy-pasted in four places (dashboard age widgets, briefs, dream, peer).
2607
+ _file_mtime() {
2608
+ stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0
2609
+ }
2610
+
2567
2611
  # Derive a minute in [1,55] from project path hash + offset so different projects
2568
2612
  # and different services within a project don't fire at the same time.
2569
2613
  # Offsets used: loop=0, dream=2, brief=4 → always three distinct values (2<55).
@@ -2589,6 +2633,24 @@ _loop_event() {
2589
2633
  ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
2590
2634
  slug=$(_project_slug 2>/dev/null || basename "$PWD")
2591
2635
  evfile="${_SHARED_ROOT:-$HOME/.shared/roll}/loop/events-${slug}.ndjson"
2636
+ # FIX-065 tripwire: in a test context (BATS or temp cwd), refuse to write
2637
+ # into production ~/.shared/roll/. Catching this in code is the last line
2638
+ # of defense if some unusual path bypassed the auto-sandbox at source-time.
2639
+ # Skipped when HOME itself has been redirected to a sandbox dir — then
2640
+ # $HOME/.shared/roll IS the sandbox, not prod.
2641
+ case "${HOME:-}" in
2642
+ /tmp/*|/private/tmp/*|*/var/folders/*|*/tmp.*) ;;
2643
+ *)
2644
+ if [ -n "${HOME:-}" ] && [ "${evfile#${HOME}/.shared/roll/}" != "$evfile" ]; then
2645
+ case "${BATS_TEST_FILENAME:-}${PWD}" in
2646
+ */tmp.*|*/var/folders/*|/tmp/*|/private/tmp/*|*.bats)
2647
+ echo "[FIX-065] refusing prod _loop_event write: $evfile (test context)" >&2
2648
+ return 1
2649
+ ;;
2650
+ esac
2651
+ fi
2652
+ ;;
2653
+ esac
2592
2654
  mkdir -p "$(dirname "$evfile")"
2593
2655
 
2594
2656
  # stdout: tab-separated for tmux display
@@ -2779,7 +2841,7 @@ fi
2779
2841
  printf '%s:%s\n' "\$\$" "\$(date -u +%s)" > "\$INNER_LOCK"
2780
2842
  # FIX-038: background heartbeat writer — outer script uses this as primary liveness signal
2781
2843
  # to detect stale execution without relying on PID reuse heuristics.
2782
- HEARTBEAT_FILE="${HOME}/.shared/roll/loop/.heartbeat-${slug}"
2844
+ HEARTBEAT_FILE="\${_SHARED_ROOT:-\${HOME}/.shared/roll}/loop/.heartbeat-${slug}"
2783
2845
  _heartbeat_writer() {
2784
2846
  while true; do echo "\$(date -u +%s)" > "\$HEARTBEAT_FILE"; sleep 60; done
2785
2847
  }
@@ -2791,6 +2853,11 @@ _HEARTBEAT_PID=\$!
2791
2853
  # next cron tick can proceed. Overridable via env for tests.
2792
2854
  LOOP_CYCLE_TIMEOUT_SEC="\${ROLL_LOOP_CYCLE_TIMEOUT_SEC:-2700}"
2793
2855
  _CYCLE_TIMED_OUT=0
2856
+ # IDEA-028 / FIX-066: track whether cycle_end has been emitted via any of the
2857
+ # explicit completion paths (publish/merge_back/orphan-push/claude-failed).
2858
+ # When zero, the EXIT trap emits a fallback so cycle_start never orphans the
2859
+ # dashboard into a phantom "still running" row.
2860
+ _CYCLE_END_WRITTEN=0
2794
2861
  _on_sigterm() { _CYCLE_TIMED_OUT=1; }
2795
2862
  trap '_on_sigterm' TERM
2796
2863
  # US-LOOP-005: idempotent runs.jsonl writer shared by normal exit, timeout
@@ -2798,7 +2865,7 @@ trap '_on_sigterm' TERM
2798
2865
  # multiple callers in the same cycle are safe.
2799
2866
  _runs_append() {
2800
2867
  local _status="\$1"; local _tcr="\${2:-0}"; local _built="\${3:-[]}"
2801
- local _runs_dst="${HOME}/.shared/roll/loop/runs.jsonl"
2868
+ local _runs_dst="\${_SHARED_ROOT:-\${HOME}/.shared/roll}/loop/runs.jsonl"
2802
2869
  command -v jq >/dev/null 2>&1 || return 0
2803
2870
  local _cid="\${CYCLE_ID:-pre-cycle-\$\$}"
2804
2871
  local _rid="loop-\${_cid%-*}"
@@ -2834,12 +2901,21 @@ _inner_cleanup() {
2834
2901
  for _pid in \$(jobs -p); do kill "\$_pid" 2>/dev/null; done
2835
2902
  if [ "\${_CYCLE_TIMED_OUT:-0}" -eq 1 ]; then
2836
2903
  _loop_event cycle_end "\${CYCLE_ID:-unknown}" "\${BRANCH:-}" "blocked" 2>/dev/null || true
2904
+ _CYCLE_END_WRITTEN=1
2837
2905
  # US-LOOP-005 T9: timeout path must also write runs.jsonl row so dashboard
2838
2906
  # has a terminal record (cycle_end alone is insufficient — runs.jsonl is
2839
2907
  # the canonical history feed for 'roll loop runs').
2840
2908
  _runs_append "failed" 0 "[]" 2>/dev/null || true
2841
2909
  _worktree_alert "cycle \${CYCLE_ID:-unknown}: \${LOOP_CYCLE_TIMEOUT_SEC}s timeout — claude/python killed; in-progress story marked Blocked" 2>/dev/null || true
2842
2910
  fi
2911
+ # IDEA-028 / FIX-066: catch every other abort (SIGKILL, set -e fire, ALERT
2912
+ # poisoning that bypasses the retry budget, etc.). Without this, cycle_start
2913
+ # is emitted but cycle_end never is, and dashboard renders the cycle as
2914
+ # "still running" until the next successful cycle rolls past it.
2915
+ if [ "\${_CYCLE_END_WRITTEN:-0}" -eq 0 ] && [ -n "\${CYCLE_ID:-}" ]; then
2916
+ _loop_event cycle_end "\${CYCLE_ID}" "\${BRANCH:-}" "aborted" 2>/dev/null || true
2917
+ _runs_append "aborted" 0 "[]" 2>/dev/null || true
2918
+ fi
2843
2919
  rm -f "\$INNER_LOCK" "\$HEARTBEAT_FILE"
2844
2920
  exit "\$_rc"
2845
2921
  }
@@ -3023,13 +3099,13 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
3023
3099
  fi
3024
3100
  fi
3025
3101
  _worktree_cleanup "\$WT" "\$BRANCH"
3026
- _loop_event cycle_end "\${CYCLE_ID}" "" "done" || true
3102
+ _loop_event cycle_end "\${CYCLE_ID}" "" "done" || true; _CYCLE_END_WRITTEN=1
3027
3103
  echo "[loop] cycle \${CYCLE_ID}: published; worktree cleaned"
3028
3104
  elif [ "\$_publish_status" -eq 2 ]; then
3029
3105
  if ( cd "${project_path}" && _worktree_merge_back "\$BRANCH" ); then
3030
3106
  _worktree_cleanup "\$WT" "\$BRANCH"
3031
3107
  # US-LOOP-005 T3: gh unavailable + ff merge_back OK → cycle_end done
3032
- _loop_event cycle_end "\${CYCLE_ID}" "" "done" || true
3108
+ _loop_event cycle_end "\${CYCLE_ID}" "" "done" || true; _CYCLE_END_WRITTEN=1
3033
3109
  echo "[loop] cycle \${CYCLE_ID}: gh unavailable; merged via ff and cleaned up"
3034
3110
  else
3035
3111
  # FIX-039: gh unavailable + merge_back failed — push orphan branch+tag to origin
@@ -3040,12 +3116,12 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
3040
3116
  && git push origin "\$_orphan_tag" 2>/dev/null ); then
3041
3117
  _worktree_cleanup "\$WT" "\$BRANCH"
3042
3118
  # US-LOOP-005 T4: gh unavailable + orphan push OK → cycle_end orphan
3043
- _loop_event cycle_end "\${CYCLE_ID}" "" "orphan" || true
3119
+ _loop_event cycle_end "\${CYCLE_ID}" "" "orphan" || true; _CYCLE_END_WRITTEN=1
3044
3120
  _worktree_alert "cycle \${CYCLE_ID}: gh+merge_back failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
3045
3121
  echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
3046
3122
  else
3047
3123
  # US-LOOP-005 T5: gh unavailable + all failed → cycle_end failed
3048
- _loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true
3124
+ _loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true; _CYCLE_END_WRITTEN=1
3049
3125
  _worktree_alert "cycle \${CYCLE_ID}: gh+merge_back+push all failed; worktree preserved at \$WT"
3050
3126
  echo "[loop] cycle \${CYCLE_ID}: all publish paths failed; worktree preserved at \$WT"
3051
3127
  fi
@@ -3059,12 +3135,12 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
3059
3135
  && git push origin "\$_orphan_tag" 2>/dev/null ); then
3060
3136
  _worktree_cleanup "\$WT" "\$BRANCH"
3061
3137
  # US-LOOP-005 T6: PR publish failed + orphan push OK → cycle_end orphan
3062
- _loop_event cycle_end "\${CYCLE_ID}" "" "orphan" || true
3138
+ _loop_event cycle_end "\${CYCLE_ID}" "" "orphan" || true; _CYCLE_END_WRITTEN=1
3063
3139
  _worktree_alert "cycle \${CYCLE_ID}: PR publish failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
3064
3140
  echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
3065
3141
  else
3066
3142
  # US-LOOP-005 T7: PR publish failed + orphan push failed → cycle_end failed
3067
- _loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true
3143
+ _loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true; _CYCLE_END_WRITTEN=1
3068
3144
  _worktree_alert "cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT (branch \$BRANCH)"
3069
3145
  echo "[loop] cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT"
3070
3146
  fi
@@ -3072,7 +3148,7 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
3072
3148
  fi
3073
3149
  else
3074
3150
  # US-LOOP-005 T8: claude session failed after retry budget → cycle_end failed
3075
- _loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true
3151
+ _loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true; _CYCLE_END_WRITTEN=1
3076
3152
  _worktree_alert "cycle \${CYCLE_ID}: claude exited \$_exit; worktree preserved at \$WT (branch \$BRANCH)"
3077
3153
  echo "[loop] cycle \${CYCLE_ID}: claude failed (exit \$_exit); worktree preserved at \$WT"
3078
3154
  fi
@@ -3117,13 +3193,13 @@ if [ -z "\$ROLL_LOOP_FORCE" ] && [ -f "\$PAUSE" ]; then exit 0; fi
3117
3193
  HEARTBEAT_TIMEOUT="\${ROLL_HEARTBEAT_TIMEOUT:-1800}"
3118
3194
  # FIX-052: per-project STATE_FILE (was global state.yaml — caused two projects
3119
3195
  # to clobber each other's cycle state).
3120
- STATE_FILE="${HOME}/.shared/roll/loop/state-${slug}.yaml"
3196
+ STATE_FILE="\${_SHARED_ROOT:-\${HOME}/.shared/roll}/loop/state-${slug}.yaml"
3121
3197
  if [ -f "\$STATE_FILE" ]; then
3122
3198
  _state=\$(grep '^status:' "\$STATE_FILE" | awk '{print \$2}' 2>/dev/null || echo "")
3123
3199
  if [ "\$_state" = "running" ]; then
3124
3200
  _still_active=false
3125
3201
  # FIX-038: heartbeat is primary signal
3126
- _heartbeat_file="${HOME}/.shared/roll/loop/.heartbeat-${slug}"
3202
+ _heartbeat_file="\${_SHARED_ROOT:-\${HOME}/.shared/roll}/loop/.heartbeat-${slug}"
3127
3203
  if [ -f "\$_heartbeat_file" ]; then
3128
3204
  _hb_ts=\$(cat "\$_heartbeat_file" 2>/dev/null || echo "0")
3129
3205
  _now=\$(date -u +%s)
@@ -5289,7 +5365,7 @@ cmd_brief() {
5289
5365
  latest=$(ls "${briefs_dir}"/*.md 2>/dev/null | sort | tail -1 || true)
5290
5366
  else
5291
5367
  local mod_time now age
5292
- mod_time=$(stat -c %Y "$latest" 2>/dev/null || stat -f %m "$latest" 2>/dev/null || echo 0)
5368
+ mod_time=$(_file_mtime "$latest")
5293
5369
  now=$(date +%s); age=$(( now - mod_time ))
5294
5370
  if (( age > 86400 )); then
5295
5371
  info "Brief is $(( age / 3600 ))h old — regenerating... 简报已 $(( age / 3600 )) 小时未更新,重新生成..."
@@ -5666,7 +5742,7 @@ _dash_last_dream_hours() {
5666
5742
  local dream_log="${HOME}/.shared/roll/dream/log.md"
5667
5743
  [[ -f "$dream_log" ]] || return 0
5668
5744
  local mod_time now
5669
- mod_time=$(stat -c %Y "$dream_log" 2>/dev/null || stat -f %m "$dream_log" 2>/dev/null || echo 0)
5745
+ mod_time=$(_file_mtime "$dream_log")
5670
5746
  now=$(date +%s)
5671
5747
  echo $(( (now - mod_time) / 3600 ))
5672
5748
  }
@@ -5686,7 +5762,7 @@ _dash_last_peer() {
5686
5762
  local result
5687
5763
  result=$(grep -oE '(AGREE|REFINE|OBJECT|ESCALATE)' "$latest" 2>/dev/null | tail -1 || true)
5688
5764
  local mod_time now days
5689
- mod_time=$(stat -c %Y "$latest" 2>/dev/null || stat -f %m "$latest" 2>/dev/null || echo 0)
5765
+ mod_time=$(_file_mtime "$latest")
5690
5766
  now=$(date +%s)
5691
5767
  days=$(( (now - mod_time) / 86400 ))
5692
5768
  printf '%s|%s' "${result:-—}" "${days}"
@@ -5979,7 +6055,7 @@ _legacy_home() {
5979
6055
  local latest_brief; latest_brief=$(ls .roll/briefs/*.md 2>/dev/null | sort | tail -1 || true)
5980
6056
  if [[ -n "$latest_brief" ]]; then
5981
6057
  local mod_time now age summary
5982
- mod_time=$(stat -c %Y "$latest_brief" 2>/dev/null || stat -f %m "$latest_brief" 2>/dev/null || echo 0)
6058
+ mod_time=$(_file_mtime "$latest_brief")
5983
6059
  now=$(date +%s); age=$(( (now - mod_time) / 3600 ))
5984
6060
  summary=$(_dash_brief_summary "$latest_brief")
5985
6061
  printf " Brief ${CYAN}%sh${NC} ago — %s\n" "$age" "${summary:-—}"
@@ -6016,7 +6092,7 @@ _legacy_help() {
6016
6092
  echo "Commands:"
6017
6093
  echo " setup [-f] [Machine] First-time install or re-sync 首次安装或重新同步"
6018
6094
  echo " update [Upgrade] npm install latest + re-sync 一键升级到最新版"
6019
- echo " init [Project] Create AGENTS.md + .roll/backlog.md + docs/ 初始化项目工作流文件"
6095
+ echo " init [Project] Create AGENTS.md + .roll/backlog.md + .roll/features/ 初始化项目工作流文件"
6020
6096
  echo " status [Diagnostic] Show current state 显示当前状态"
6021
6097
  echo " peer [Peer Review] Cross-agent negotiation 跨 Agent 协商对审"
6022
6098
  echo " loop <on|off|now|status|monitor|resume|reset> [Autonomous] Manage scheduled BACKLOG executor 管理自主执行循环"
@@ -244,7 +244,7 @@ def _write_demo(path: str) -> None:
244
244
  ### Feature: autonomous-evolution
245
245
  | Story | Description | Status |
246
246
  |-------|-------------|--------|
247
- | [US-AUTO-042](.roll/features/autonomous-evolution.md) | Loop cost telemetry — write model and token data per cycle | 📋 Todo |
247
+ | [US-AUTO-042](.roll/features/autonomous-evolution/autonomous-evolution.md) | Loop cost telemetry — write model and token data per cycle | 📋 Todo |
248
248
 
249
249
  ## ♻️ Refactor
250
250
  | ID | Description | Status |
package/lib/roll-help.py CHANGED
@@ -45,7 +45,7 @@ AUTONOMY = [
45
45
  ]
46
46
 
47
47
  PROJECT = [
48
- ("init", "", "create AGENTS.md + .roll/backlog.md + docs/", "初始化项目工作流文件", False),
48
+ ("init", "", "create AGENTS.md + .roll/backlog.md + .roll/features/", "初始化项目工作流文件", False),
49
49
  ("status", "", "show current state and drift", "显示当前状态和漂移项", False),
50
50
  ("agent", "[use <name>]", "per-project agent selection", "切换项目 agent", False),
51
51
  ("ci", "[--wait]", "show or wait for current commit's CI status", "查看 / 等待 CI 状态", False),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "2026.519.2",
3
+ "version": "2026.519.3",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "scripts": {
6
6
  "test": "bash tests/run.sh"
@@ -278,7 +278,7 @@ When any signal appears, **do not stop — flag it**:
278
278
  # 1. Append to .roll/backlog.md under ## ♻️ Refactor
279
279
  # REFACTOR-XXX | <one-line description> | 📋 Todo
280
280
 
281
- # 2. Append a brief entry to .roll/features/refactor-log.md
281
+ # 2. Append a brief entry to .roll/features/autonomous-evolution/refactor-log.md
282
282
  ```
283
283
 
284
284
  **REFACTOR entry format in .roll/backlog.md:**
@@ -287,7 +287,7 @@ When any signal appears, **do not stop — flag it**:
287
287
  | REFACTOR-001 | {one-line plain-language description} | 📋 Todo |
288
288
  ```
289
289
 
290
- 描述写法:参见 AGENTS.md "Backlog descriptions" 规则。说清楚"什么需要改"以及"不改会怎样",技术细节写在 `.roll/features/refactor-log.md`。
290
+ 描述写法:参见 AGENTS.md "Backlog descriptions" 规则。说清楚"什么需要改"以及"不改会怎样",技术细节写在 `.roll/features/autonomous-evolution/refactor-log.md`。
291
291
 
292
292
  **refactor-log.md entry format:**
293
293
 
@@ -208,11 +208,14 @@ For each item, **before invoking the executor skill**, mark the story 🔨 In Pr
208
208
 
209
209
  This commit is what makes the work visible — without it, tcr micro-commits during execution are invisible to `roll-brief`.
210
210
 
211
- 选定故事后,调用 `_loop_event` 发出 story 事件,让 monitor attach 能渲染当前进度:
211
+ 选定故事后,调用 `_loop_event` 发出 pick_todo 事件,让 dashboard / monitor / attach 都能把"这个 cycle 选了哪个 story"正确归类:
212
212
 
213
213
  ```bash
214
214
  # 选定故事后立即 emit(在调用 executor skill 之前)
215
- _loop_event story "$US_ID" "$story_title" ""
215
+ # label 必须是 cycle_id(来自 bin/roll 注入的 LOOP_CYCLE_ID 环境变量),
216
+ # 不是 US_ID — dashboard 按 label 聚类,US_ID 当 label 会让事件分到错的桶
217
+ # 里,cycle 看起来"有 token 没 ID"。
218
+ _loop_event pick_todo "$LOOP_CYCLE_ID" "$US_ID" ""
216
219
  ```
217
220
 
218
221
  Then invoke the executor: