@seanyao/roll 2026.511.6 → 2026.511.7

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## v2026.511.11
4
+ - **Added**: loop 默认 auto-attach 弹窗 — 每次 loop 触发时,runner script 自动通过 osascript 开一个背景 Terminal 窗口 `tmux attach -t roll-loop-<slug>`,看 claude 实时打字干活;弹窗用 `delay 0.3` + 还原前一个 frontmost app 做到不抢焦点,tmux session 结束窗口保留供回看,关掉窗口 loop 仍在 tmux 里继续跑。
5
+ - **Added**: `roll loop mute` / `roll loop unmute` 一键开关 — 不想看弹窗时 `roll loop mute` 创建 `~/.shared/roll/mute` 标记文件即刻静音,`unmute` 删掉它恢复;mute 状态对所有项目生效(一个开关治整机),`roll loop status` 新增 `Auto-attach live | muted` 一行实时显示。
6
+ - **Added**: tmux 升级为 `roll setup` 必装依赖 — 新增 `_ensure_tmux` helper:macOS 自动 `brew install tmux`(无 brew 给手动命令),Linux/其他系统打印对应包管理器安装指引;任何失败路径都返回 0,不阻塞 setup 主流程。
7
+ - **Fixed**: launchd runner 缺 brew PATH 导致 hook 子进程报 `node: command not found` — launchd 默认 PATH 不含 `/opt/homebrew/bin`,claude 通过 `sh -c` 调 SessionEnd hook 时找不到 node;inner runner 模板显式 `export PATH="/opt/homebrew/bin:$PATH"` 让整条 fork 链都能拿到 brew 工具。
8
+ - **Fixed**: runs.jsonl schema 漂移 — 早期 claude 在 status/ts/alerts/project 字段自由发挥(`built` vs `success` vs `noop`、UTC vs `+08:00`、number vs array、全路径 vs slug)。SKILL Step 5 改为"严格契约":ts 强制 UTC Z 后缀、project 用 slug、alerts/built/skipped 永远是数组、status 限定 `built/idle/failed` 三个 enum 无同义词;contract test 锁死关键不变量留在 prompt 里。
9
+
10
+ ## v2026.511.9
11
+ - **Added**: `roll loop runs` 每次 loop 运行的快速可见性 — 单次 loop 结束追加一行 JSON 到 `~/.shared/roll/loop/runs.jsonl`(含 ts/project/run_id/status/built/skipped/alerts/tcr_count/duration_sec),新命令 `roll loop runs [N] [--all]` 倒序显示最近 N 次(默认 10),不必等次日早报就能查到中间 13 次 loop 各干了啥。
12
+ - **Added**: loop 跑在 tmux session + `roll loop attach` 实时观看 — runner script 自动把 claude 包进 detached tmux session `roll-loop-<slug>`,输出同时 pipe 到 `cron.log`;执行 `roll loop attach` 可随时 attach 上去看它打字、写文件、commit,Ctrl-B D 分离后 loop 继续跑;未装 tmux 时自动 fallback 到原 headless 模式,零依赖回退。
13
+ - **Improved**: roll-.dream 日志改为中文输出 — Dream Log 输出模板(概要 / 死代码 / 架构漂移 / 裁剪候选 / 新兴模式 / 创建的 REFACTOR 条目)和"未发现 / 部分完成"等固定文案全部中文化,与 roll-brief 风格对齐,晨间扫一眼不再需要在中英文之间切换语境。
14
+
3
15
  ## v2026.511.8
4
16
  - **Fixed**: 集成测试 launchd ghost 泄漏 — `integration_teardown` 在删除 TEST_TMP 之前,先 `launchctl bootout` 该沙箱里被 `roll loop on` 注册到 user gui domain 的所有 `com.roll.*` 服务,避免删 plist 后 launchd 仍保留指向不存在路径的 ghost 注册。
5
17
 
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.6"
7
+ VERSION="2026.511.7"
8
8
  ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
9
9
  ROLL_CONFIG="${ROLL_HOME}/config.yaml"
10
10
  ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
