@seanyao/roll 2026.519.3 → 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 +19 -0
- package/README.md +37 -86
- package/bin/roll +545 -97
- package/conventions/global/AGENTS.md +3 -2
- package/lib/roll-peer.py +242 -0
- package/lib/roll_render.py +21 -0
- package/package.json +1 -1
- package/skills/roll-loop/SKILL.md +16 -8
- 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-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
|
@@ -201,12 +201,21 @@ 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
220
|
选定故事后,调用 `_loop_event` 发出 pick_todo 事件,让 dashboard / monitor / attach 都能把"这个 cycle 选了哪个 story"正确归类:
|
|
212
221
|
|
|
@@ -255,8 +264,7 @@ After each item completes:
|
|
|
255
264
|
it derives `owner/repo` from the git remote and uses `gh -R <slug>`, which
|
|
256
265
|
is required to work through `~/.ssh/config` host rewrites that break gh's
|
|
257
266
|
auto-detection.
|
|
258
|
-
- CI passes →
|
|
259
|
-
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
|
|
260
268
|
- CI fails / times out / `gh` call fails → enter **CI self-heal** (US-AUTO-041)
|
|
261
269
|
- `gh` binary not installed (`command -v gh` fails) → skip gracefully
|
|
262
270
|
(return 0). Any other `gh` error is **not** "gh unavailable" — it is a
|
|
@@ -264,9 +272,9 @@ After each item completes:
|
|
|
264
272
|
|
|
265
273
|
**CI self-heal (US-AUTO-041)** — bounded auto-fix before ALERT.
|
|
266
274
|
|
|
267
|
-
|
|
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.
|
|
268
276
|
|
|
269
|
-
**Path A — attempt allowed (
|
|
277
|
+
**Path A — attempt allowed (counter incremented in `state.yaml`):**
|
|
270
278
|
|
|
271
279
|
1. Capture failure summary:
|
|
272
280
|
```
|
|
Binary file
|
|
Binary file
|
|
Binary file
|