@seanyao/roll 2026.512.7 → 2026.512.8

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## v2026.512.8
4
+ - **Added**: `$roll-doc` — legacy 项目文档自动化技能:四阶段扫描(索引 + 缺口分析 + 草稿补全 + 报告),支持 `--dry-run` / `--force`,适用任何项目
5
+ - **Added**: `roll-.dream` Scan 6 — 文档新鲜度检测(滞后文档 / 未记录 ENV 变量 / 架构文档缺失),依赖 roll-doc,发现写入 REFACTOR 条目
6
+ - **Fixed**: loop CI gate 在 SSH config 改写 github.com 为 IP 的环境下失灵 — `gh` 自动识别失败被静默吞掉,loop 把 "gh 出错" 误判为 "gh 未装",在 CI 红的情况下继续把 story 标 ✅ Done;现从 git remote 推导 `owner/repo` 强制传 `-R`,gh 调用失败 = ALERT,loop 起跑前先验 HEAD CI 红绿,红则拒绝 build
7
+
3
8
  ## v2026.512.7
4
9
  - **Added**: `roll alert` — 查看、确认、清除 loop 告警,不用再去翻 loop status
5
10
  - **Added**: macOS 系统通知 — story 完成或告警写入时自动弹通知,静音模式下不打扰
package/README.md CHANGED
@@ -19,12 +19,13 @@
19
19
 
20
20
  ## What is Roll?
21
21
 
22
- Roll is an instruction and workflow framework for AI agentsit encodes proven engineering practices (TDD, TCR, SRE, INVEST) as skills any agent can follow, distributes unified conventions to every AI client on your machine, and optionally lets the agent work unattended via an autonomous evolution layer.
22
+ Roll is an autonomous delivery system for software teamsAI agents pick stories from your BACKLOG, execute them with encoded engineering discipline, and ship continuously while you stay focused on what to build next.
23
23
 
24
- **Three core values:**
25
- 1. **Any agent, same constraints** — Claude, Cursor, Kimi, DeepSeek, Codex all receive identical engineering guardrails
26
- 2. **Skill system** — 20 skills encode research design build → check → fix as reliable, repeatable workflows
27
- 3. **Autonomous evolution** — `roll loop on` runs BACKLOG items hourly; humans retain sole release authority
24
+ **Two core values:**
25
+ 1. **Autonomous delivery** — `roll loop on` runs BACKLOG items hourly; Dream (nightly code-health scan) surfaces maintenance tasks; humans retain sole release authority
26
+ 2. **Skill-driven execution** — 20+ skills encode TDD, TCR, and INVEST practices as reliable, repeatable workflows any agent can follow
27
+
28
+ _Works with Claude, Cursor, Codex, or your own agent._
28
29
 
29
30
  ---
30
31
 
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.512.7"
7
+ VERSION="2026.512.8"
8
8
  ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
9
9
  ROLL_CONFIG="${ROLL_HOME}/config.yaml"
10
10
  ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
@@ -2595,11 +2595,12 @@ _loop_runs_dur() {
2595
2595
  }
2596
2596
 
2597
2597
  # Format a single JSONL run record for display.
