@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.
- package/CHANGELOG.md +44 -1
- package/README.md +5 -6
- package/dist/roll.mjs +16146 -16186
- package/package.json +3 -2
- package/skills/README.md +14 -1
- package/skills/docs/skill-authoring.md +66 -0
- package/skills/reports/skill-audit-summary.md +53 -0
- package/skills/roll-.changelog/SKILL.md +25 -443
- package/skills/roll-.changelog/references/full-contract.md +462 -0
- package/skills/roll-.clarify/SKILL.md +6 -4
- package/skills/roll-.dream/SKILL.md +26 -353
- package/skills/roll-.dream/references/full-contract.md +365 -0
- package/skills/roll-.echo/SKILL.md +6 -4
- package/skills/roll-.qa/SKILL.md +25 -236
- package/skills/roll-.qa/references/full-contract.md +256 -0
- package/skills/roll-.review/SKILL.md +6 -2
- package/skills/roll-brief/SKILL.md +6 -8
- package/skills/roll-build/SKILL.md +28 -864
- package/skills/roll-build/references/full-contract.md +883 -0
- package/skills/roll-debug/SKILL.md +26 -585
- package/skills/roll-debug/references/full-contract.md +607 -0
- package/skills/roll-design/SKILL.md +28 -903
- package/skills/roll-design/references/full-contract.md +923 -0
- package/skills/roll-doc/SKILL.md +25 -574
- package/skills/roll-doc/references/full-contract.md +594 -0
- package/skills/roll-doctor/SKILL.md +21 -2
- package/skills/roll-fix/SKILL.md +28 -621
- package/skills/roll-fix/references/full-contract.md +640 -0
- package/skills/roll-idea/SKILL.md +6 -2
- package/skills/roll-loop/SKILL.md +27 -543
- package/skills/roll-loop/references/full-contract.md +555 -0
- package/skills/roll-notes/SKILL.md +6 -2
- package/skills/roll-onboard/SKILL.md +6 -2
- package/skills/roll-peer/SKILL.md +27 -316
- package/skills/roll-peer/references/full-contract.md +329 -0
- package/skills/roll-propose/SKILL.md +6 -8
- package/skills/roll-review-pr/SKILL.md +6 -2
- package/skills/roll-sentinel/SKILL.md +26 -344
- package/skills/roll-sentinel/references/full-contract.md +363 -0
- package/skills/roll-spar/SKILL.md +27 -269
- package/skills/roll-spar/references/full-contract.md +288 -0
- package/skills/route-cases/skills.json +235 -0
- package/skills/scripts/audit-skills.mjs +272 -0
- package/skills/scripts/test-audit-skills.mjs +39 -0
- package/skills/tests/fixtures/skill-audit/block-skill/SKILL.md +12 -0
- package/skills/tests/fixtures/skill-audit/minimal-skill/SKILL.md +8 -0
- package/skills/tests/fixtures/skill-audit/quoted-skill/SKILL.md +10 -0
- package/skills/tests/fixtures/skill-audit/route-cases.json +21 -0
- package/skills/tests/fixtures/skill-audit/spoke-skill/SKILL.md +12 -0
- package/skills/tests/fixtures/skill-audit/spoke-skill/references/runbook.md +3 -0
- package/bin/roll +0 -15361
- package/lib/backfill-pi-usage.py +0 -243
- package/lib/changelog_audit.py +0 -149
- package/lib/changelog_generate.py +0 -470
- package/lib/consistency_check.py +0 -409
- package/lib/context_feed_budget.sh +0 -194
- package/lib/github_sync.py +0 -876
- package/lib/i18n/slides.sh +0 -3
- package/lib/i18n/slides_build.sh +0 -38
- package/lib/i18n/slides_delete.sh +0 -19
- package/lib/i18n/slides_list.sh +0 -14
- package/lib/i18n/slides_logs.sh +0 -12
- package/lib/i18n/slides_new.sh +0 -15
- package/lib/i18n/slides_preview.sh +0 -14
- package/lib/i18n/slides_templates.sh +0 -7
- package/lib/i18n.sh +0 -211
- package/lib/loop-exit-summary.py +0 -393
- package/lib/loop-fmt.py +0 -589
- package/lib/loop_pick_agent.py +0 -316
- package/lib/loop_result_eval.py +0 -469
- package/lib/loop_unstick.py +0 -180
- package/lib/model_prices.py +0 -194
- package/lib/prices_fetcher.py +0 -534
- package/lib/roll-backlog.py +0 -225
- package/lib/roll-brief.py +0 -286
- package/lib/roll-help.py +0 -158
- package/lib/roll-home.py +0 -556
- package/lib/roll-init.py +0 -156
- package/lib/roll-loop-status.py +0 -1691
- package/lib/roll-loop-story.py +0 -191
- package/lib/roll-peer.py +0 -252
- package/lib/roll-setup.py +0 -102
- package/lib/roll-status.py +0 -367
- package/lib/roll_git.py +0 -41
- package/lib/roll_render.py +0 -414
- package/lib/slides/components/README.md +0 -123
- package/lib/slides/components/cards-2.html +0 -9
- package/lib/slides/components/cards-3.html +0 -9
- package/lib/slides/components/cards-4.html +0 -9
- package/lib/slides/components/compare.html +0 -22
- package/lib/slides/components/highlight.html +0 -9
- package/lib/slides/components/pipeline.html +0 -12
- package/lib/slides/components/plain.html +0 -7
- package/lib/slides/components/quote.html +0 -4
- package/lib/slides/components/timeline.html +0 -9
- package/lib/slides/templates/introduction-v3.html +0 -571
- package/lib/slides/templates/pitch.html +0 -0
- package/lib/slides-render.py +0 -778
- package/lib/slides-validate.py +0 -357
- package/lib/test_quality_gate.py +0 -143
- package/skills/roll-deck/SKILL.md +0 -296
- /package/skills/roll-debug/{injectable-bb.js → assets/injectable-bb.js} +0 -0
- /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
|
-
|
|
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
|
-
|
|
18
|
-
> Common Sense defined in the project AGENTS.md.
|
|
11
|
+
## Load
|
|
19
12
|
|
|
20
|
-
|
|
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
|
-
##
|
|
15
|
+
## When Not to Use
|
|
25
16
|
|
|
26
|
-
|
|
27
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
##
|
|
25
|
+
## Workflow Skeleton
|
|
42
26
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
+
- Loop never cuts a release autonomously.
|
|
36
|
+
- Fail-loud and PAUSE beat silent fallback.
|
|
73
37
|
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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<string> | Story ids completed this cycle. `[]` when none. **Always array, never null/number.** |
|
|
398
|
-
| `skipped` | array<string> | Story ids skipped because they were `🔨 In Progress`. `[]` when none. **Always array.** |
|
|
399
|
-
| `alerts` | array<string> | 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.
|