@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.
@@ -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). docs-only commits (matching CI `paths-ignore`) don't reset
64
- the gate either way.
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)
@@ -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 + docs/", "初始化项目工作流文件", False),
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),
@@ -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()
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "2026.519.2",
3
+ "version": "2026.520.1",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "scripts": {
6
6
  "test": "bash tests/run.sh"
@@ -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's being worked on:
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
- 1. Edit .roll/backlog.md: change the row's Status column from `📋 Todo` to `🔨 In Progress`.
207
- 2. Commit: `git commit -am "chore: mark US-XXX in progress"` (use the actual story id).
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
- This commit is what makes the work visible without it, tcr micro-commits during execution are invisible to `roll-brief`.
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` 发出 story 事件,让 monitor attach 能渲染当前进度:
220
+ 选定故事后,调用 `_loop_event` 发出 pick_todo 事件,让 dashboard / monitor / attach 都能把"这个 cycle 选了哪个 story"正确归类:
212
221
 
213
222
  ```bash
214
223
  # 选定故事后立即 emit(在调用 executor skill 之前)
215
- _loop_event story "$US_ID" "$story_title" ""
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 → call `_loop_clear_heal_state <story_id>` (idempotent) and
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
- Call `_loop_self_heal_ci <story_id>` to check if another attempt is permitted.
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 (exit 0, counter incremented in `state.yaml`):**
277
+ **Path A — attempt allowed (counter incremented in `state.yaml`):**
267
278
 
268
279
  1. Capture failure summary:
269
280
  ```