@@ -531,6 +531,35 @@ _sync_skills() {
531
531
  # COMMAND: setup [--force]
532
532
  # Initialize ~/.roll/ and sync everything to AI tools in one step
533
533
  # ═══════════════════════════════════════════════════════════════════════════════
534
+ # Ensures tmux is available (US-AUTO-026 promoted it from soft to required
535
+ # dependency for visible loop runs). On macOS attempts `brew install tmux`
536
+ # when brew exists; elsewhere prints the install command. Never fails the
537
+ # setup main flow — returns 0 even if install was not possible so the rest
538
+ # of `roll setup` proceeds.
539
+ _ensure_tmux() {
540
+ if command -v tmux >/dev/null 2>&1; then
541
+ return 0
542
+ fi
543
+
544
+ local os; os="$(uname)"
545
+ if [[ "$os" == "Darwin" ]]; then
546
+ if command -v brew >/dev/null 2>&1; then
547
+ info "tmux not found — installing via brew... 未安装 tmux,正在通过 brew 安装..."
548
+ if brew install tmux >/dev/null 2>&1; then
549
+ ok "tmux installed. tmux 已安装。"
550
+ return 0
551
+ fi
552
+ warn "brew install tmux failed — install manually: brew install tmux brew 安装失败,请手动 'brew install tmux'"
553
+ return 0
554
+ fi
555
+ warn "tmux required but brew not available — install manually: brew install tmux 缺少 brew,请手动 'brew install tmux'"
556
+ return 0
557
+ fi
558
+
559
+ warn "tmux required — install via your package manager (e.g. apt install tmux / pacman -S tmux) 请用系统包管理器安装 tmux"
560
+ return 0
561
+ }
562
+
534
563
  cmd_setup() {
535
564
  local force=false
536
565
  while [[ $# -gt 0 ]]; do
@@ -555,6 +584,10 @@ cmd_setup() {
555
584
  _peer_ensure_state_dir
556
585
  ok "Peer state directory ready. Peer 状态目录已就绪。"
557
586
 
587
+ echo ""
588
+ info "Ensuring tmux is installed (required for visible loop runs)... 正在确认 tmux 已安装..."
589
+ _ensure_tmux
590
+
558
591
  if [[ "$(uname)" == "Darwin" ]]; then
559
592
  echo ""
560
593
  info "Installing launchd plists... 正在安装 LaunchAgents..."
@@ -1751,6 +1784,8 @@ _LOOP_TAG="# roll-loop"
1751
1784
  _SHARED_ROOT="${HOME}/.shared/roll"
1752
1785
  _LOOP_STATE="${HOME}/.shared/roll/loop/state.yaml"
1753
1786
  _LOOP_ALERT="${HOME}/.shared/roll/loop/ALERT.md"
1787
+ _LOOP_RUNS="${HOME}/.shared/roll/loop/runs.jsonl"
1788
+ _LOOP_MUTE_FILE="${HOME}/.shared/roll/mute"
1754
1789
  _LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
1755
1790
 
1756
1791
  # Returns a filesystem-safe slug combining the project basename and a 6-char
@@ -1847,26 +1882,60 @@ _write_runner_script() {
1847
1882
 
1848
1883
  # Like _write_runner_script but prepends an active window guard.
1849
1884
  # Silently exits when current hour is outside [active_start, active_end).
1885
+ # When tmux is available, wraps the inner command in a detached tmux session
1886
+ # named `roll-loop-<slug>` so `roll loop attach` can watch in real time.
1887
+ # Falls back to headless execution when tmux is not installed.
1850
1888
  _write_loop_runner_script() {
1851
1889
  local script_path="$1" project_path="$2" cmd="$3" log_path="$4"
1852
1890
  local active_start="${5:-10}" active_end="${6:-18}"
1853
1891
  mkdir -p "$(dirname "$script_path")"
1892
+
1893
+ local inner_path="${script_path%.sh}-inner.sh"
1894
+ cat > "$inner_path" << INNER
1895
+ #!/bin/bash -l
1896
+ export PATH="/opt/homebrew/bin:\$PATH"
1897
+ cd "${project_path}" && ${cmd}
1898
+ INNER
1899
+ chmod +x "$inner_path"
1900
+
1854
1901
  cat > "$script_path" << SCRIPT
1855
1902
  #!/bin/bash -l
1856
1903
  h=\$(printf '%d' "\$(date +%H)")
1857
1904
  if [ "\$h" -lt ${active_start} ] || [ "\$h" -ge ${active_end} ]; then exit 0; fi
1858
1905
  LOCK="\$(dirname "\$0")/.LOCK-\$(basename "\$0" .sh | sed 's/^run-//')"
1906
+ SESSION="roll-loop-\$(basename "\$0" .sh | sed 's/^run-//')"
1907
+ INNER_SCRIPT="${inner_path}"
1908
+ LOG="${log_path}"
1859
1909
  if [ -f "\$LOCK" ]; then
1860
1910
  prev_pid=\$(head -1 "\$LOCK" 2>/dev/null || echo "")
1861
1911
  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}"
1912
+ echo "[\$(date '+%Y-%m-%dT%H:%M:%S%z')] loop already running (PID \$prev_pid), skipping" >> "\$LOG"
1863
1913
  exit 0
1864
1914
  fi
1865
1915
  rm -f "\$LOCK"
1866
1916
  fi
1867
1917
  echo "\$\$" > "\$LOCK"
1868
1918
  trap 'rm -f "\$LOCK"' EXIT
1869
- cd "${project_path}" && ${cmd} >> "${log_path}" 2>&1
1919
+ if command -v tmux >/dev/null 2>&1; then
1920
+ tmux kill-session -t "\$SESSION" 2>/dev/null || true
1921
+ tmux new-session -d -s "\$SESSION" -x 200 -y 50 "bash \"\$INNER_SCRIPT\""
1922
+ tmux pipe-pane -t "\$SESSION" "cat >> \"\$LOG\""
1923
+ # Auto-attach popup: when not muted, spawn a Terminal window attached to the
1924
+ # tmux session so the user can watch the loop work in real time. Best-effort
1925
+ # focus retention: capture the current frontmost app and re-activate after.
1926
+ if [ ! -f "\$HOME/.shared/roll/mute" ] && [ "\$(uname)" = "Darwin" ] && command -v osascript >/dev/null 2>&1; then
1927
+ (osascript \\
1928
+ -e 'tell application "System Events" to set _prev to name of first application process whose frontmost is true' \\
1929
+ -e "tell application \"Terminal\" to do script \"tmux attach -t \$SESSION\"" \\
1930
+ -e 'delay 0.3' \\
1931
+ -e 'try' \\
1932
+ -e 'tell application _prev to activate' \\
1933
+ -e 'end try' >/dev/null 2>&1) &
1934
+ fi
1935
+ while tmux has-session -t "\$SESSION" 2>/dev/null; do sleep 5; done
1936
+ else
1937
+ bash "\$INNER_SCRIPT" >> "\$LOG" 2>&1
1938
+ fi
1870
1939
  SCRIPT
1871
1940
  chmod +x "$script_path"
1872
1941
  }
@@ -1978,9 +2047,13 @@ cmd_loop() {
1978
2047
  now) _loop_now ;;
1979
2048
  status) _loop_status ;;
1980
2049
  monitor) _loop_monitor "${1:-3}" ;;
2050
+ runs) _loop_runs "$@" ;;
2051
+ attach) _loop_attach ;;
2052
+ mute) _loop_mute ;;
2053
+ unmute) _loop_unmute ;;
1981
2054
  resume) _loop_resume ;;
