@seanyao/roll 3.609.2 → 3.610.2

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.
Files changed (103) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +5 -6
  3. package/dist/roll.mjs +16146 -16186
  4. package/package.json +3 -2
  5. package/skills/README.md +14 -1
  6. package/skills/docs/skill-authoring.md +66 -0
  7. package/skills/reports/skill-audit-summary.md +53 -0
  8. package/skills/roll-.changelog/SKILL.md +25 -443
  9. package/skills/roll-.changelog/references/full-contract.md +462 -0
  10. package/skills/roll-.clarify/SKILL.md +6 -4
  11. package/skills/roll-.dream/SKILL.md +26 -353
  12. package/skills/roll-.dream/references/full-contract.md +365 -0
  13. package/skills/roll-.echo/SKILL.md +6 -4
  14. package/skills/roll-.qa/SKILL.md +25 -236
  15. package/skills/roll-.qa/references/full-contract.md +256 -0
  16. package/skills/roll-.review/SKILL.md +6 -2
  17. package/skills/roll-brief/SKILL.md +6 -8
  18. package/skills/roll-build/SKILL.md +28 -864
  19. package/skills/roll-build/references/full-contract.md +883 -0
  20. package/skills/roll-debug/SKILL.md +26 -585
  21. package/skills/roll-debug/references/full-contract.md +607 -0
  22. package/skills/roll-design/SKILL.md +28 -903
  23. package/skills/roll-design/references/full-contract.md +923 -0
  24. package/skills/roll-doc/SKILL.md +25 -574
  25. package/skills/roll-doc/references/full-contract.md +594 -0
  26. package/skills/roll-doctor/SKILL.md +21 -2
  27. package/skills/roll-fix/SKILL.md +28 -621
  28. package/skills/roll-fix/references/full-contract.md +640 -0
  29. package/skills/roll-idea/SKILL.md +6 -2
  30. package/skills/roll-loop/SKILL.md +27 -543
  31. package/skills/roll-loop/references/full-contract.md +555 -0
  32. package/skills/roll-notes/SKILL.md +6 -2
  33. package/skills/roll-onboard/SKILL.md +6 -2
  34. package/skills/roll-peer/SKILL.md +27 -316
  35. package/skills/roll-peer/references/full-contract.md +329 -0
  36. package/skills/roll-propose/SKILL.md +6 -8
  37. package/skills/roll-review-pr/SKILL.md +6 -2
  38. package/skills/roll-sentinel/SKILL.md +26 -344
  39. package/skills/roll-sentinel/references/full-contract.md +363 -0
  40. package/skills/roll-spar/SKILL.md +27 -269
  41. package/skills/roll-spar/references/full-contract.md +288 -0
  42. package/skills/route-cases/skills.json +235 -0
  43. package/skills/scripts/audit-skills.mjs +272 -0
  44. package/skills/scripts/test-audit-skills.mjs +39 -0
  45. package/skills/tests/fixtures/skill-audit/block-skill/SKILL.md +12 -0
  46. package/skills/tests/fixtures/skill-audit/minimal-skill/SKILL.md +8 -0
  47. package/skills/tests/fixtures/skill-audit/quoted-skill/SKILL.md +10 -0
  48. package/skills/tests/fixtures/skill-audit/route-cases.json +21 -0
  49. package/skills/tests/fixtures/skill-audit/spoke-skill/SKILL.md +12 -0
  50. package/skills/tests/fixtures/skill-audit/spoke-skill/references/runbook.md +3 -0
  51. package/bin/roll +0 -15361
  52. package/lib/backfill-pi-usage.py +0 -243
  53. package/lib/changelog_audit.py +0 -149
  54. package/lib/changelog_generate.py +0 -470
  55. package/lib/consistency_check.py +0 -409
  56. package/lib/context_feed_budget.sh +0 -194
  57. package/lib/github_sync.py +0 -876
  58. package/lib/i18n/slides.sh +0 -3
  59. package/lib/i18n/slides_build.sh +0 -38
  60. package/lib/i18n/slides_delete.sh +0 -19
  61. package/lib/i18n/slides_list.sh +0 -14
  62. package/lib/i18n/slides_logs.sh +0 -12
  63. package/lib/i18n/slides_new.sh +0 -15
  64. package/lib/i18n/slides_preview.sh +0 -14
  65. package/lib/i18n/slides_templates.sh +0 -7
  66. package/lib/i18n.sh +0 -211
  67. package/lib/loop-exit-summary.py +0 -393
  68. package/lib/loop-fmt.py +0 -589
  69. package/lib/loop_pick_agent.py +0 -316
  70. package/lib/loop_result_eval.py +0 -469
  71. package/lib/loop_unstick.py +0 -180
  72. package/lib/model_prices.py +0 -194
  73. package/lib/prices_fetcher.py +0 -534
  74. package/lib/roll-backlog.py +0 -225
  75. package/lib/roll-brief.py +0 -286
  76. package/lib/roll-help.py +0 -158
  77. package/lib/roll-home.py +0 -556
  78. package/lib/roll-init.py +0 -156
  79. package/lib/roll-loop-status.py +0 -1691
  80. package/lib/roll-loop-story.py +0 -191
  81. package/lib/roll-peer.py +0 -252
  82. package/lib/roll-setup.py +0 -102
  83. package/lib/roll-status.py +0 -367
  84. package/lib/roll_git.py +0 -41
  85. package/lib/roll_render.py +0 -414
  86. package/lib/slides/components/README.md +0 -123
  87. package/lib/slides/components/cards-2.html +0 -9
  88. package/lib/slides/components/cards-3.html +0 -9
  89. package/lib/slides/components/cards-4.html +0 -9
  90. package/lib/slides/components/compare.html +0 -22
  91. package/lib/slides/components/highlight.html +0 -9
  92. package/lib/slides/components/pipeline.html +0 -12
  93. package/lib/slides/components/plain.html +0 -7
  94. package/lib/slides/components/quote.html +0 -4
  95. package/lib/slides/components/timeline.html +0 -9
  96. package/lib/slides/templates/introduction-v3.html +0 -571
  97. package/lib/slides/templates/pitch.html +0 -0
  98. package/lib/slides-render.py +0 -778
  99. package/lib/slides-validate.py +0 -357
  100. package/lib/test_quality_gate.py +0 -143
  101. package/skills/roll-deck/SKILL.md +0 -296
  102. /package/skills/roll-debug/{injectable-bb.js → assets/injectable-bb.js} +0 -0
  103. /package/skills/roll-design/{ENGINEERING_CHECKLIST.md → references/engineering-checklist.md} +0 -0
