@seanyao/roll 2026.529.5 → 2026.601.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 (59) hide show
  1. package/CHANGELOG.md +57 -25
  2. package/README.md +10 -7
  3. package/bin/roll +3952 -317
  4. package/conventions/config.yaml +7 -0
  5. package/lib/__pycache__/github_sync.cpython-314.pyc +0 -0
  6. package/lib/__pycache__/loop_result_eval.cpython-314.pyc +0 -0
  7. package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
  8. package/lib/__pycache__/roll-home.cpython-314.pyc +0 -0
  9. package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
  10. package/lib/__pycache__/roll_git.cpython-314.pyc +0 -0
  11. package/lib/__pycache__/slides-render.cpython-314.pyc +0 -0
  12. package/lib/agent_usage/__init__.py +4 -0
  13. package/lib/agent_usage/__pycache__/__init__.cpython-314.pyc +0 -0
  14. package/lib/agent_usage/__pycache__/gemini.cpython-314.pyc +0 -0
  15. package/lib/agent_usage/__pycache__/kimi.cpython-314.pyc +0 -0
  16. package/lib/agent_usage/__pycache__/openai.cpython-314.pyc +0 -0
  17. package/lib/agent_usage/__pycache__/qwen.cpython-314.pyc +0 -0
  18. package/lib/agent_usage/gemini.py +127 -0
  19. package/lib/agent_usage/kimi.py +127 -0
  20. package/lib/agent_usage/openai.py +126 -0
  21. package/lib/agent_usage/qwen.py +128 -0
  22. package/lib/context_feed_budget.sh +194 -0
  23. package/lib/github_sync.py +876 -0
  24. package/lib/i18n/agent.sh +54 -0
  25. package/lib/i18n/init.sh +22 -0
  26. package/lib/i18n/peer.sh +7 -0
  27. package/lib/i18n/peer_help.sh +4 -0
  28. package/lib/i18n/skills_catalog.sh +30 -0
  29. package/lib/loop-exit-summary.py +393 -0
  30. package/lib/loop-fmt.py +93 -75
  31. package/lib/loop_pick_agent.py +241 -170
  32. package/lib/loop_result_eval.py +469 -0
  33. package/lib/model_prices.py +0 -10
  34. package/lib/roll-home.py +1 -28
  35. package/lib/roll-loop-status.py +330 -40
  36. package/lib/roll-onboard-render.py +378 -0
  37. package/lib/roll-peer.py +1 -1
  38. package/lib/roll-plan-validate.py +165 -0
  39. package/lib/roll_git.py +41 -0
  40. package/lib/slides/components/README.md +8 -2
  41. package/lib/slides/templates/introduction-v3.html +1 -6
  42. package/lib/slides-render.py +305 -15
  43. package/lib/slides-validate.py +195 -7
  44. package/package.json +1 -1
  45. package/skills/roll-.changelog/SKILL.md +67 -56
  46. package/skills/roll-brief/SKILL.md +1 -1
  47. package/skills/roll-build/SKILL.md +14 -12
  48. package/skills/roll-deck/SKILL.md +152 -0
  49. package/skills/roll-design/SKILL.md +13 -6
  50. package/skills/roll-doc/SKILL.md +269 -6
  51. package/skills/roll-fix/SKILL.md +15 -9
  52. package/skills/roll-loop/SKILL.md +9 -7
  53. package/skills/roll-notes/SKILL.md +1 -1
  54. package/skills/roll-onboard/SKILL.md +85 -0
  55. package/skills/roll-peer/SKILL.md +6 -5
  56. package/lib/agent_routes_lint.py +0 -203
  57. package/skills/roll-research/SKILL.md +0 -316
  58. package/skills/roll-research/references/schema.json +0 -166
  59. package/skills/roll-research/scripts/md_to_pdf.py +0 -289
package/lib/loop-fmt.py CHANGED
@@ -116,6 +116,10 @@ class LoopFmt:
116
116
  self.pending_ci = False
117
117
  self.pending_story = False
118
118
  self.spinner = Spinner()
119
+ # US-VIEW-020: consecutive same-file Edit/Write streak. Tracks the last
120
+ # file path and how many times in a row it's been edited so the renderer
121
+ # can collapse N identical lines into one `✏ <basename> ×N` line.
122
+ self._edit_streak = (None, 0) # (last_file_path, count)
119
123
  # Accumulate token usage across all assistant turns in the cycle so
120
124
  # the trailing result event can emit a 'usage' event carrying the