1982
2055
  reset) _loop_reset ;;
1983
- *) err "Usage: roll loop <on|off|now|status|monitor|resume|reset>"; exit 1 ;;
2056
+ *) err "Usage: roll loop <on|off|now|status|monitor|runs|attach|mute|unmute|resume|reset>"; exit 1 ;;
1984
2057
  esac
1985
2058
  }
1986
2059
 
@@ -2118,6 +2191,12 @@ _loop_status() {
2118
2191
  echo -e " Scheduler ${YELLOW}○ disabled${NC} run: roll loop on"
2119
2192
  fi
2120
2193
  fi
2194
+ echo ""
2195
+ if [[ -f "$_LOOP_MUTE_FILE" ]]; then
2196
+ echo -e " Auto-attach ${YELLOW}muted${NC} run: roll loop unmute"
2197
+ else
2198
+ echo -e " Auto-attach ${GREEN}live${NC} run: roll loop mute"
2199
+ fi
2121
2200
  [[ -f "$_LOOP_ALERT" ]] && { echo ""; echo -e " ${RED}⚠ ALERT:${NC}"; sed 's/^/ /' "$_LOOP_ALERT"; }
2122
2201
  [[ -f "$_LOOP_STATE" ]] && { echo ""; echo " State:"; sed 's/^/ /' "$_LOOP_STATE"; }
2123
2202
  echo ""
@@ -2143,6 +2222,138 @@ _loop_reset() {
2143
2222
  fi
2144
2223
  }
2145
2224
 
2225
+ # Suppress the auto-attach popup. When the marker file exists, runner scripts
2226
+ # skip the osascript Terminal-popup step on next fire. Loop output still goes
2227
+ # to tmux + log; users can run `roll loop attach` manually.
2228
+ _loop_mute() {
2229
+ mkdir -p "$(dirname "$_LOOP_MUTE_FILE")"
2230
+ : > "$_LOOP_MUTE_FILE"
2231
+ ok "🔇 muted — auto-attach disabled 已静音,自动弹窗已关闭"
2232
+ }
2233
+
2234
+ # Re-enable the auto-attach popup.
2235
+ _loop_unmute() {
2236
+ rm -f "$_LOOP_MUTE_FILE"
2237
+ ok "🔔 unmuted — auto-attach live 已恢复,自动弹窗已开启"
2238
+ }
2239
+
2240
+ # Attach to the tmux session a running loop iteration writes to. Returns 1 when
2241
+ # tmux is missing or no session exists for the current project.
2242
+ _loop_attach() {
2243
+ local project_path; project_path=$(pwd -P)
2244
+ local slug; slug=$(_project_slug "$project_path")
2245
+ local session="roll-loop-${slug}"
2246
+
2247
+ if ! command -v tmux >/dev/null 2>&1; then
2248
+ warn "tmux not installed — install with 'brew install tmux' 请先安装 tmux"
2249
+ return 1
2250
+ fi
2251
+
2252
+ if ! tmux has-session -t "$session" 2>/dev/null; then
2253
+ info "No running loop session for this project 当前项目无运行中的 loop"
2254
+ info "Wait for next scheduled fire, or run: roll loop now"
2255
+ return 1
2256
+ fi
2257
+
2258
+ exec tmux attach -t "$session"
2259
+ }
2260
+
2261
+ # Pretty-print a duration in seconds as "Xs" / "Ym" / "Yh Zm".
2262
+ _loop_runs_dur() {
2263
+ local s="${1:-0}"
2264
+ if [[ "$s" -lt 60 ]]; then printf "%ds" "$s"
2265
+ elif [[ "$s" -lt 3600 ]]; then printf "%dm" "$((s / 60))"
2266
+ else printf "%dh %dm" "$((s / 3600))" "$(((s % 3600) / 60))"
2267
+ fi
2268
+ }
2269
+
2270
+ # Format a single JSONL run record for display.
2271
+ _loop_runs_format_line() {
2272
+ local line="$1" show_project="$2"
2273
+ command -v jq >/dev/null 2>&1 || { echo " (jq required)"; return 0; }
2274
+
2275
+ local ts status project tcr duration alerts run_id reason built_count built_csv skipped_count
2276
+ ts=$(jq -r '.ts // ""' <<<"$line")
2277
+ status=$(jq -r '.status // ""' <<<"$line")
2278
+ project=$(jq -r '.project // ""' <<<"$line")
2279
+ tcr=$(jq -r '.tcr_count // 0' <<<"$line")
2280
+ duration=$(jq -r '.duration_sec // 0' <<<"$line")
2281
+ alerts=$(jq -r '.alerts // 0' <<<"$line")
2282
+ run_id=$(jq -r '.run_id // ""' <<<"$line")
2283
+ reason=$(jq -r '.reason // ""' <<<"$line")
2284
+ built_count=$(jq -r '(.built // []) | length' <<<"$line")
2285
+ built_csv=$(jq -r '(.built // []) | join(", ")' <<<"$line")
2286
+ skipped_count=$(jq -r '(.skipped // []) | length' <<<"$line")
2287
+
2288
+ local hhmm; hhmm=$(printf "%s" "$ts" | sed -E 's/.*T([0-9]{2}):([0-9]{2}).*/\1:\2/')
2289
+ local prefix=""
2290
+ if [[ "$show_project" == "true" ]]; then
2291
+ prefix="[$(basename "$project")] "
2292
+ fi
2293
+
2294
+ case "$status" in
2295
+ built)
2296
+ local skipped_note=""
2297
+ [[ "$skipped_count" -gt 0 ]] && skipped_note=", ${skipped_count} skipped"
2298
+ printf " %s %s✅ built %s (%d items, %d tcr%s, %s)\n" \
2299
+ "$hhmm" "$prefix" "$built_csv" "$built_count" "$tcr" "$skipped_note" "$(_loop_runs_dur "$duration")"
2300
+ ;;
2301
+ idle)
2302
+ printf " %s %s○ idle — no Todo items\n" "$hhmm" "$prefix"
2303
+ ;;
2304
+ failed)
2305
+ local msg="${reason:-unknown}"
2306
+ printf " %s %s✗ FAILED — %s\n" "$hhmm" "$prefix" "$msg"
2307
+ ;;
2308
+ *)
2309
+ printf " %s %s? %s\n" "$hhmm" "$prefix" "$status"
2310
+ ;;
2311
+ esac
2312
+ }
2313
+
2314
+ # `roll loop runs [N] [--all]` — show recent loop iteration summaries.
2315
+ _loop_runs() {
2316
+ local n=10 all_flag=false
2317
+ while [[ $# -gt 0 ]]; do
2318
+ case "$1" in
2319
+ --all) all_flag=true; shift ;;
2320
+ [0-9]*) n="$1"; shift ;;
2321
+ *) shift ;;
2322
+ esac
2323
+ done
2324
+
2325
+ if [[ ! -f "$_LOOP_RUNS" ]] || [[ ! -s "$_LOOP_RUNS" ]]; then
2326
+ echo "No loop runs yet 尚无 loop 运行记录"
2327
+ return 0
2328
+ fi
2329
+
2330
+ if ! command -v jq >/dev/null 2>&1; then
2331
+ err "jq required for 'roll loop runs' 需要 jq"
2332
+ return 1
2333
+ fi
2334
+
2335
+ local project_path; project_path=$(pwd -P)
2336
+ local filtered
2337
+ if $all_flag; then
2338
+ filtered=$(cat "$_LOOP_RUNS")
2339
+ else
2340
+ filtered=$(jq -c --arg p "$project_path" 'select(.project == $p)' "$_LOOP_RUNS")
2341
+ fi
2342
+
2343
+ if [[ -z "$filtered" ]]; then
2344
+ echo "No loop runs for current project 当前项目尚无 loop 运行记录"
2345
+ return 0
2346
+ fi
2347
+
2348
+ local reversed; reversed=$(printf "%s\n" "$filtered" | awk '{a[NR]=$0} END{for(i=NR; i>=1; i--) print a[i]}')
2349
+ local recent; recent=$(printf "%s\n" "$reversed" | head -n "$n")
2350
+
2351
+ while IFS= read -r line; do
2352
+ [[ -z "$line" ]] && continue
2353
+ _loop_runs_format_line "$line" "$all_flag"
2354
+ done <<<"$recent"
2355
+ }
2356
+
2146
2357
  # Count `tcr:` prefixed commits in the current git repo since started_at timestamp.