2598
+ # Reads _LOOP_RUNS_BACKLOG global for ID→description lookup (set by _loop_runs).
2598
2599
  _loop_runs_format_line() {
2599
- local line="$1" show_project="$2"
2600
+ local line="$1" show_project="$2" is_darwin="$3"
2600
2601
  command -v jq >/dev/null 2>&1 || { echo " (jq required)"; return 0; }
2601
2602
 
2602
- local ts status project tcr duration alerts run_id reason built_count built_csv skipped_count
2603
+ local ts status project tcr duration alerts run_id reason built_count skipped_count
2603
2604
  ts=$(jq -r '.ts // ""' <<<"$line")
2604
2605
  status=$(jq -r '.status // ""' <<<"$line")
2605
2606
  project=$(jq -r '.project // ""' <<<"$line")
@@ -2609,10 +2610,16 @@ _loop_runs_format_line() {
2609
2610
  run_id=$(jq -r '.run_id // ""' <<<"$line")
2610
2611
  reason=$(jq -r '.reason // ""' <<<"$line")
2611
2612
  built_count=$(jq -r '(.built // []) | length' <<<"$line")
2612
- built_csv=$(jq -r '(.built // []) | join(", ")' <<<"$line")
2613
2613
  skipped_count=$(jq -r '(.skipped // []) | length' <<<"$line")
2614
2614
 
2615
- local hhmm; hhmm=$(printf "%s" "$ts" | sed -E 's/.*T([0-9]{2}):([0-9]{2}).*/\1:\2/')
2615
+ local hhmm epoch=""
2616
+ if [[ "$is_darwin" == "1" ]]; then
2617
+ epoch=$(date -j -u -f "%Y-%m-%dT%H:%M:%SZ" "$ts" "+%s" 2>/dev/null) || epoch=""
2618
+ [[ -n "$epoch" ]] && hhmm=$(date -j -f "%s" "$epoch" "+%H:%M" 2>/dev/null) || hhmm=""
2619
+ else
2620
+ hhmm=$(date -d "$ts" "+%H:%M" 2>/dev/null) || hhmm=""
2621
+ fi
2622
+ [[ -z "$hhmm" ]] && hhmm=$(printf "%s" "$ts" | sed -E 's/.*T([0-9]{2}):([0-9]{2}).*/\1:\2/')
2616
2623
  local prefix=""
2617
2624
  if [[ "$show_project" == "true" ]]; then
2618
2625
  prefix="[$(basename "$project")] "
@@ -2622,8 +2629,29 @@ _loop_runs_format_line() {
2622
2629
  built)
2623
2630
  local skipped_note=""
2624
2631
  [[ "$skipped_count" -gt 0 ]] && skipped_note=", ${skipped_count} skipped"
2625
- printf " %s %s✅ built %s (%d items, %d tcr%s, %s)\n" \
2626
- "$hhmm" "$prefix" "$built_csv" "$built_count" "$tcr" "$skipped_note" "$(_loop_runs_dur "$duration")"
2632
+ local items_word; [[ "$built_count" -eq 1 ]] && items_word="item" || items_word="items"
2633
+ printf " %s %s✅ built %d %s (%d tcr%s, %s)\n" \
2634
+ "$hhmm" "$prefix" "$built_count" "$items_word" "$tcr" "$skipped_note" "$(_loop_runs_dur "$duration")"
2635
+ local id desc
2636
+ while IFS= read -r id; do
2637
+ [[ -z "$id" ]] && continue
2638
+ desc=""
2639
+ if [[ -n "$_LOOP_RUNS_BACKLOG" ]]; then
2640
+ desc=$(printf "%s\n" "$_LOOP_RUNS_BACKLOG" | awk -F'|' -v id="$id" '
2641
+ NF >= 3 {
2642
+ cell = $2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", cell)
2643
+ if (cell == id || cell ~ "^\\[" id "\\]") {
2644
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", $3); print $3; exit
2645
+ }
2646
+ }')
2647
+ fi
2648
+ if [[ -n "$desc" ]]; then
2649
+ [[ ${#desc} -gt 72 ]] && desc="${desc:0:69}..."
2650
+ printf " • %-14s %s\n" "$id" "$desc"
2651
+ else
2652
+ printf " • %s\n" "$id"
2653
+ fi
2654
+ done < <(jq -r '(.built // []) | .[]' <<<"$line")
2627
2655
  ;;
2628
2656
  idle)
2629
2657
  printf " %s %s○ idle — no Todo items\n" "$hhmm" "$prefix"
@@ -2676,10 +2704,17 @@ _loop_runs() {
2676
2704
  local reversed; reversed=$(printf "%s\n" "$filtered" | awk '{a[NR]=$0} END{for(i=NR; i>=1; i--) print a[i]}')
2677
2705
  local recent; recent=$(printf "%s\n" "$reversed" | head -n "$n")
2678
2706
 
2707
+ local _is_darwin=""
2708
+ [[ "$(uname)" == "Darwin" ]] && _is_darwin="1"
2709
+
2710
+ _LOOP_RUNS_BACKLOG=""
2711
+ [[ -f "$project_path/BACKLOG.md" ]] && _LOOP_RUNS_BACKLOG=$(cat "$project_path/BACKLOG.md")
2712
+
2679
2713
  while IFS= read -r line; do
2680
2714
  [[ -z "$line" ]] && continue
2681
- _loop_runs_format_line "$line" "$all_flag"
2715
+ _loop_runs_format_line "$line" "$all_flag" "$_is_darwin"
2682
2716
  done <<<"$recent"
2717
+ unset _LOOP_RUNS_BACKLOG
2683
2718
  }
2684
2719
 
2685
2720
  # Send a macOS system notification. No-op when muted, non-macOS, or osascript unavailable.
@@ -2699,9 +2734,27 @@ _loop_tcr_count() {
2699
2734
  | awk '/^[a-f0-9]+ tcr:/{n++} END{print n+0}'
2700
2735
  }
2701
2736
 
2737
+ # Parse origin remote URL → "owner/repo" for GitHub repos.
2738
+ # Returns non-zero if no origin, or origin is not github.com.
2739
+ # Decoupled from `gh` auto-detection so SSH config host rewrites don't break it.
2740
+ _gh_repo_slug() {
2741
+ local url
2742
+ url=$(git config --get remote.origin.url 2>/dev/null) || return 1
2743
+ case "$url" in
2744
+ git@github.com:*) url="${url#git@github.com:}" ;;
2745
+ ssh://git@github.com/*) url="${url#ssh://git@github.com/}" ;;
2746
+ https://github.com/*) url="${url#https://github.com/}" ;;
2747
+ http://github.com/*) url="${url#http://github.com/}" ;;
2748
+ *) return 1 ;;
2749
+ esac
2750
+ url="${url%.git}"
2751
+ [[ -z "$url" ]] && return 1
2752
+ printf "%s\n" "$url"
2753
+ }
2754
+
2702
2755
  # Poll gh run list until current commit's CI completes.
2703
- # Returns 0 on success or when gh is unavailable (graceful skip).
2704
- # Returns 1 on CI failure or timeout.
2756
+ # Returns 0 on success (or when gh binary missing graceful skip).
2757
+ # Returns 1 on CI failure, timeout, or any gh call failure.
2705
2758
  _ci_wait() {
2706
2759
  local timeout="${1:-300}"
2707
2760
  local interval=15
@@ -2712,13 +2765,20 @@ _ci_wait() {
2712
2765
  local commit; commit=$(git rev-parse HEAD 2>/dev/null) || { err "Not a git repo 非 git 仓库"; return 1; }
2713
2766
  local short; short=$(git rev-parse --short HEAD 2>/dev/null)
2714
2767
 
2715
- ok "Waiting for CI on ${short} 等待 CI: ${short}"
2768
+ # Resolve owner/repo from git remote so we don't depend on gh's auto-detection,
2769
+ # which breaks when ~/.ssh/config rewrites `Host github.com` → IP address.
2770
+ local repo_slug; repo_slug=$(_gh_repo_slug) || {
2771
+ err "Cannot determine GitHub repo from origin remote 无法从 origin 推导 GitHub 仓库"
2772
+ return 1
2773
+ }
2774
+
2775
+ ok "Waiting for CI on ${short} (${repo_slug}) 等待 CI: ${short}"
2716
2776
 
2717
2777
  while (( elapsed < timeout )); do
2718
2778
  local runs
2719
- runs=$(gh run list --commit "$commit" --json status,conclusion 2>/dev/null) || {
2720
- warn "gh run list failed skipping CI gate"
2721
- return 0
2779
+ runs=$(gh -R "$repo_slug" run list --commit "$commit" --json status,conclusion 2>&1) || {
2780
+ err "gh run list failed for ${repo_slug}@${short}: ${runs} gh 调用失败"
2781
+ return 1
2722
2782
  }
2723
2783
 
2724
2784
  if [[ -z "$runs" || "$runs" == "[]" ]]; then
@@ -2754,6 +2814,46 @@ _ci_wait() {
2754
2814
  return 1
2755
2815
  }
2756
2816
 
2817
+ # Pre-run CI health check — call before picking up new stories.
2818
+ # Refuses to build on a red base (HEAD CI failed). Lenient on unknown states
2819
+ # (gh missing, repo unparseable, no runs yet) — the post-build _loop_enforce_ci
2820
+ # is the strict gate.
2821
+ # Returns 0: ok to proceed (green / pending / unknown / no gh).
2822
+ # Returns 1: HEAD CI is definitively red → ALERT written, do not build.
2823
+ _loop_precheck_ci() {
2824
+ command -v gh &>/dev/null || return 0
2825
+
2826
+ local commit; commit=$(git rev-parse HEAD 2>/dev/null) || return 0
2827
+ local slug; slug=$(_gh_repo_slug) || return 0
2828
+
2829
+ local runs
2830
+ runs=$(gh -R "$slug" run list --commit "$commit" --json conclusion 2>/dev/null) || return 0
2831
+ [[ -z "$runs" || "$runs" == "[]" ]] && return 0
2832
+
2833
+ local failed
2834
+ failed=$(echo "$runs" | jq -r '[.[] | select(.conclusion != null and .conclusion != "success" and .conclusion != "skipped")] | length' 2>/dev/null || echo "0")
2835
+
2836
+ if [[ "$failed" -gt 0 ]]; then
2837
+ local short; short=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)
2838
+ err "Pre-run CI check: HEAD CI is red — refuse to build on broken base (${short}) HEAD CI 红,拒绝在破损的基础上构建"
2839
+ mkdir -p "$(dirname "$_LOOP_ALERT")"
2840
+ cat > "$_LOOP_ALERT" << EOF
2841
+ # ALERT — Pre-run CI check failed (red base)
2842
+
2843
+ **Time**: $(date '+%Y-%m-%d %H:%M')
2844
+ **Commit**: ${short}
2845
+ **Reason**: HEAD CI is red — loop refused to build on a broken base HEAD CI 红,loop 拒绝在破损的基础上构建
2846
+
2847
+ **Action required**:
2848
+ - Investigate and fix CI: \`gh -R $(_gh_repo_slug) run list --commit ${commit}\`
2849
+ - After fixing and pushing green commit: \`roll loop now\`
2850
+ EOF
2851
+ _notify "roll ⚠ CI red" "loop refused to build on broken base (${short})"
2852
+ return 1
2853
+ fi
2854
+ return 0
2855
+ }
2856
+
2757
2857
  # CI gate before marking a story Done.
2758
2858
  # On CI failure: writes ALERT, returns 1 (caller keeps story 🔨 In Progress).
2759
2859
  # When gh unavailable: returns 0 (graceful skip).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "2026.512.7",
3
+ "version": "2026.512.8",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "scripts": {
6
6
  "test": "bash tests/run.sh"
@@ -6,8 +6,9 @@ allowed-tools: "Read, Glob, Grep, Bash(git:*), Write, Edit"
6
6
  description: |
7
7
  Nightly code and architecture health scan. Passively triggered by scheduler
8
8
  (cron or GitHub Actions), not invoked by users directly. Detects dead code,
9
- architectural drift from domain model, pruning candidates, and emerging patterns.
10
- Outputs REFACTOR entries to BACKLOG.md and a daily log to docs/dream/.
9
+ architectural drift from domain model, pruning candidates, emerging patterns,
10
+ doc coverage gaps, and doc staleness (文档新鲜度). Outputs REFACTOR entries
11
+ to BACKLOG.md and a daily log to docs/dream/.
11
12
  Distinct from roll-sentinel: sentinel monitors runtime behavior; dream reviews
12
13
  code structure and architectural health.
13
14
  ---
@@ -33,7 +34,7 @@ in the morning brief.
33
34
 
34
35
  ## Scan Logic
35
36
 
36
- Run all four scans every night. Each scan is independent.
37
+ Run all six scans every night. Each scan is independent.
37
38
 
38
39
  ### Scan 1 — Dead Code
39
40
 
@@ -68,6 +69,8 @@ Flag: modules that import directly from a different Bounded Context without
68
69
  an Anti-Corruption Layer, or module names that have diverged from the
69
70
  Ubiquitous Language.
70
71
 
72
+ **Distinction from Scan 6C**: Scan 2 flags *import boundary violations* (cross-context coupling). Scan 6C flags *missing documentation entries* (module exists but has no entry in `docs/domain/*.md`). Never double-flag — Scan 2 and Scan 6C are orthogonal checks.
73
+
71
74
  ### Scan 3 — Pruning Candidates
72
75
 
73
76
  Find over-engineering that can be simplified:
@@ -137,6 +140,77 @@ Flag any `.md` file directly in `docs/` root (allowed subdirs: `guide/`, `domain
137
140
  {发现内容 或 "文档结构符合规范,无缺口。"}
138
141
  ```
139
142
 
143
+ ### Scan 6 — 文档新鲜度 (Doc Freshness)
144
+
145
+ **Dependency gate**: Skip Scan 6 entirely when `$roll-doc` (US-SKILL-008) is not yet deployed.
146
+ Check: `[ -f "$ROLL_HOME/skills/roll-doc/SKILL.md" ]`. If absent, log "Scan 6 skipped — roll-doc not deployed" in the dream log and stop. No fallback.
147
+
148
+ When deployed, each finding produces a REFACTOR entry with `$roll-doc` as execution hint:
149
+ ```markdown
150
+ | REFACTOR-XXX | docs: <description> — flagged by dream <date> (hint: $roll-doc) | 📋 Todo |
151
+ ```
152
+
153
+ #### Check A — Stale Docs
154
+
155
+ Flag source files whose owning doc is >30 days stale:
156
+
157
+ ```bash
158
+ # For each file listed in docs/features/*.md or README.md "## Files:" sections:
159
+ # owner_doc_commit = git log -1 --format="%ci" -- <doc_file>
160
+ # source_commit = git log -1 --format="%ci" -- <source_file>
161
+ # lag_days = (source_commit - owner_doc_commit) in days
162
+ # if lag_days > 30 AND doc contains at least one specific file path reference → flag
163
+ ```
164
+
165
+ The "owner doc" for a source file is the nearest `README.md` or `docs/features/*.md` that lists the file path in a `## Files:` section. Skip docs that contain only conceptual descriptions (no specific file path references) — they cannot be objectively stale.
166
+
167
+ #### Check B — Undocumented ENV Vars
168
+
169
+ Flag environment variables that appear frequently in source but have no documentation:
170
+
171
+ ```bash
172
+ # Detect ENV var patterns in non-test source files:
173
+ patterns=(
174
+ 'process\.env\.[A-Z_]+' # Node.js
175
+ 'os\.getenv\("[A-Z_]+"\)' # Python
176
+ 'ENV\["[A-Z_]+"\]' # Ruby
177
+ )
178
+ # For each matched variable name:
179
+ # count occurrences across all source files
180
+ # if count >= 5 AND zero mentions in any .md file → flag
181
+ ```
182
+
183
+ Flag variables appearing ≥5× in source with zero mentions in any `.md`.
184
+ "Other convention signals" (comment clusters, module structure templates) are explicitly deferred — too vague for deterministic detection.
185
+
186
+ #### Check C — Existence Drift
187
+
188
+ Find module directories that exist in code but are absent from architecture docs.
189
+ This is distinct from Scan 2 (which checks *import violations*) — Scan 6C checks *documentation existence*:
190
+
191
+ ```bash
192
+ # Walk all non-excluded directories
193
+ # For each dir with >= 3 non-hidden, non-.md source files:
194
+ # check if any docs/domain/*.md contains the directory name
195
+ # if not found → flag as "existence drift"
196
+ ```
197
+
198
+ Exclusions: `node_modules/`, `.git/`, `dist/`, `build/`, `.shared/`, `docs/`, `tests/`.
199
+
200
+ Flag directories with ≥3 source files and zero name-match in `docs/domain/*.md`.
201
+
202
+ #### Dream Log Section
203
+
204
+ Add after `## 文档覆盖度` section:
205
+
206
+ ```markdown
207
+ ## 文档新鲜度
208
+ - 滞后文档:{N} 个(超过 30 天未更新但绑定了代码文件)
209
+ - 未记录 ENV 变量:{N} 个(出现 ≥5 次但无文档)
210
+ - 架构文档缺失模块:{N} 个(≥3 个源文件的目录未出现在 docs/domain/)
211
+ {发现内容列表 或 "文档新鲜度良好,无滞后或缺失项。"}
212
+ ```
213
+
140
214
  ## Output
141
215
 
142
216
  ### REFACTOR Entry (BACKLOG.md)
@@ -161,7 +235,7 @@ without context switching:
161
235
  # Dream Log {YYYY-MM-DD}
162
236
 
163
237
  ## 概要
164
- - 扫描项:死代码 / 架构漂移 / 裁剪候选 / 新兴模式 / 文档覆盖度
238
+ - 扫描项:死代码 / 架构漂移 / 裁剪候选 / 新兴模式 / 文档覆盖度 / 文档新鲜度
165
239
  - 发现:{N} 项标记,{M} 个 REFACTOR 条目已创建
166
240
 
167
241
  ## 死代码
@@ -176,6 +250,15 @@ without context switching:
176
250
  ## 新兴模式
177
251
  {发现内容 或 "未发现可提取的重复模式。"}
178
252
 
253
+ ## 文档覆盖度
254
+ {发现内容 或 "文档结构符合规范,无缺口。"}
255
+
256
+ ## 文档新鲜度
257
+ - 滞后文档:{N} 个
258
+ - 未记录 ENV 变量:{N} 个
259
+ - 架构文档缺失模块:{N} 个
260
+ {发现内容 或 "文档新鲜度良好,无滞后或缺失项。"}
261
+
179
262
  ## 创建的 REFACTOR 条目
180
263
  {列表 或 "无。"}
181
264
  ```
@@ -0,0 +1,184 @@
1
+ ---
2
+ name: roll-doc
3
+ license: MIT
4
+ allowed-tools: "Read, Write, Edit, Glob, Grep, Bash(date:*,find:*,stat:*,wc:*)"
5
+ description: "Legacy project documentation automation. Scans all docs, builds/updates docs/INDEX.md, identifies undocumented modules, and generates draft fills for gaps. Works on any project."
6
+ ---
7
+
8
+ # roll-doc
9
+
10
+ Four-phase legacy documentation automation: scan → index → gap analysis → fill.
11
+
12
+ Works on any project root. No manual mode switching — reads the project state and decides what to do.
13
+
14
+ ## When to Use
15
+
16
+ - Starting to work on a legacy project with scattered or missing documentation
17
+ - `docs/INDEX.md` is out of date or doesn't exist yet
18
+ - `roll-.dream` Scan 6 flagged undocumented modules (REFACTOR entry referencing roll-doc)
19
+ - You want a complete picture of what's documented and what isn't
20
+
21
+ ## When Not to Use
22
+
23
+ - The project has up-to-date, maintained docs — no need to rebuild the index
24
+ - You need authoritative documentation reviewed and approved (drafts only; human reviews and commits)
25
+ - You want to write a specific known doc from scratch — write it directly
26
+
27
+ ## Invocation
28
+
29
+ ```
30
+ $roll-doc # Full four-phase run
31
+ $roll-doc --dry-run # Phases 1–2 only; print Phase 3 plan without writing any files
32
+ $roll-doc --force # Re-generate drafts even for existing files
33
+ ```
34
+
35
+ ## Phase 1 — Scan & Index
36
+
37
+ Scan the project root for all `*.md` files and known convention files.
38
+
39
+ **Exclusions — never scan these directories:**
40
+
41
+ ```
42
+ node_modules/ .git/ dist/ build/ .shared/ docs/dream/ docs/briefs/
43
+ ```
44
+
45
+ **Convention files — detect by filename anywhere in the tree:**
46
+
47
+ ```
48
+ AGENTS.md CLAUDE.md GEMINI.md CONVENTIONS.md CONTRIBUTING.md
49
+ ARCHITECTURE.md ADR-*.md
50
+ ```
51
+
52
+ **Classification rules:**
53
+
54
+ | Category | Criteria |
55
+ |----------|----------|
56
+ | `guide` | Path under `docs/guide/` |
57
+ | `domain` | Path under `docs/domain/` |
58
+ | `convention` | Filename matches convention markers list above |
59
+ | `module` | File is `<dir>/README.md` for a source directory |
60
+ | `stray` | None of the above (top-level, unorganized, or orphaned) |
61
+
62
+ **Output — produce/update `docs/INDEX.md`:**
63
+
64
+ ```markdown
65
+ # Documentation Index
66
+
67
+ > Auto-generated by roll-doc on YYYY-MM-DD. Edit individual docs, not this file.
68
+
69
+ ## Index
70
+
71
+ | Path | Title | Category | Last Modified |
72
+ |------|-------|----------|---------------|
73
+ | docs/guide/en/loop.md | Loop User Guide | guide | 2026-05-01 |
74
+ | AGENTS.md | Agent Conventions | convention | 2026-04-28 |
75
+
76
+ ## Coverage Summary
77
+
78
+ - Total docs indexed: N
79
+ - By category: guide (N) / domain (N) / convention (N) / module (N) / stray (N)
80
+
81
+ ## Gap Report
82
+
83
+ Directories with ≥3 source files and no linked documentation:
84
+
85
+ | Directory | Source Files | Missing Doc |
86
+ |-----------|-------------|-------------|
87
+ | src/commands/ | 8 | README.md |
88
+ ```
89
+
90
+ `docs/INDEX.md` is always overwritten on each run — it is a derived artifact, not authoritative content.
91
+
92
+ ## Phase 2 — Gap Analysis
93
+
94
+ Walk every directory (applying Phase 1 exclusions):
95
+
96
+ 1. Count non-hidden, non-`.md` source files directly in the directory
97
+ 2. If count ≥ 3 AND no `README.md` in that directory AND no `docs/INDEX.md` entry links to it → **module gap**
98
+
99
+ **Special gaps (checked once per project):**
100
+ - No `docs/domain/` directory or empty → gap: `docs/domain/context-map.md`
101
+ - No `CONVENTIONS.md` or `docs/CONVENTIONS.md` exists → gap: `docs/CONVENTIONS.md`
102
+
103
+ **Threshold**: directories with ≥ 3 source files (default). Tune by editing the skill.
104
+
105
+ Record all gaps — they become Phase 3 input.
106
+
107
+ ## Phase 3 — Fill
108
+
109
+ **Skip this phase entirely when:**
110
+ - `--dry-run` was passed (print the fill plan to stdout, write nothing)
111
+ - Phase 2 found zero gaps
112
+
113
+ **Idempotency rule**: Without `--force`, re-running when no new gaps exist is a **no-op** — no files are written, no existing drafts are modified.
114
+
115
+ For each gap:
116
+ 1. Read up to 20 source files from the target directory to infer module purpose, key exports, dependencies, and configuration patterns
117
+ 2. Generate a draft document at the conventional location (see table below)
118
+ 3. **Skip if the target file already exists**, unless `--force` was passed
119
+
120
+ **Draft locations by gap type:**
121
+
122
+ | Gap Type | Draft Location |
123
+ |----------|---------------|
124
+ | Module with no README | `<dir>/README.md` |
125
+ | No `docs/domain/` entries | `docs/domain/context-map.md` |
126
+ | No conventions doc | `docs/CONVENTIONS.md` |
127
+
128
+ **Every generated file starts with this exact header line:**
129
+
130
+ ```
131
+ > **Draft** — auto-generated by roll-doc on YYYY-MM-DD. Review before treating as authoritative.
132
+ ```
133
+
134
+ **Minimum draft content:**
135
+
136
+ - Module README: purpose (1–2 sentences), key files with one-line descriptions, dependencies (imports from / depended on by)
137
+ - Context map: bounded contexts identified in the project, their responsibilities, relationships
138
+ - Conventions: detected patterns — ENV vars, naming conventions, repeated file structure templates
139
+
140
+ Do not fabricate details — infer only from source files actually read.
141
+
142
+ ## Phase 4 — Report
143
+
144
+ After all phases complete, output a summary:
145
+
146
+ ```
147
+ 📚 roll-doc complete
148
+
149
+ Phase 1 — Index
150
+ N docs scanned, docs/INDEX.md updated
151
+ Categories: guide(N) domain(N) convention(N) module(N) stray(N)
152
+
153
+ Phase 2 — Gaps
154
+ N undocumented module directories found
155
+ N special gaps (domain map / conventions)
156
+
157
+ Phase 3 — Fill
158
+ N drafts generated: [list of paths]
159
+ N skipped (already exist; use --force to regenerate)
160
+
161
+ 📋 Review priority (largest / most active modules first):
162
+ 1. src/commands/README.md — 8 source files
163
+ 2. docs/CONVENTIONS.md — 6 patterns detected
164
+ ```
165
+
166
+ If no gaps were found:
167
+
168
+ ```
169
+ ✅ roll-doc: no gaps found. docs/INDEX.md updated.
170
+ ```
171
+
172
+ If `--dry-run`:
173
+
174
+ ```
175
+ 🔍 roll-doc --dry-run: N drafts would be generated (nothing written).
176
+ ```
177
+
178
+ ## Rules
179
+
180
+ - Never modify existing documentation files — only generate new drafts
181
+ - `docs/INDEX.md` is the only existing file that may be overwritten (derived artifact)
182
+ - Draft files are never committed by this skill — human reviews and commits them
183
+ - Works on any project; always read project structure before acting
184
+ - Do not fabricate module details — infer only from source files actually read
@@ -68,7 +68,21 @@ fi
68
68
  2. For each such story, check `state.yaml`:
69
69
  - If `current_item` matches the story id AND `status: running` → this is the resume case (handled above), leave it.
70
70
  - 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.
71
- 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.
71
+ 3. After orphan sweep, proceed to Step 1.5 (Pre-run CI health check) before scanning.
72
+
73
+ ### Step 1.5 — Pre-run CI Health Check
74
+
75
+ Call `_loop_precheck_ci` before scanning BACKLOG. This is a **defensive gate**
76
+ against building on a broken base — if the most recent commit on the branch
77
+ has red CI, the loop must not stack new commits on top (which would create the
78
+ exact stuck-red state FIX-026 traces to).
79
+
80
+ - HEAD CI green / pending / no-run-yet → proceed to Step 2.
81
+ - HEAD CI red → write ALERT, **do not pick up any stories this cycle**,
82
+ exit cleanly. The next cycle will retry; the human must fix CI manually
83
+ (typically by reverting or pushing a green commit) before the loop resumes.
84
+ - `gh` missing or repo unparseable → graceful skip (`_loop_precheck_ci`
85
+ returns 0); the post-build `_loop_enforce_ci` remains the strict gate.
72
86
 
73
87
  ### Step 2 — Scan BACKLOG
74
88
 
@@ -142,11 +156,18 @@ After each item completes:
142
156
  - Count `tcr:` prefix commits since `started_at` via `git log --oneline --since=<started_at>`
143
157
  - Count == 0 → revert story status in BACKLOG.md from ✅ Done → 📋 Todo; write ALERT to `~/.shared/roll/loop/ALERT.md` with story ID, time, reason "zero tcr: commits since story start", and suggested actions (`roll loop now` / `$roll-build <id>` / `roll loop reset`)
144
158
  - Count > 0 → continue normally
145
- 2. **CI Gate** — call `roll ci --wait` (or `_loop_enforce_ci <story_id>`):
146
- - Polls `gh run list --commit <HEAD>` until all CI runs complete
159
+ 2. **CI Gate** — **MUST** invoke `roll ci --wait` (the `_loop_enforce_ci`
160
+ wrapper). **Do NOT call `gh` directly** (no `gh run list`, no `gh run watch`,
161
+ no ad-hoc shell checks): `roll ci --wait` is the only sanctioned entry —
162
+ it derives `owner/repo` from the git remote and uses `gh -R <slug>`, which
163
+ is required to work through `~/.ssh/config` host rewrites that break gh's
164
+ auto-detection.
147
165
  - CI passes → continue normally
148
- - CI fails or times out → keep story as `🔨 In Progress` (do NOT mark ✅ Done); write ALERT; skip to next story
149
- - `gh` not installed skip gracefully (return 0)
166
+ - CI fails / times out / `gh` call fails → keep story as `🔨 In Progress`
167
+ (do NOT mark Done); write ALERT; skip to next story
168
+ - `gh` binary not installed (`command -v gh` fails) → skip gracefully
169
+ (return 0). Any other `gh` error is **not** "gh unavailable" — it is a
170
+ hard failure and must block the gate.
150
171
  3. Update state file: `status: idle`
151
172
  4. Check if a Feature is now fully complete (all its Stories ✅)
152
173
  5. If yes and `brief_on_feature_complete: true` → invoke `Skill("roll-brief")`