@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.
- package/CHANGELOG.md +57 -25
- package/README.md +10 -7
- package/bin/roll +3952 -317
- package/conventions/config.yaml +7 -0
- package/lib/__pycache__/github_sync.cpython-314.pyc +0 -0
- package/lib/__pycache__/loop_result_eval.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-home.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_git.cpython-314.pyc +0 -0
- package/lib/__pycache__/slides-render.cpython-314.pyc +0 -0
- package/lib/agent_usage/__init__.py +4 -0
- package/lib/agent_usage/__pycache__/__init__.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/gemini.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/kimi.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/openai.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/qwen.cpython-314.pyc +0 -0
- package/lib/agent_usage/gemini.py +127 -0
- package/lib/agent_usage/kimi.py +127 -0
- package/lib/agent_usage/openai.py +126 -0
- package/lib/agent_usage/qwen.py +128 -0
- package/lib/context_feed_budget.sh +194 -0
- package/lib/github_sync.py +876 -0
- package/lib/i18n/agent.sh +54 -0
- package/lib/i18n/init.sh +22 -0
- package/lib/i18n/peer.sh +7 -0
- package/lib/i18n/peer_help.sh +4 -0
- package/lib/i18n/skills_catalog.sh +30 -0
- package/lib/loop-exit-summary.py +393 -0
- package/lib/loop-fmt.py +93 -75
- package/lib/loop_pick_agent.py +241 -170
- package/lib/loop_result_eval.py +469 -0
- package/lib/model_prices.py +0 -10
- package/lib/roll-home.py +1 -28
- package/lib/roll-loop-status.py +330 -40
- package/lib/roll-onboard-render.py +378 -0
- package/lib/roll-peer.py +1 -1
- package/lib/roll-plan-validate.py +165 -0
- package/lib/roll_git.py +41 -0
- package/lib/slides/components/README.md +8 -2
- package/lib/slides/templates/introduction-v3.html +1 -6
- package/lib/slides-render.py +305 -15
- package/lib/slides-validate.py +195 -7
- package/package.json +1 -1
- package/skills/roll-.changelog/SKILL.md +67 -56
- package/skills/roll-brief/SKILL.md +1 -1
- package/skills/roll-build/SKILL.md +14 -12
- package/skills/roll-deck/SKILL.md +152 -0
- package/skills/roll-design/SKILL.md +13 -6
- package/skills/roll-doc/SKILL.md +269 -6
- package/skills/roll-fix/SKILL.md +15 -9
- package/skills/roll-loop/SKILL.md +9 -7
- package/skills/roll-notes/SKILL.md +1 -1
- package/skills/roll-onboard/SKILL.md +85 -0
- package/skills/roll-peer/SKILL.md +6 -5
- package/lib/agent_routes_lint.py +0 -203
- package/skills/roll-research/SKILL.md +0 -316
- package/skills/roll-research/references/schema.json +0 -166
- 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
|
-
|
|
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.
|
|
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":
|