121
125
  # cumulative totals (result.usage only carries the last turn's).
@@ -199,10 +203,89 @@ class LoopFmt:
199
203
  m2 = re.search(r'(\w+)\s*→\s*(\w+)', text)
200
204
  if m2:
201
205
  agents = f"{m2.group(1)} → {m2.group(2)}"
206
+ self._flush_edit_streak()
202
207
  print(step("peer", agents, f"{round_str} · {verdict}"))
203
208
  return
204
209
  # All other text: Tier 3, suppress
205
210
 
211
+ @staticmethod
212
+ def _edit_hint(inp):
213
+ """US-VIEW-021: derive a ≤20-char change feature from an Edit/Write input.
214
+
215
+ Priority:
216
+ 1. replace_all=true → "replace-all"
217
+ 2. else first non-blank token of new_string's first line, with leading
218
+ whitespace / comment markers stripped.
219
+ Truncates to 20 chars (unicode chars, not bytes) with a trailing "…".
220
+ Empty / all-whitespace new_string → "" (caller omits the ` | ` segment).
221
+ """
222
+ if inp.get("replace_all") is True:
223
+ return "replace-all"
224
+ new_string = inp.get("new_string") or ""
225
+ if not isinstance(new_string, str):
226
+ return ""
227
+ # first non-blank line
228
+ first_line = next((l for l in new_string.splitlines() if l.strip()), "")
229
+ s = first_line.strip()
230
+ # strip leading comment markers (#, //, /*, *, --, ;) then re-strip
231
+ s = re.sub(r'^(#+|//+|/\*+|\*+|--+|;+)\s*', '', s).strip()
232
+ # first token (non-whitespace run)
233
+ token = s.split()[0] if s.split() else ""
234
+ if not token:
235
+ return ""
236
+ if len(token) > 20:
237
+ return token[:20] + "…"
238
+ return token
239
+
240
+ def _edit_streak_line(self, path, count, hint=""):
241
+ """Render the streak line for `path` at `count`. basename only.
242
+
243
+ US-VIEW-021: when `hint` is non-empty, insert a ` | <hint>` segment
244
+ before the ×N suffix: `✏ <basename> | <hint> ×N`.
245
+ """
246
+ base = os.path.basename(path) or path
247
+ hint_part = f" | {hint}" if hint else ""
248
+ suffix = f" ×{count}" if count >= 2 else ""
249
+ return f" {DARK_GRAY}✏ {base}{hint_part}{suffix}{RESET}"
250
+
251
+ def _flush_edit_streak(self):
252
+ """Finalize the current Edit/Write streak (if any), leaving its last
253
+ line in place, and reset the streak state. In production the streak
254
+ used `\\r` in-place refresh, so we end with a newline to keep the final
255
+ line; in test mode each line was already printed standalone."""
256
+ path, count = self._edit_streak
257
+ if path is None:
258
+ return
259
+ if _SPIN_ENABLED:
260
+ # finish the in-place line so subsequent output starts fresh
261
+ sys.stdout.write("\n")
262
+ sys.stdout.flush()
263
+ self._edit_streak = (None, 0)
264
+
265
+ def _handle_edit(self, path, hint=""):
266
+ last_path, count = self._edit_streak
267
+ if path == last_path:
268
+ count += 1
269
+ self._edit_streak = (path, count)
270
+ line = self._edit_streak_line(path, count, hint)
271
+ if _SPIN_ENABLED:
272
+ # in-place refresh: overwrite the current line with the new ×N
273
+ sys.stdout.write("\r" + line)
274
+ sys.stdout.flush()
275
+ else:
276
+ # deterministic test mode: static line per Spinner-style convention
277
+ print(line)
278
+ else:
279
+ # different file: flush previous streak, then start a new one
280
+ self._flush_edit_streak()
281
+ self._edit_streak = (path, 1)
282
+ line = self._edit_streak_line(path, 1, hint)
283
+ if _SPIN_ENABLED:
284
+ sys.stdout.write(line)
285
+ sys.stdout.flush()
286
+ else:
287
+ print(line)
288
+
206
289
  def _handle_tool_use(self, blk):
207
290
  name = blk.get("name", "")
208
291
  inp = blk.get("input", {})
@@ -212,9 +295,13 @@ class LoopFmt:
212
295
 
213
296
  if name in ("Edit", "Write"):
214
297
  path = inp.get("file_path") or inp.get("path", "")
