@seanyao/roll 2026.519.2 → 2026.520.1
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 +39 -0
- package/README.md +40 -70
- package/bin/roll +639 -115
- package/conventions/global/AGENTS.md +3 -2
- package/lib/roll-backlog.py +1 -1
- package/lib/roll-help.py +1 -1
- package/lib/roll-peer.py +242 -0
- package/lib/roll_render.py +21 -0
- package/package.json +1 -1
- package/skills/roll-build/SKILL.md +2 -2
- package/skills/roll-loop/SKILL.md +21 -10
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_render.cpython-314.pyc +0 -0
|
@@ -60,8 +60,9 @@
|
|
|
60
60
|
- Before pushing any new code commit, verify the **previous** code-changing push's CI
|
|
61
61
|
is green. Never stack new code commits on top of a red CI (this is the failure
|
|
62
62
|
mode FIX-026 / `_loop_precheck_ci` exists to prevent for the loop — humans need
|
|
63
|
-
the same discipline).
|
|
64
|
-
|
|
63
|
+
the same discipline). Every commit now triggers CI (US-POS-006 removed the
|
|
64
|
+
`paths-ignore` allow-list), so doc-only commits run CI too; treat their result
|
|
65
|
+
the same way as any other push.
|
|
65
66
|
- If CI is red, the next action is **fix or revert**, not "queue something else".
|
|
66
67
|
- **Commit message format**:
|
|
67
68
|
- Format: `<type>: <description>` (Git Hook may auto-prepend type prefix)
|
package/lib/roll-backlog.py
CHANGED
|
@@ -244,7 +244,7 @@ def _write_demo(path: str) -> None:
|
|
|
244
244
|
### Feature: autonomous-evolution
|
|
245
245
|
| Story | Description | Status |
|
|
246
246
|
|-------|-------------|--------|
|
|
247
|
-
| [US-AUTO-042](.roll/features/autonomous-evolution.md) | Loop cost telemetry — write model and token data per cycle | 📋 Todo |
|
|
247
|
+
| [US-AUTO-042](.roll/features/autonomous-evolution/autonomous-evolution.md) | Loop cost telemetry — write model and token data per cycle | 📋 Todo |
|
|
248
248
|
|
|
249
249
|
## ♻️ Refactor
|
|
250
250
|
| ID | Description | Status |
|
package/lib/roll-help.py
CHANGED
|
@@ -45,7 +45,7 @@ AUTONOMY = [
|
|
|
45
45
|
]
|
|
46
46
|
|
|
47
47
|
PROJECT = [
|
|
48
|
-
("init", "", "create AGENTS.md + .roll/backlog.md +
|
|
48
|
+
("init", "", "create AGENTS.md + .roll/backlog.md + .roll/features/", "初始化项目工作流文件", False),
|
|
49
49
|
("status", "", "show current state and drift", "显示当前状态和漂移项", False),
|
|
50
50
|
("agent", "[use <name>]", "per-project agent selection", "切换项目 agent", False),
|
|
51
51
|
("ci", "[--wait]", "show or wait for current commit's CI status", "查看 / 等待 CI 状态", False),
|
package/lib/roll-peer.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""roll-peer — v2 terminal view for `roll peer` (US-VIEW-009).
|
|
3
|
+
|
|
4
|
+
Renders a cross-agent review log as a turn-based ROUND transcript:
|
|
5
|
+
eyebrow + subject + proposer/reviewer overview + ROUND N sections
|
|
6
|
+
(each carrying agent turns with weight chips) + final VERDICT line
|
|
7
|
+
+ artifact path / next-step hint.
|
|
8
|
+
|
|
9
|
+
NO_COLOR=1 falls through to glyph + weight + spacing only.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
_LIB_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
18
|
+
if _LIB_DIR not in sys.path:
|
|
19
|
+
sys.path.insert(0, _LIB_DIR)
|
|
20
|
+
import roll_render
|
|
21
|
+
from roll_render import c, row, COLS
|
|
22
|
+
|
|
23
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
24
|
+
# Agent palette — each agent gets a stable color so reviewer/proposer pairs
|
|
25
|
+
# read at a glance across rounds. Unknown agents fall back to fg.
|
|
26
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
27
|
+
|
|
28
|
+
_AGENT_COLOR = {
|
|
29
|
+
"claude": "blue",
|
|
30
|
+
"codex": "pink",
|
|
31
|
+
"kimi": "amber",
|
|
32
|
+
"deepseek": "green",
|
|
33
|
+
"gemini": "purple",
|
|
34
|
+
"pi": "yellow",
|
|
35
|
+
"opencode": "muted",
|
|
36
|
+
"trae": "fg",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Weight chip — (glyph, color, label) per turn.weight
|
|
40
|
+
_WEIGHTS = {
|
|
41
|
+
"concern": ("●", "amber", "concern"),
|
|
42
|
+
"nit": ("○", "dim", "nit"),
|
|
43
|
+
"ack": ("✓", "green", "ack"),
|
|
44
|
+
"block": ("✗", "red", "block"),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _agent_c(name: str) -> str:
|
|
49
|
+
return _AGENT_COLOR.get(name.lower(), "fg")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
53
|
+
# Demo data — illustrative cross-agent review: claude proposes, codex reviews
|
|
54
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
55
|
+
|
|
56
|
+
_DEMO_SUBJECT = {
|
|
57
|
+
"story": "US-AUTH-014",
|
|
58
|
+
"title": "Session refresh fallback when refresh-token API 5xx",
|
|
59
|
+
"pr": "#412",
|
|
60
|
+
"diff_stat": "+184 −37 · 6 files",
|
|
61
|
+
"trigger": "complexity=large",
|
|
62
|
+
"proposer": "claude",
|
|
63
|
+
"reviewer": "codex",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_DEMO_ROUNDS = [
|
|
67
|
+
{
|
|
68
|
+
"n": 1,
|
|
69
|
+
"hint": "first pass — proposer ships, reviewer probes",
|
|
70
|
+
"turns": [
|
|
71
|
+
("claude", "concern",
|
|
72
|
+
"Refresh path swallows 503 silently — caller sees a stale session "
|
|
73
|
+
"without any signal that re-auth is needed."),
|
|
74
|
+
("codex", "nit",
|
|
75
|
+
"Naming: `tryRefresh` reads as best-effort, but the retry budget "
|
|
76
|
+
"actually escalates. Suggest `refreshWithBackoff`."),
|
|
77
|
+
("codex", "block",
|
|
78
|
+
"Backoff jitter uses Math.random — flakes integration tests. "
|
|
79
|
+
"Inject the rng so tests can pin it."),
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"n": 2,
|
|
84
|
+
"hint": "proposer revises, reviewer signs off",
|
|
85
|
+
"turns": [
|
|
86
|
+
("claude", "ack",
|
|
87
|
+
"Renamed to `refreshWithBackoff`; threaded `rng` through the "
|
|
88
|
+
"config object. Added a test that pins seed 42."),
|
|
89
|
+
("codex", "ack",
|
|
90
|
+
"Looks right — retries fire 3× with jitter, surfaces 503 to "
|
|
91
|
+
"caller after budget exhausted. Approving."),
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
_DEMO_VERDICT = {
|
|
97
|
+
"outcome": "approved",
|
|
98
|
+
"reason": "2 rounds · 5 turns · all blocks resolved",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_DEMO_ARTIFACT = "~/.roll/.peer-state/logs/20260519_213700_claude_codex.md"
|
|
102
|
+
_DEMO_NEXT = [
|
|
103
|
+
("Continue execution", "claude resumes work on US-AUTH-014"),
|
|
104
|
+
("Inspect log", "open the artifact above to replay the transcript"),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
109
|
+
# Render primitives
|
|
110
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
111
|
+
|
|
112
|
+
def _divider(char: str = "─") -> None:
|
|
113
|
+
print(c("dim", char * min(COLS, 80)))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _eyebrow(trigger: str) -> None:
|
|
117
|
+
left = (" " + c("blue", "PEER", bold=True) +
|
|
118
|
+
c("dim", " · ") +
|
|
119
|
+
c("dim", "roll peer · cross-agent review"))
|
|
120
|
+
right = c("purple", trigger, bold=True) + " "
|
|
121
|
+
print(row(left, right))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _subject(subj: dict) -> None:
|
|
125
|
+
story = c("blue", subj["story"], bold=True)
|
|
126
|
+
title = c("fg", subj["title"])
|
|
127
|
+
pr = c("amber", subj["pr"], bold=True)
|
|
128
|
+
diff = c("muted", subj["diff_stat"])
|
|
129
|
+
line = " " + story + c("muted", " · ") + title
|
|
130
|
+
print(line)
|
|
131
|
+
print(" " + pr + c("muted", " ") + diff)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _pair_overview(subj: dict) -> None:
|
|
135
|
+
p_name = subj["proposer"]
|
|
136
|
+
r_name = subj["reviewer"]
|
|
137
|
+
p_c = _agent_c(p_name)
|
|
138
|
+
r_c = _agent_c(r_name)
|
|
139
|
+
proposer = c("dim", "proposer ") + c(p_c, p_name, bold=True)
|
|
140
|
+
reviewer = c("dim", "reviewer ") + c(r_c, r_name, bold=True)
|
|
141
|
+
sep = c("muted", " → ")
|
|
142
|
+
print(" " + proposer + sep + reviewer)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _round_header(n: int, hint: str) -> None:
|
|
146
|
+
label = c("pink", f"ROUND {n}", bold=True)
|
|
147
|
+
print()
|
|
148
|
+
print(" " + label + c("muted", " · ") + c("dim", hint))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _weight_chip(weight: str) -> str:
|
|
152
|
+
glyph, color, label = _WEIGHTS.get(weight, ("·", "muted", weight))
|
|
153
|
+
return c(color, glyph + " " + label, bold=(weight in ("ack", "block")))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _turn(agent: str, weight: str, body: str) -> None:
|
|
157
|
+
agent_c = _agent_c(agent)
|
|
158
|
+
name = c(agent_c, agent, bold=True)
|
|
159
|
+
chip = _weight_chip(weight)
|
|
160
|
+
# First line: agent chip
|
|
161
|
+
print(" " + name + c("muted", " ") + chip)
|
|
162
|
+
# Body wrapped with hanging indent so long sentences stay readable.
|
|
163
|
+
_print_wrapped(body, indent=6, width=min(COLS, 80))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _print_wrapped(s: str, *, indent: int, width: int) -> None:
|
|
167
|
+
avail = max(20, width - indent)
|
|
168
|
+
line = ""
|
|
169
|
+
pad = " " * indent
|
|
170
|
+
for word in s.split():
|
|
171
|
+
if line and len(line) + 1 + len(word) > avail:
|
|
172
|
+
print(pad + c("dim", line))
|
|
173
|
+
line = word
|
|
174
|
+
else:
|
|
175
|
+
line = (line + " " + word) if line else word
|
|
176
|
+
if line:
|
|
177
|
+
print(pad + c("dim", line))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _verdict(v: dict) -> None:
|
|
181
|
+
outcome = v["outcome"]
|
|
182
|
+
if outcome == "approved":
|
|
183
|
+
glyph, color, label = "✓", "green", "approved"
|
|
184
|
+
else:
|
|
185
|
+
glyph, color, label = "✗", "red", "changes requested"
|
|
186
|
+
head = c(color, f"{glyph} VERDICT", bold=True) + c("muted", " · ") + c(color, label)
|
|
187
|
+
print()
|
|
188
|
+
print(" " + head)
|
|
189
|
+
print(" " + c("dim", v["reason"]))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _footer(artifact: str, next_steps: list) -> None:
|
|
193
|
+
print()
|
|
194
|
+
print(" " + c("dim", "artifact ") + c("muted", artifact))
|
|
195
|
+
print()
|
|
196
|
+
print(" " + c("pink", "NEXT", bold=True) + c("dim", " · 下一步"))
|
|
197
|
+
for i, (label, hint) in enumerate(next_steps, start=1):
|
|
198
|
+
num = c("dim", f" {i}.")
|
|
199
|
+
print(f"{num} {c('fg', label, bold=True)}")
|
|
200
|
+
print(" " + c("dim", hint))
|
|
201
|
+
_divider("═")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
205
|
+
# Top-level render
|
|
206
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
207
|
+
|
|
208
|
+
def render_demo() -> None:
|
|
209
|
+
_eyebrow(_DEMO_SUBJECT["trigger"])
|
|
210
|
+
_divider()
|
|
211
|
+
print()
|
|
212
|
+
_subject(_DEMO_SUBJECT)
|
|
213
|
+
print()
|
|
214
|
+
_pair_overview(_DEMO_SUBJECT)
|
|
215
|
+
for rd in _DEMO_ROUNDS:
|
|
216
|
+
_round_header(rd["n"], rd["hint"])
|
|
217
|
+
for agent, weight, body in rd["turns"]:
|
|
218
|
+
_turn(agent, weight, body)
|
|
219
|
+
_verdict(_DEMO_VERDICT)
|
|
220
|
+
_footer(_DEMO_ARTIFACT, _DEMO_NEXT)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
224
|
+
# Entry point
|
|
225
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
226
|
+
|
|
227
|
+
def main() -> None:
|
|
228
|
+
ap = argparse.ArgumentParser(add_help=False)
|
|
229
|
+
ap.add_argument("--demo", action="store_true")
|
|
230
|
+
ap.add_argument("--no-color", dest="no_color", action="store_true")
|
|
231
|
+
ap.add_argument("--en", action="store_true")
|
|
232
|
+
ap.add_argument("--zh", action="store_true")
|
|
233
|
+
args, _ = ap.parse_known_args()
|
|
234
|
+
|
|
235
|
+
if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
|
|
236
|
+
roll_render.USE_COLOR = False
|
|
237
|
+
|
|
238
|
+
render_demo()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
main()
|
package/lib/roll_render.py
CHANGED
|
@@ -88,6 +88,21 @@ def fmt_dur(s: int) -> str:
|
|
|
88
88
|
return f"{s // 60}m"
|
|
89
89
|
return f"{s // 3600}h {(s % 3600) // 60}m"
|
|
90
90
|
|
|
91
|
+
def fmt_model(model) -> str:
|
|
92
|
+
"""Short label for the cycle row's model column.
|
|
93
|
+
|
|
94
|
+
`claude-opus-4-7-20251001` → `opus-4-7`
|
|
95
|
+
None / empty → `—`
|
|
96
|
+
Non-claude vendor → `?`
|
|
97
|
+
"""
|
|
98
|
+
if not model:
|
|
99
|
+
return "—"
|
|
100
|
+
if not model.startswith("claude-"):
|
|
101
|
+
return "?"
|
|
102
|
+
s = model[len("claude-"):]
|
|
103
|
+
s = re.sub(r"-\d{6,8}$", "", s)
|
|
104
|
+
return s if s else "?"
|
|
105
|
+
|
|
91
106
|
def fmt_tokens(n: int) -> str:
|
|
92
107
|
"""Format a token count with K / M / B unit scaling, 1 decimal place.
|
|
93
108
|
Uppercase suffix disambiguates from duration's lowercase m / h on the
|
|
@@ -298,11 +313,17 @@ def cycle_row(cy: Dict[str, Any], backlog: Dict[str, str]) -> None:
|
|
|
298
313
|
time_c = "red" if outcome == "fail" else "fg"
|
|
299
314
|
sid_c = "red" if outcome == "fail" else "blue"
|
|
300
315
|
|
|
316
|
+
model_label = fmt_model(cy.get("model"))
|
|
317
|
+
# Auto-hide model column on narrow screens — keeps the dashboard readable
|
|
318
|
+
# when terminal is < 100 cols (cost / story IDs are higher-priority).
|
|
319
|
+
show_model = COLS >= 100
|
|
320
|
+
model_seg = c("muted", pad(model_label, 11)) + " " if show_model else ""
|
|
301
321
|
inner = (
|
|
302
322
|
" " + c(glyph_c, glyph, bold=True) + " " +
|
|
303
323
|
c(time_c, pad(time_str, 5), bold=(outcome == "fail")) + " " +
|
|
304
324
|
c("muted", pad(dur, 4, "r")) + " " +
|
|
305
325
|
c("muted", pad(tok, 6, "r")) + " " +
|
|
326
|
+
model_seg +
|
|
306
327
|
c("muted", pad(cost, 7, "r")) + " " +
|
|
307
328
|
c(sid_c, ids_str, bold=True)
|
|
308
329
|
)
|
package/package.json
CHANGED
|
@@ -278,7 +278,7 @@ When any signal appears, **do not stop — flag it**:
|
|
|
278
278
|
# 1. Append to .roll/backlog.md under ## ♻️ Refactor
|
|
279
279
|
# REFACTOR-XXX | <one-line description> | 📋 Todo
|
|
280
280
|
|
|
281
|
-
# 2. Append a brief entry to .roll/features/refactor-log.md
|
|
281
|
+
# 2. Append a brief entry to .roll/features/autonomous-evolution/refactor-log.md
|
|
282
282
|
```
|
|
283
283
|
|
|
284
284
|
**REFACTOR entry format in .roll/backlog.md:**
|
|
@@ -287,7 +287,7 @@ When any signal appears, **do not stop — flag it**:
|
|
|
287
287
|
| REFACTOR-001 | {one-line plain-language description} | 📋 Todo |
|
|
288
288
|
```
|
|
289
289
|
|
|
290
|
-
描述写法:参见 AGENTS.md "Backlog descriptions" 规则。说清楚"什么需要改"以及"不改会怎样",技术细节写在 `.roll/features/refactor-log.md`。
|
|
290
|
+
描述写法:参见 AGENTS.md "Backlog descriptions" 规则。说清楚"什么需要改"以及"不改会怎样",技术细节写在 `.roll/features/autonomous-evolution/refactor-log.md`。
|
|
291
291
|
|
|
292
292
|
**refactor-log.md entry format:**
|
|
293
293
|
|
|
@@ -201,18 +201,30 @@ Together these mean: only one loop runs at a time per project (LOCK), and within
|
|
|
201
201
|
|
|
202
202
|
### Step 3 — Route and Execute
|
|
203
203
|
|
|
204
|
-
For each item, **before invoking the executor skill**, mark the story 🔨 In Progress in .roll/backlog.md so brief and peer agents can see it
|
|
204
|
+
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:
|
|
205
205
|
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
```bash
|
|
207
|
+
bash -c 'source "$(command -v roll)"; _loop_mark_in_progress US-XXX'
|
|
208
|
+
# Updates ${ROLL_MAIN_PROJECT}/.roll/backlog.md in place: flips the row
|
|
209
|
+
# containing US-XXX from "📋 Todo" to "🔨 In Progress". Idempotent.
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
If the executor fails (TCR aborts, CI red, etc.), revert the marker so the next cycle can re-pick the story:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
bash -c 'source "$(command -v roll)"; _loop_mark_todo US-XXX'
|
|
216
|
+
```
|
|
208
217
|
|
|
209
|
-
|
|
218
|
+
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.
|
|
210
219
|
|
|
211
|
-
选定故事后,调用 `_loop_event` 发出
|
|
220
|
+
选定故事后,调用 `_loop_event` 发出 pick_todo 事件,让 dashboard / monitor / attach 都能把"这个 cycle 选了哪个 story"正确归类:
|
|
212
221
|
|
|
213
222
|
```bash
|
|
214
223
|
# 选定故事后立即 emit(在调用 executor skill 之前)
|
|
215
|
-
|
|
224
|
+
# label 必须是 cycle_id(来自 bin/roll 注入的 LOOP_CYCLE_ID 环境变量),
|
|
225
|
+
# 不是 US_ID — dashboard 按 label 聚类,US_ID 当 label 会让事件分到错的桶
|
|
226
|
+
# 里,cycle 看起来"有 token 没 ID"。
|
|
227
|
+
_loop_event pick_todo "$LOOP_CYCLE_ID" "$US_ID" ""
|
|
216
228
|
```
|
|
217
229
|
|
|
218
230
|
Then invoke the executor:
|
|
@@ -252,8 +264,7 @@ After each item completes:
|
|
|
252
264
|
it derives `owner/repo` from the git remote and uses `gh -R <slug>`, which
|
|
253
265
|
is required to work through `~/.ssh/config` host rewrites that break gh's
|
|
254
266
|
auto-detection.
|
|
255
|
-
- CI passes →
|
|
256
|
-
continue normally
|
|
267
|
+
- 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
|
|
257
268
|
- CI fails / times out / `gh` call fails → enter **CI self-heal** (US-AUTO-041)
|
|
258
269
|
- `gh` binary not installed (`command -v gh` fails) → skip gracefully
|
|
259
270
|
(return 0). Any other `gh` error is **not** "gh unavailable" — it is a
|
|
@@ -261,9 +272,9 @@ After each item completes:
|
|
|
261
272
|
|
|
262
273
|
**CI self-heal (US-AUTO-041)** — bounded auto-fix before ALERT.
|
|
263
274
|
|
|
264
|
-
|
|
275
|
+
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.
|
|
265
276
|
|
|
266
|
-
**Path A — attempt allowed (
|
|
277
|
+
**Path A — attempt allowed (counter incremented in `state.yaml`):**
|
|
267
278
|
|
|
268
279
|
1. Capture failure summary:
|
|
269
280
|
```
|
|
Binary file
|
|
Binary file
|
|
Binary file
|