2147
2358
  _loop_tcr_count() {
2148
2359
  local started_at="$1"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "2026.511.6",
3
+ "version": "2026.511.7",
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"
@@ -113,29 +113,31 @@ complexity or prevent future bugs. Ignore cosmetic issues.
113
113
 
114
114
  ### Dream Log (docs/dream/YYYY-MM-DD.md)
115
115
 
116
- Always write a log, even when no REFACTOR entries are created:
116
+ Always write a log, even when no REFACTOR entries are created. Output uses
117
+ Chinese to align with roll-brief style — easier for the morning reader to scan
118
+ without context switching:
117
119
 
118
120
  ```markdown
119
121
  # Dream Log {YYYY-MM-DD}
120
122
 
121
- ## Summary
122
- - Scans run: Dead Code / Architectural Drift / Pruning / Patterns
123
- - Findings: {N} flagged, {M} REFACTOR entries created
123
+ ## 概要
124
+ - 扫描项:死代码 / 架构漂移 / 裁剪候选 / 新兴模式
125
+ - 发现:{N} 项标记,{M} REFACTOR 条目已创建
124
126
 
125
- ## Dead Code
126
- {finding or "Nothing flagged."}
127
+ ## 死代码
128
+ {发现内容 "未发现死代码。"}
127
129
 
128
- ## Architectural Drift
129
- {finding or "No drift detected."}
130
+ ## 架构漂移
131
+ {发现内容 "未发现架构漂移。"}
130
132
 
131
- ## Pruning Candidates
132
- {finding or "Nothing flagged."}
133
+ ## 裁剪候选
134
+ {发现内容 "未发现可裁剪项。"}
133
135
 
134
- ## Emerging Patterns
135
- {finding or "No patterns detected."}
136
+ ## 新兴模式
137
+ {发现内容 "未发现可提取的重复模式。"}
136
138
 
137
- ## REFACTOR Entries Created
138
- {list or "None."}
139
+ ## 创建的 REFACTOR 条目
140
+ {列表 "无。"}
139
141
  ```
140
142
 
141
143
  ## Scheduler Configuration
@@ -151,7 +153,7 @@ The cron entry is generated using the configured agent — no manual cron editin
151
153
 
152
154
  If the scan fails partway through:
153
155
 
154
- 1. Write partial results to `docs/dream/YYYY-MM-DD.md` with a `## Status: PARTIAL` header
156
+ 1. Write partial results to `docs/dream/YYYY-MM-DD.md` with a `## 状态:部分完成` header
155
157
  2. Do not write incomplete REFACTOR entries to BACKLOG
156
158
  3. Log the error to `~/.shared/roll/dream/error.log`
157
159
 
@@ -153,6 +153,67 @@ last_run_items: [US-AUTH-003, FIX-007]
153
153
  last_run_outcome: success
154
154
  ```
155
155
 
156
+ Then append a JSONL record to `~/.shared/roll/loop/runs.jsonl` for per-iteration
157
+ visibility (one line per cycle, append-only — never delete or rewrite earlier lines).
158
+
159
+ **⚠️ Strict schema contract — do NOT deviate.** Every field has exactly one
160
+ canonical form. Synonyms like `"success"`, `"noop"`, `"completed"` are forbidden
161
+ for `status`. Numbers and arrays cannot be interchanged. UTC `Z` suffix only,
162
+ no timezone offsets. **No extra fields** — emit only the keys listed below (plus
163
+ optional `reason` when `status="failed"`); do not add `note`, `comment`,
164
+ `details`, `info`, etc. If you feel the urge to annotate, put it in the cycle's
165
+ final report in `cron.log` instead.
166
+
167
+ **Canonical record (copy this exact shape, fill in real values):**
168
+
169
+ ```json
170
+ {"ts":"2026-05-11T11:46:43Z","project":"roll-d9dfa0","run_id":"loop-20260511-1911","status":"built","built":["US-AUTO-024","US-AUTO-025"],"skipped":[],"alerts":[],"tcr_count":5,"duration_sec":2080}
171
+ ```
172
+
173
+ **Field contract — types are enforced**:
174
+
175
+ | Field | Type | Format / Enum |
176
+ |---|---|---|
177
+ | `ts` | string | ISO 8601 **UTC** with `Z` suffix. Get via `date -u +%Y-%m-%dT%H:%M:%SZ`. Never use `+08:00` or other offsets. |
178
+ | `project` | string | Project **slug** only (e.g. `roll-d9dfa0`), NOT the absolute path. Derive from `basename` of plist label or `_project_slug` output. |
179
+ | `run_id` | string | Matches `state.yaml` `run_id` exactly. Format: `loop-YYYYMMDD-HHMM`. |
180
+ | `status` | enum | Exactly one of: `built` (≥1 story shipped), `idle` (no Todo items found), `failed` (paused/error). **No synonyms.** |
181
+ | `built` | array&lt;string&gt; | Story ids completed this cycle. `[]` when none. **Always array, never null/number.** |
182
+ | `skipped` | array&lt;string&gt; | Story ids skipped because they were `🔨 In Progress`. `[]` when none. **Always array.** |
183
+ | `alerts` | array&lt;string&gt; | Newly raised ALERT identifiers/tags this cycle. `[]` when none. **Always array, never number.** |
184
+ | `tcr_count` | integer | Total `tcr:` prefix commits made this cycle. `0` when none. |
185
+ | `duration_sec` | integer | Seconds from cycle start to completion. Integer only, no decimals. |
186
+
187
+ Optional field, only when `status == "failed"`:
188
+ - `reason` (string): short human-readable explanation.
189
+
190
+ **Write recipe:**
191
+
192
+ ```bash
193
+ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
194
+ project=$(basename "$(pwd)" | sed 's/.*-//') # or use _project_slug if available
195
+ # duration_sec = cycle_end_epoch - cycle_start_epoch (track at Step 1)
196
+ # tcr_count = git log --oneline --since="<cycle_start>" | grep -c '^[a-f0-9]* tcr:'
197
+
198
+ jq -nc \
199
+ --arg ts "$ts" \
200
+ --arg project "$project" \
201
+ --arg run_id "$run_id" \
202
+ --arg status "built" \
203
+ --argjson built '["US-AUTO-024"]' \
204
+ --argjson skipped '[]' \
205
+ --argjson alerts '[]' \
206
+ --argjson tcr_count 14 \
207
+ --argjson duration_sec 1680 \
208
+ '{ts:$ts, project:$project, run_id:$run_id, status:$status,
209
+ built:$built, skipped:$skipped, alerts:$alerts,
210
+ tcr_count:$tcr_count, duration_sec:$duration_sec}' \
211
+ >> ~/.shared/roll/loop/runs.jsonl
212
+ ```
213
+
214
+ The companion read-side is `roll loop runs [N] [--all]` — shows the most recent
215
+ N records (default 10) for the current project, or across all projects with `--all`.
216
+
156
217
  ## Failure Handling
157
218
 
158
219
  ### Network Error (transient)
@@ -235,6 +296,38 @@ roll loop status # show current state
235
296
  roll loop now # execute one cycle immediately
236
297
  ```