@@ -2,562 +2,46 @@
2
2
  name: roll-loop
3
3
  license: MIT
4
4
  allowed-tools: "Read, Glob, Grep, Write, Edit, Bash(git:*), Bash(cat:*), Skill"
5
- description: |
6
- Autonomous BACKLOG executor. Runs on a schedule (hourly via cron or GitHub
7
- Actions), scans .roll/backlog.md for 📋 Todo items, and routes each to the
8
- appropriate skill: US-XXX → $roll-build, FIX-XXX → $roll-fix,
9
- REFACTOR-XXX → $roll-build. Retries the primary agent up to 3 times on
10
- transient failure; pauses with ALERT on persistent failure.
11
- Never cuts a release autonomously — release is always a human decision.
12
- Triggers roll-brief when a Feature completes.
5
+ description: "Load when configuring, explaining, or operating Roll autonomous backlog execution loop that scans Todo work and dispatches US/FIX/REFACTOR items."
13
6
  ---
7
+ # Roll Loop
14
8
 
15
- # Roll Loop (Autonomous BACKLOG Executor)
9
+ This hub keeps the routing boundary, hard gates, and execution skeleton in the initial context. Load the heavier runbook only when the task actually needs the detailed contract.
16
10
 
17
- > Follows the Architecture Constraints, Development Discipline, and Engineering
18
- > Common Sense defined in the project AGENTS.md.
11
+ ## Load
19
12
 
20
- Runs on a schedule. Picks up pending BACKLOG items and executes them without
21
- human intervention. The human stays informed via `roll-brief` and retains
22
- sole authority over releases.
13
+ Load when configuring, explaining, or operating Roll autonomous backlog execution loop that scans Todo work and dispatches US/FIX/REFACTOR items.
23
14
 
24
- ## Execution Boundary
15
+ ## When Not to Use
25
16
 
26
- **What roll-loop executes autonomously:**
27
- - US-XXX (User Stories) `$roll-build`
28
- - FIX-XXX (Bug fixes) → `$roll-fix`
29
- - REFACTOR-XXX (Refactors) → `$roll-build`
17
+ - One-shot story execution by a human agent; load roll-build or roll-fix.
18
+ - Nightly architecture scan; load roll-.dream.
30
19
 
31
- **What roll-loop never executes:**
32
- - Releases — production deployment is always a human decision (requires 2FA in real terminal)
33
- - Any Story marked 🚫 Hold or flagged for human review
34
- - Destructive operations outside normal skill scope
20
+ ## Read On Demand
35
21
 
36
- **Human bypass path** roll-loop 是默认调度器,不垄断执行权。任何时刻人可直接
37
- `$roll-build US-XXX` `$roll-fix FIX-XXX` 绕过 loop 立即执行(紧急 bug、中断插入、
38
- 故事评审等场景)。loop 通过 LOCK 和 `🔨 In Progress` 状态识别并跳过人正在做的故事,
39
- 人机并行不会撞车(见 Concurrency Safety)。
22
+ - Read [the full contract](references/full-contract.md) before executing the workflow end to end, recovering from failures, or checking exact output templates.
23
+ - Keep this hub in context for trigger boundaries and hard gates.
40
24
 
41
- ## Environment Constraints (autonomous loop)
25
+ ## Workflow Skeleton
42
26
 
43
- You are running inside an autonomous cycle. No human is watching this turn.
44
- Adapt commands to the constraints below — otherwise you will burn turns on
45
- denied operations and the cycle will idle-exit.
27
+ 1. Scan BACKLOG for Todo items.
28
+ 2. Route US/FIX/REFACTOR to the right skill.
29
+ 3. Run bounded cycles with fresh context.
30
+ 4. Persist events, runs, alerts, and status.
31
+ 5. Pause on repeated failure.
46
32
 