215
- print(f" {DARK_GRAY}✏ {path}{RESET}")
298
+ hint = self._edit_hint(inp)
299
+ self._handle_edit(path, hint)
216
300
  return # Tier 2
217
301
 
302
+ # Any non-Edit tool_use breaks the streak (don't collapse across them).
303
+ self._flush_edit_streak()
304
+
218
305
  if name == "Bash":
219
306
  cmd = inp.get("command", "")
220
307
  first_line = next((l.strip() for l in cmd.splitlines() if l.strip()), cmd)
@@ -258,6 +345,7 @@ class LoopFmt:
258
345
  self.last_test_count = int(m.group(1))
259
346
 
260
347
  if is_err:
348
+ self._flush_edit_streak() # don't stack an error onto a streak
261
349
  tool_name = "tool"
262
350
  lines = [l for l in text.splitlines() if l.strip()][:3]
263
351
  detail = " | ".join(lines)
@@ -324,6 +412,9 @@ class LoopFmt:
324
412
  return str(content) if content else ""
325
413
 
326
414
  def _handle_result(self, ev):
415
+ # US-VIEW-020: flush any residual Edit streak before the cycle summary
416
+ # so the final ✏ line isn't lost / left mid-`\r`.
417
+ self._flush_edit_streak()
327
418
  dur_ms = ev.get("duration_ms", 0)
328
419
  cost_usd = ev.get("total_cost_usd", 0)
329
420
  dur_s = dur_ms / 1000
@@ -480,83 +571,10 @@ def _passthrough_main(agent):
480
571
  # - this runs once per retry attempt, so emitting here wrote N usage
481
572
  # events per cycle and the dashboard SUMS same-label usage → ×N.
482
573
  # Instead bin/roll calls agent_usage/pi_emit.py exactly once after the
483
- # agent phase, recovering real usage from pi's session files. The
484
- # _emit_* helpers below are retained for US-LOOP-010 unit tests.
574
+ # agent phase, recovering real usage from pi's session files.
485
575
  _ = (accumulated, evfile) # intentionally unused now
486
576
 
487
577
 
488
- def _emit_passthrough_event(evfile, cycle, agent, text):
489
- """Best-effort append a usage-type event to evfile (null payload).
490
-
491
- Kept for backward-compat with US-LOOP-010 tests.
492
- """
493
- payload = {
494
- "model": agent,
495
- "input_tokens": None,
496
- "output_tokens": None,
497
- "cost_list_usd": None,
498
- "duration_ms": None,
499
- }
500
- record = json.dumps({
501
- "ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
502
- "stage": "usage",
503
- "label": cycle,
504
- "detail": payload,
505
- "outcome": "ok",
506
- }) + "\n"
507
- try:
508
- with open(evfile, "a") as f:
509
- f.write(record)
510
- except Exception:
511
- pass
512
-
513
-
514
- def _emit_final_usage_event(evfile, cycle, agent, accumulated_lines):
515
- """Try plugin extraction; emit one usage event (real or null).
516
-
517
- US-LOOP-026: at cycle end, dispatches accumulated stdout to the
518
- agent_usage plugin registry. If a plugin returns real data, emits
519
- a usage event with it. Otherwise emits a single null-payload event
520
- (US-LOOP-010 backward-compat).
521
- """
522
- payload = None
523
- try:
524
- from agent_usage import extract_usage
525
- usage = extract_usage(agent, accumulated_lines)
526
- if usage is not None:
527
- payload = {
528
- "model": usage.get("model", agent),
529
- "input_tokens": usage.get("input_tokens"),
530
- "output_tokens": usage.get("output_tokens"),
531
- "cost_list_usd": usage.get("cost_list_usd"),
532
- "duration_ms": usage.get("duration_ms"),
533
- }
534
- except Exception:
535
- pass
536
-
537
- if payload is None:
538
- payload = {
539
- "model": agent,
540
- "input_tokens": None,
541
- "output_tokens": None,
542
- "cost_list_usd": None,
543
- "duration_ms": None,
544
- }
545
-
546
- record = json.dumps({
547
- "ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
548
- "stage": "usage",
549
- "label": cycle,
550
- "detail": payload,
551
- "outcome": "ok",
552
- }) + "\n"
553
- try:
554
- with open(evfile, "a") as f:
555
- f.write(record)
556
- except Exception:
557
- pass
558
-
559
-
560
578
  def main():
561
579
  agent = os.environ.get("ROLL_LOOP_AGENT", "claude")
562
580
  if agent == "claude":