237
298
 
299
+ ### Live attach (transparency)
300
+
301
+ Each loop iteration runs inside a detached tmux session named
302
+ `roll-loop-<slug>` (tmux is a required dependency — `roll setup` auto-installs
303
+ it via Homebrew on macOS, or prints the install command elsewhere).
304
+
305
+ **Default — auto-attach popup**: when the loop fires, a background Terminal
306
+ window pops up running `tmux attach -t roll-loop-<slug>`. You can watch the
307
+ agent work in real time without typing anything. The popup is best-effort
308
+ focus-retaining (it captures the previously-active app and restores focus
309
+ after the window appears) and the tmux session keeps running even if you
310
+ close the window.
311
+
312
+ **Manual attach** (any time):
313
+
314
+ ```bash
315
+ roll loop attach # exec tmux attach -t roll-loop-<slug>
316
+ ```
317
+
318
+ Press `Ctrl-B D` to detach — the loop continues running uninterrupted.
319
+
320
+ **Mute / unmute the popup**:
321
+
322
+ ```bash
323
+ roll loop mute # 🔇 — suppress auto-attach popup (loop still runs in tmux)
324
+ roll loop unmute # 🔔 — re-enable the popup
325
+ ```
326
+
327
+ Mute state is a single marker file at `~/.shared/roll/mute` and is shared
328
+ across all projects on this machine. Check the current state with
329
+ `roll loop status` — it shows an `Auto-attach: live | muted` line.
330
+
238
331
  ## Integration Map
239
332
 
240
333
  ```