47
- - **No `AskUserQuestion`**: no human can answer. If you genuinely cannot
48
- proceed without a decision, write an entry to `${HOME}/.shared/roll/loop/ALERT-<slug>.md`
49
- describing what's needed and exit cleanly.
50
- - **Avoid compound bash**: each `Bash` call must run a single command.
51
- No `cmd1 && cmd2`, no `cmd1 ; cmd2`, no pipes (`|`), no `$(...)` /
52
- backtick subshells, no `bash -c '...'` with nested quoting. These are
53
- rejected by static analysis before they run. Chain operations as
54
- separate Bash calls and read intermediate output yourself.
55
- - **Prefer Read/Edit over cat/sed**: use the `Read` tool for any file
56
- lookup, `Edit` for modifications. They cross sandbox boundaries that
57
- `cat` / `ls` / `sed` cannot.
58
- - **CWD-relative paths first**: the cycle's CWD is the per-cycle worktree.
59
- Files inside it (.roll/backlog.md, bin/roll, tests/, docs/) are always
60
- accessible. Files at `~/.shared/roll/...` are reachable via the `Read`
61
- tool but not via shell commands.
62
- - **Quote every glob**: the `Bash` tool runs commands through the user's
63
- login shell, which on macOS is typically `zsh`. zsh's default `nomatch`
64
- aborts unquoted globs that find no match with `(eval):1: no matches
65
- found: <pattern>` and exit 1, burning a turn on a meaningless error.
66
- Quote literal globs (`ls 'tests/integration/helpers.*'`) or — better —
67
- use the `Glob` tool, which is shell-agnostic and never aborts on empty
68
- matches.
69
- - **Skill invocation is the work**: route US/REFACTOR via `$roll-build`,
70
- FIX via `$roll-fix`. Do not try to re-implement those flows inline.
33
+ ## Hard Gates
71
34
 
72
- ## Configuration
35
+ - Loop never cuts a release autonomously.
36
+ - Fail-loud and PAUSE beat silent fallback.
73
37
 
74
- ```yaml
75
- # ~/.roll/config.yaml
76
- loop:
77
- primary_agent: claude # claude | deepseek | kimi | pi | ...
78
- max_items_per_run: 1 # one story per cycle — atomic delivery, predictable cycle time
79
- brief_on_feature_complete: true
80
- retry_backoff: [2, 4, 8, 16] # seconds, exponential
81
- ```
38
+ ## Gotchas
82
39
 
83
- ## Workflow
40
+ - Loop dispatches backlog items; it must not merge releases or bypass human-on-the-loop decisions.
41
+ - Fail-loud pause behavior is preferable to silent fallback when repeated execution breaks.
84
42
 
85
- > **One story per cycle (强约束)**: 每个 cycle 只 pick 一个 Todo、跑完 Step 4
86
- > 立刻退出,不再回 Step 2 找下一个。理由:
87
- > - cycle 时间可预测(不会因贪心一连串 PR 撞 45 分钟 hard timeout)
88
- > - PR / events / dashboard 每行一个故事,归因清晰
89
- > - 一个故事一个 PR 一次 review,blast radius 最小
90
- >
91
- > 唯一例外是依赖修复(CI self-heal 等)已经内嵌在当前故事的 Step 4 里——
92
- > 那部分不算"新挑故事"。
93
- >
94
- > 实现层:max_items_per_run 默认 1,executor skill 跑完 Step 5 必须 exit。
95
- > 不要在同一个 cycle 内多次 emit `pick_todo` 事件。
43
+ ## Maintenance
96
44
 
97
- ### Step 1 Orphan 🔨 Recovery
98
-
99
- Process-level crash recovery (LOCK, heartbeat, retry budget) is handled by
100
- the runner in `bin/roll:_write_loop_runner_script` — the per-project LOCK
101
- guarantees only one cycle for this slug is alive when you start. So at
102
- this point, any `🔨 In Progress` row in `.roll/backlog.md` belongs to a
103
- previous cycle that crashed before flipping it back; reclaim it before
104
- scanning.
105
-
106
- 1. Scan .roll/backlog.md for all rows whose Status column contains `🔨 In Progress`.
107
- 2. For each such row: revert the status back to
108
- `📋 Todo`, commit `chore: revert orphan 🔨 US-XXX to 📋`, and append
109
- a line to `~/.shared/roll/loop/ALERT-<slug>.md` recording the orphan
110
- id and time so the next brief surfaces it.
111
- 3. After orphan sweep, proceed to Step 1.5 (Pre-run CI health check) before scanning.
112
-
113
- ### Step 1.5 — Pre-run CI Health Check
114
-
115
- Call `roll loop precheck-ci` before scanning BACKLOG. This is a **defensive gate**
116
- against building on a broken base. Check the **exit code** and route accordingly:
117
-
118
- | Exit code | Meaning | Action |
119
- |-----------|---------|--------|
120
- | `0` | CI green / pending / unknown | Proceed to Step 1.6 (PR Inbox) and Step 2 (BACKLOG scan) |
121
- | `1` | CI red AND heal exhausted or `ROLL_LOOP_NO_HEAL=1` | ALERT already written; exit cleanly this cycle |
122
- | `2` | CI red AND heal attempt allowed (US-LOOP-046) | **Hot-fix path** — skip BACKLOG, fix CI instead (see below) |
123
-
124
- `gh` missing or repo unparseable → `precheck-ci` returns `0`; graceful skip.
125
-
126
- **Hot-fix path (exit code 2) — US-LOOP-046:**
127
-
128
- Do NOT pick any BACKLOG stories this cycle. Instead:
129
-
130
- 1. Capture context: `roll loop hotfix-head-context` → prints path to context log
131
- 2. Invoke `Skill("roll-fix")` with brief:
132
- `"CI red on HEAD. Failing run logs at <context-log-path>. Diagnose root cause, fix via TCR, commit, push. Do NOT change BACKLOG status."`
133
- 3. After `roll-fix` completes, re-run `roll ci --wait` to verify the fix
134
- 4. If CI is still red: run `roll loop precheck-ci` again; if it returns `1` (heal exhausted),
135
- exit cleanly — ALERT was already written by the precheck
136
-
137
- ### Step 1.6 — PR Inbox (US-AUTO-034)
138
-
139
- Before scanning BACKLOG, process open PRs first. PRs are also units of work:
140
- external contributors and human teammates expect their PRs to be reviewed and
141
- moved forward, not starved while loop opens new fronts.
142
-
143
- Call `_loop_pr_inbox` after the pre-run CI check passes. It walks
144
- `gh pr list --state open` and routes each PR by classification:
145
-
146
- | Classification | Action |
147
- |---|---|
148
- | `loop_self` (head ref starts with `loop/` **or** `claude/`, CI not red) | `_loop_pr_merge_self_eager` — squash-merge directly when CI green + clean; if the PR is BEHIND/CONFLICTING with main, `_loop_pr_rebase_stale` rebases it first (circuit-gated) so it merges on a later tick. Never AI-review your own commit. (Agent-authored `claude/*` PRs are loop-owned the same way; a CI-red `claude/*` PR is **not** auto-healed — it falls through for a human to decide.) |
149
- | `loop_self_ci_red` (loop/* PR whose CI went red) | **US-LOOP-062a**: `_loop_pr_heal_self` — background-heal (per-PR lock + heal budget `ROLL_LOOP_HEAL_MAX`, default 2, via `_project_agent`); on `ROLL_LOOP_NO_HEAL=1` / budget exhausted → deduped `[TYPE:loop-pr-ci-red]` ALERT (never silently dropped) |
150
- | `blocked_human_request_changes` | Skip — last human review requested changes; wait for the author to push fixes |
151
- | `blocked_human_approved` | **US-LOOP-062b**: `_loop_pr_merge_approved` — merge directly (`gh pr merge --squash`) when CI green + mergeable, instead of relying on repo auto-merge (which may be off); merge failure is non-fatal (retried next tick) |
152
- | `stale` (CI failed or branch behind/conflicting) | Try `_loop_pr_rebase_stale` after the circuit breaker allows it |
153
- | `eligible` (clean external PR, no blocking review) | Invoke `_loop_pr_review_external` — the actual decision is provided by US-AUTO-035's GitHub Action |
154
-
155
- **Rebase circuit breaker** — `_loop_pr_rebase_circuit <pr>` records each rebase
156
- attempt under `pr_state.<PR>.attempts_at` in the per-slug state file
157
- (`~/.shared/roll/loop/state-<slug>.yaml`, FIX-052), pruning entries older
158
- than 24 h. Once ≥3 attempts land within 24 h, further rebases are blocked and an
159
- ALERT is written (typical cause: a broken workflow file makes CI never run,
160
- which would otherwise drive infinite rebase loops).
161
-
162
- **Lenient on infrastructure** — `gh` missing, repo unparseable, or any
163
- `gh` API failure → `_loop_pr_inbox` returns 0 and the loop falls through to
164
- Step 2 (BACKLOG scan). Same posture as the pre-run CI check.
165
-
166
- ### Step 2 — Scan BACKLOG
167
-
168
- Read `.roll/backlog.md`. Collect all rows where Status = `📋 Todo`, in order:
169
-
170
- Priority: FIX-XXX first (bugs block progress), then US-XXX, then REFACTOR-XXX.
171
-
172
- **Skip rows with Status = `🔨 In Progress`**. These are currently being executed by:
173
- - Another concurrent executor (human via `$roll-build`, peer agent)
174
- - An earlier loop iteration that hasn't finished yet (rare; should be guarded by LOCK)
175
- - A previous interrupted run (the resume logic in Step 1 will pick these up)
176
-
177
- **In-flight PR gate** (FIX-048). Before picking, also exclude stories already
178
- claimed by an **open `loop/*` PR**. Each cycle's worktree is branched from
179
- `origin/main`, so a story another cycle has marked 🔨 In Progress is invisible
180
- locally until that cycle's PR merges. Without this gate, two cycles started
181
- back-to-back will both pick the same Todo row and produce duplicate PRs.
182
-
183
- ```bash
184
- bash -c 'source "$(command -v roll)"; _loop_pr_claimed_stories'
185
- # stdout: one story ID per line (deduped) — these are claimed by open
186
- # loop/* PRs on the remote. SKIP any candidate whose ID appears.
187
- # exit 0 always (lenient: gh missing / API error → empty output).
188
- ```
189
-
190
- **Dependency gate** (FIX-032). For each `📋 Todo` candidate, before picking:
191
-
192
- ```bash
193
- # Source bin/roll once per cycle, then call the helper per candidate.
194
- source "$(command -v roll)"
195
-
196
- bash -c 'source "$(command -v roll)"; _loop_check_depends_on "<story-id>" .roll/backlog.md'
197
- # exit 0 → all `depends-on:US-X,US-Y` are ✅ Done → eligible
198
- # exit 1 → stdout lists unsatisfied dep IDs; SKIP this story, log to
199
- # runs.jsonl `skipped` with reason "depends-on: <unsatisfied>"
200
- ```
201
-
202
- Move to the next candidate when skipping. The gate is a pure function
203
- over .roll/backlog.md text — no side effects, no LOCK interaction.
204
-
205
- Cap at `max_items_per_run` to limit blast radius per cycle.
206
-
207
- ### Concurrency Safety
208
-
209
- Loop has two layers of concurrency protection:
210
-
211
- 1. **Per-project LOCK** (enforced by runner script, see `bin/roll:_write_loop_runner_script`):
212
- - LOCK file path: `~/.shared/roll/loop/.LOCK-<project-slug>`
213
- - On launch: if LOCK exists and the PID inside is alive → exit 0 (previous loop still running)
214
- - On launch: if LOCK exists but PID is dead → clean up stale LOCK and continue
215
- - On exit (normal or via trap): LOCK is removed
216
- - One LOCK per project — different projects' loops run independently
217
-
218
- 2. **🔨 In Progress story status** (enforced here):
219
- - Before picking a story, check its status is `📋 Todo`
220
- - Skip any `🔨 In Progress` row (someone else is on it)
221
- - Mark each story `🔨 In Progress` BEFORE invoking the executor skill (see Step 3)
222
- - On completion: update to `✅ Done`; on TCR failure: revert to `📋 Todo`
223
-
224
- 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).
225
-
226
- ### Step 3 — Route and Execute
227
-
228
- > **US-AGENT-006 — Per-story agent routing (pre-cycle)**
229
- >
230
- > Before this skill even starts, the runner inner script has already:
231
- > 1. Picked the next eligible Todo via `_loop_pick_next_story` (priority FIX > US > REFACTOR, depends-on gate respected)
232
- > 2. Read its Agent profile (est_min / risk_zone) and routed an agent via `_loop_pick_agent_for_story` (hard rules from `.roll/agent-routes.yaml` + soft preference from `runs.jsonl`)
233
- > 3. Exported `ROLL_LOOP_ROUTED_STORY` / `ROLL_LOOP_ROUTED_AGENT` / `ROLL_LOOP_ROUTED_RULE` and printed `[loop] story <id> routed to <agent> via <rule_kind>` to cron.log
234
- >
235
- > When `ROLL_LOOP_ROUTED_STORY` is set, prefer it as `US_ID` for this cycle. The story has already been chosen by hard+soft routing rules — and, per FIX-146, the runner re-validates it against the authoritative backlog right before handing it to you (re-picking the next eligible Todo if it went ✅ Done / In Progress / ineligible between pick and handoff, emitting a `story_stale` event). So treat `ROLL_LOOP_ROUTED_STORY` as already-eligible and just work it. Only if you still find at cycle start that it is no longer 📋 Todo in BACKLOG (a residual concurrent flip), re-pick the next eligible Todo via `_loop_pick_next_story` rather than idling the whole cycle.
236
- >
237
- > Old single-agent fallback (`primary_agent` from `~/.roll/config.yaml`) still applies when:
238
- > - no story is pickable (empty Todo / all blocked by depends-on)
239
- > - the matching agent-routes.yaml has no agent that fits the story profile (then `cold_start_default` is used)
240
-
241
- For each item, **before invoking the executor skill**, mark the story 🔨 In Progress in the **main repo's** .roll/backlog.md so brief and peer agents can see it being worked on. The cycle worktree is gitignored at .roll/, so editing the worktree's own copy + committing carries no change back to main — write directly via the helper instead:
242
-
243
- ```bash
244
- bash -c 'source "$(command -v roll)"; _loop_mark_in_progress US-XXX'
245
- # Updates ${ROLL_MAIN_PROJECT}/.roll/backlog.md in place: flips the row
246
- # containing US-XXX from "📋 Todo" to "🔨 In Progress". Idempotent.
247
- ```
248
-
249
- If the executor fails (TCR aborts, CI red, etc.), revert the marker so the next cycle can re-pick the story:
250
-
251
- ```bash
252
- bash -c 'source "$(command -v roll)"; _loop_mark_todo US-XXX'
253
- ```
254
-
255
- Status flips happen in main directly — no per-cycle commit needed. `roll-brief` reads main's backlog, so the 🔨 marker is visible the moment the helper returns.
256
-
257
- 选定故事后,调用 `_loop_event` 发出 pick_todo 事件,让 dashboard / monitor / attach 都能把"这个 cycle 选了哪个 story"正确归类:
258
-
259
- ```bash
260
- # 选定故事后立即 emit(在调用 executor skill 之前)
261
- # label 必须是 cycle_id(来自 bin/roll 注入的 LOOP_CYCLE_ID 环境变量),
262
- # 不是 US_ID — dashboard 按 label 聚类,US_ID 当 label 会让事件分到错的桶
263
- # 里,cycle 看起来"有 token 没 ID"。
264
- _loop_event pick_todo "$LOOP_CYCLE_ID" "$US_ID" ""
265
- ```
266
-
267
- Then invoke the executor:
268
-
269
- ```
270
- Item type → Skill invoked
271
- ─────────────────────────────────
272
- US-XXX → Skill("roll-build", "US-XXX")
273
- FIX-XXX → Skill("roll-fix", "FIX-XXX")
274
- REFACTOR-XXX → Skill("roll-build", "REFACTOR-XXX")
275
- ```
276
-
277
- The executor will update the row to `✅ Done` on success (it transitions from `🔨 In Progress` → `✅ Done`, same Edit logic as from `📋 Todo`).
278
-
279
- Before invoking, also write current item to the per-slug state file
280
- (`~/.shared/roll/loop/state-<slug>.yaml`, FIX-052):
281
-
282
- ```yaml
283
- status: running
284
- current_item: US-AUTO-004
285
- started_at: "2026-05-10T02:00:00+08:00"
286
- agent: claude
287
- run_id: loop-20260510-0200
288
- ```
289
-
290
- ### Step 4 — Post-Item Cleanup
291
-
292
- After each item completes:
293
-
294
- 1. **TCR 硬校验** — call `roll loop enforce-tcr <story_id> <started_at>`:
295
- - Count `tcr:` prefix commits since `started_at` via `git log --oneline --since=<started_at>`
296
- - Count == 0 → revert story status in .roll/backlog.md from ✅ Done → 📋 Todo; write ALERT to `~/.shared/roll/loop/ALERT-<slug>.md` with story ID, time, reason "zero tcr: commits since story start", and suggested actions (`roll loop now` / `$roll-build <id>` / `roll loop reset`)
297
- - Count > 0 → continue normally
298
- 2. **CI Gate** — **MUST** invoke `roll ci --wait` (the `_loop_enforce_ci`
299
- wrapper). **Do NOT call `gh` directly** (no `gh run list`, no `gh run watch`,
300
- no ad-hoc shell checks): `roll ci --wait` is the only sanctioned entry —
301
- it derives `owner/repo` from the git remote and uses `gh -R <slug>`, which
302
- is required to work through `~/.ssh/config` host rewrites that break gh's
303
- auto-detection.
304
- - CI passes → clear any `heal_count:` entry in `~/.shared/roll/loop/state-<slug>.yaml` (idempotent — drop the line if present, no-op otherwise) and continue normally
305
- - CI fails / times out / `gh` call fails → enter **CI self-heal** (US-AUTO-041)
306
- - `gh` binary not installed (`command -v gh` fails) → skip gracefully
307
- (return 0). Any other `gh` error is **not** "gh unavailable" — it is a
308
- hard failure and must block the gate.
309
-
310
- **CI self-heal (US-AUTO-041)** — bounded auto-fix before ALERT.
311
-
312
- Read `heal_count:` from `~/.shared/roll/loop/state-<slug>.yaml`; treat a missing line as `0`. If the count is below `ROLL_LOOP_HEAL_MAX` (default 2) and `ROLL_LOOP_NO_HEAL` is not set, increment it and take Path A. Otherwise take Path B.
313
-
314
- **Path A — attempt allowed (counter incremented in `state-<slug>.yaml`):**
315
-
316
- 1. Capture failure summary:
317
- ```
318
- gh run view --log-failed --repo <slug> \
319
- $(gh run list --commit HEAD --json databaseId,conclusion -L 5 \
320
- | jq -r '.[] | select(.conclusion=="failure") | .databaseId' | head -1) \
321
- 2>/dev/null | head -200 > /tmp/roll-heal-<story_id>.log
322
- ```
323
- 2. Invoke `Skill("roll-fix")` with brief:
324
- `"CI red after <story_id>. Failing run logs at /tmp/roll-heal-<story_id>.log.
325
- Diagnose root cause, fix via TCR, commit, push. Do NOT change <story_id>'s
326
- BACKLOG status — it stays ✅ Done. The fix is a follow-up."`
327
- 3. After `roll-fix` completes, return to step 2 (CI Gate) — re-run `roll ci --wait`.
328
- The counter in `state-<slug>.yaml` prevents infinite loops.
329
-
330
- **Path B — heal exhausted (≥`ROLL_LOOP_HEAL_MAX`, default 2) or disabled (`ROLL_LOOP_NO_HEAL=1`) (exit 1):**
331
-
332
- 1. Do NOT force ✅ Done here. CI red means the PR will not merge. Under
333
- **US-AUTO-044** the main loop no longer waits for merge — it publishes the
334
- PR and exits; the dedicated PR Loop (`com.roll.pr.<slug>`, every 5 min)
335
- merges / rebases / closes it asynchronously. There is no false-Done risk:
336
- with worktree isolation the ✅ Done lives only in the unmerged PR, never on
337
- the loop's main checkout, and the story is not re-picked meanwhile via the
338
- open-PR eligibility gate (`_loop_story_is_eligible`, FIX-146). The story's
339
- ✅ Done lands on main only when the PR Loop actually merges the PR.
340
- 2. Write ALERT to `~/.shared/roll/loop/ALERT-<slug>.md` with:
341
- - story ID, time, commit SHA
342
- - heal attempts made (read `heal_count:` from `state-<slug>.yaml`)
343
- - last failure summary (head of `/tmp/roll-heal-<story_id>.log`)
344
- - suggested actions: `$roll-fix` manually / inspect CI / `roll loop reset`
345
- 3. Skip to next story.
346
-
347
- **Bypass for debugging / cost control:** set `ROLL_LOOP_NO_HEAL=1` to restore
348
- pre-US-AUTO-041 fail-fast behaviour.
349
- 3. Update state file: `status: idle`
350
- 4. Check if a Feature is now fully complete (all its Stories ✅)
351
- 5. If yes and `brief_on_feature_complete: true` → invoke `Skill("roll-brief")`
352
- 6. **EXIT the cycle.** 不要回 Step 2 找下一个故事,不要再 emit `pick_todo`。
353
- 一个 cycle 只交付一个故事;剩下的 Todo 等下一个 launchd tick 起新 cycle 处理。
354
-
355
- ### Step 5 — Write Run Summary
356
-
357
- > **FIX-044**: The inner runner script (`_write_loop_runner_script` in `bin/roll`)
358
- > now appends this record deterministically at cycle end. The shell write is the
359
- > authoritative record; the agent should still emit a run summary in the cycle's
360
- > final report for `cron.log` visibility.
361
-
362
- After all items in this cycle:
363
-
364
- ```yaml
365
- # ~/.shared/roll/loop/state-<slug>.yaml (FIX-052)
366
- status: idle
367
- last_run: "2026-05-10T02:15:00+08:00"
368
- last_run_items: [US-AUTH-003, FIX-007]
369
- last_run_outcome: success
370
- ```
371
-
372
- Then append a JSONL record to `~/.shared/roll/loop/runs.jsonl` for per-iteration
373
- visibility (one line per cycle, append-only — never delete or rewrite earlier lines).
374
-
375
- **⚠️ Strict schema contract — do NOT deviate.** Every field has exactly one
376
- canonical form. Synonyms like `"success"`, `"noop"`, `"completed"` are forbidden
377
- for `status`. Numbers and arrays cannot be interchanged. UTC `Z` suffix only,
378
- no timezone offsets. **No extra fields** — emit only the keys listed below (plus
379
- optional `reason` when `status="failed"`); do not add `note`, `comment`,
380
- `details`, `info`, etc. If you feel the urge to annotate, put it in the cycle's
381
- final report in `cron.log` instead.
382
-
383
- **Canonical record (copy this exact shape, fill in real values):**
384
-
385
- ```json
386
- {"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}
387
- ```
388
-
389
- **Field contract — types are enforced**:
390
-
391
- | Field | Type | Format / Enum |
392
- |---|---|---|
393
- | `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. |
394
- | `project` | string | Project **slug** only (e.g. `roll-d9dfa0`), NOT the absolute path and NOT plain `basename`. Compute via: `p=$(pwd -P); base=$(basename "$p" | tr -cs '[:alnum:]' '-' | sed 's/-*$//'); hash=$(printf '%s' "$p" | md5 | cut -c1-6 2>/dev/null || printf '%s' "$p" | md5sum | cut -c1-6); echo "${base}-${hash}"` |
395
- | `run_id` | string | Matches `state-<slug>.yaml` `run_id` exactly. Format: `loop-YYYYMMDD-HHMM`. |
396
- | `status` | enum | Exactly one of: `built` (≥1 story shipped), `idle` (no Todo items found), `failed` (paused/error). **No synonyms.** |
397
- | `built` | array&lt;string&gt; | Story ids completed this cycle. `[]` when none. **Always array, never null/number.** |
398
- | `skipped` | array&lt;string&gt; | Story ids skipped because they were `🔨 In Progress`. `[]` when none. **Always array.** |
399
- | `alerts` | array&lt;string&gt; | Newly raised ALERT identifiers/tags this cycle. `[]` when none. **Always array, never number.** |
400
- | `tcr_count` | integer | Total `tcr:` prefix commits made this cycle. `0` when none. |
401
- | `duration_sec` | integer | Seconds from cycle start to completion. Integer only, no decimals. |
402
-
403
- Optional field, only when `status == "failed"`:
404
- - `reason` (string): short human-readable explanation.
405
-
406
- **Write recipe:**
407
-
408
- ```bash
409
- ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
410
- # Compute project slug — inlined equivalent of bin/roll's _project_slug
411
- # (Claude sessions can't call roll's internal functions, so we inline).
412
- # Must produce identical output to _project_slug to match `roll loop runs` filter.
413
- _p=$(pwd -P)
414
- _base=$(basename "$_p" | tr -cs '[:alnum:]' '-' | sed 's/-*$//')
415
- _hash=$(printf '%s' "$_p" | md5 | cut -c1-6 2>/dev/null || printf '%s' "$_p" | md5sum | cut -c1-6)
416
- project="${_base}-${_hash}" # e.g. roll-d9dfa0 — must match roll loop runs filter
417
- # duration_sec = cycle_end_epoch - cycle_start_epoch (track at Step 1)
418
- # tcr_count = git log --oneline --since="<cycle_start>" | grep -c '^[a-f0-9]* tcr:'
419
-
420
- jq -nc \
421
- --arg ts "$ts" \
422
- --arg project "$project" \
423
- --arg run_id "$run_id" \
424
- --arg status "built" \
425
- --argjson built '["US-AUTO-024"]' \
426
- --argjson skipped '[]' \
427
- --argjson alerts '[]' \
428
- --argjson tcr_count 14 \
429
- --argjson duration_sec 1680 \
430
- '{ts:$ts, project:$project, run_id:$run_id, status:$status,
431
- built:$built, skipped:$skipped, alerts:$alerts,
432
- tcr_count:$tcr_count, duration_sec:$duration_sec}' \
433
- >> ~/.shared/roll/loop/runs.jsonl
434
- ```
435
-
436
- The companion read-side is `roll loop runs [N] [--all]` — shows the most recent
437
- N records (default 10) for the current project, or across all projects with `--all`.
438
-
439
- ## Failure Handling
440
-
441
- ### Network Error (transient)
442
-
443
- ```
444
- Attempt 1 fails
445
- → wait 2s → Attempt 2
446
- → wait 4s → Attempt 3
447
- → wait 8s → Attempt 4
448
- → wait 16s → Attempt 5
449
- → still failing → escalate to token/agent failure path
450
- ```
451
-
452
- ### Token Exhausted / Agent Unavailable
453
-
454
- ```
455
- Primary agent fails (non-network error)
456
- → 3 attempts at the agent_invoke phase (with 30s back-off between)
457
- → still failing → PAUSE
458
- ```
459
-
460
- ### Pause + Alert
461
-
462
- When the primary agent exhausts its retry budget:
463
-
464
- 1. Write state:
465
- ```yaml
466
- status: paused
467
- paused_at: "2026-05-10T02:07:00+08:00"
468
- paused_on: US-AUTH-003
469
- reason: "primary agent (claude) unavailable after 3 attempts"
470
- ```
471
-
472
- 2. Write alert:
473
- ```markdown
474
- # ALERT — roll-loop paused
475
-
476
- **Time**: 2026-05-10 02:07
477
- **Paused on**: US-AUTH-003
478
- **Reason**: claude exited non-zero on 3 consecutive attempts
479
-
480
- **Action required** (choose one):
481
- - Top up credits and run: `roll loop resume`
482
- - Switch agent: edit `~/.roll/config.yaml` → `primary_agent`
483
- - Take over manually: `$roll-build US-AUTH-003`
484
- ```
485
-
486
- 3. Write alert file to `~/.shared/roll/loop/ALERT-<slug>.md`
487
-
488
- ## Resuming After Pause
489
-
490
- ```bash
491
- roll loop resume # picks up from state-<slug>.yaml current_item
492
- roll loop status # show current state without running
493
- roll loop reset # clear state and start fresh next scheduled run
494
- ```
495
-
496
- ## Scheduler Configuration
497
-
498
- roll-loop runs **locally** — it needs access to the local codebase, local
499
- test runner, and local agent CLI. GitHub Actions runs on remote servers and
500
- cannot fulfill these requirements.
501
-
502
- ### Local cron (default)
503
-
504
- Install once with `roll loop on` — agent selection happens per cycle from the
505
- `.roll/agents.yaml` complexity slots (easy/default/hard/fallback), so the cron
506
- entry is agent-agnostic. No agent-specific command needed.
507
-
508
- ```bash
509
- roll loop on # install cron for loop + dream + brief
510
- roll loop off # remove cron entries
511
- roll loop status # show current state
512
- ```
513
-
514
- ### Manual run (for testing)
515
-
516
- ```bash
517
- roll loop now # execute one cycle immediately
518
- ```
519
-
520
- ### Live attach (transparency)
521
-
522
- Each loop iteration runs inside a detached tmux session named
523
- `roll-loop-<slug>` (tmux is a required dependency — `roll setup` auto-installs
524
- it via Homebrew on macOS, or prints the install command elsewhere).
525
-
526
- **Default — auto-attach popup**: when the loop fires, a background Terminal
527
- window pops up running `tmux attach -t roll-loop-<slug>`. You can watch the
528
- agent work in real time without typing anything. The popup is best-effort
529
- focus-retaining (it captures the previously-active app and restores focus
530
- after the window appears) and the tmux session keeps running even if you
531
- close the window.
532
-
533
- **Manual attach** (any time):
534
-
535
- ```bash
536
- roll loop attach # exec tmux attach -t roll-loop-<slug>
537
- ```
538
-
539
- Press `Ctrl-B D` to detach — the loop continues running uninterrupted.
540
-
541
- **Mute / unmute the popup**:
542
-
543
- ```bash
544
- roll loop mute # 🔇 — suppress auto-attach popup (loop still runs in tmux)
545
- roll loop unmute # 🔔 — re-enable the popup
546
- ```
547
-
548
- Mute state is a single marker file at `~/.shared/roll/mute` and is shared
549
- across all projects on this machine. Check the current state with
550
- `roll loop status` — it shows an `Auto-attach: live | muted` line.
551
-
552
- ## Integration Map
553
-
554
- ```
555
- roll-loop
556
- ├── reads .roll/backlog.md
557
- ├── invokes $roll-build (US-XXX, REFACTOR-XXX)
558
- ├── invokes $roll-fix (FIX-XXX)
559
- ├── invokes $roll-brief (on Feature completion)
560
- ├── reads ~/.roll/config.yaml (agent routing)
561
- ├── writes ~/.shared/roll/loop/state-<slug>.yaml
562
- └── writes ~/.shared/roll/loop/ALERT-<slug>.md (on failure)
563
- ```
45
+ - Description changes require updates in `route-cases/skills.json`.
46
+ - New observed failures should add a gotcha and the matching positive or negative route case.
47
+ - Heavy examples, templates, recovery paths, and deterministic snippets belong in `references/`, `assets/`, or `scripts/`, not in this hub.