@seanyao/roll 3.608.1 → 3.610.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.
@@ -1,191 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- roll-loop-story — compact per-story rollup for `roll loop story <ID>`.
4
-
5
- Loads the same event / cron / runs / git-merge sources as roll-loop-status,
6
- filters cycles to one story id (case-insensitive), and renders a single
7
- panel covering cycles count, span, duration, tokens, cost, model, PR list,
8
- and recent cycle lines.
9
-
10
- Usage:
11
- roll loop story US-LOOP-004
12
- roll loop story us-loop-004 # case-insensitive
13
- roll loop story US-LOOP-004 --json # machine-readable
14
- roll loop story US-LOOP-004 --days 30 # widen window (default 30)
15
-
16
- Exit codes:
17
- 0 — at least one cycle found
18
- 2 — story id has no cycles in the event window
19
- """
20
- from __future__ import annotations
21
- import argparse, importlib.util, json, os, sys
22
- from datetime import datetime, timezone
23
- from pathlib import Path
24
- from typing import Any, Dict, List
25
-
26
- _LIB_DIR = os.path.dirname(os.path.realpath(__file__))
27
-
28
- # Reuse the loaders + aggregator from roll-loop-status.py. importlib because the
29
- # filename has a hyphen and isn't import-safe.
30
- _spec = importlib.util.spec_from_file_location(
31
- "_rls", os.path.join(_LIB_DIR, "roll-loop-status.py")
32
- )
33
- rls = importlib.util.module_from_spec(_spec)
34
- _spec.loader.exec_module(rls)
35
-
36
-
37
- def collect_cycles(days: int) -> List[Dict[str, Any]]:
38
- slug = rls.project_slug()
39
- events = rls.load_events(slug, days)
40
- cron = rls.load_cron_log(slug)
41
- runs = rls.load_runs(slug)
42
- git_merges = rls.load_pr_merges_from_git(days)
43
- cycles = rls.aggregate(events, cron)
44
- if runs:
45
- rls.merge_runs_into_cycles(cycles, runs)
46
- if git_merges:
47
- rls.repair_orphan_cycles_from_git(cycles, git_merges)
48
- rls.backfill_usage_from_claude_sessions(cycles, slug)
49
- return cycles
50
-
51
-
52
- def _outcome_glyph(o: str) -> str:
53
- return {"fail": "✗", "running": "⏵", "idle": "·"}.get(o, "✓")
54
-
55
-
56
- def _fmt_dt(dt: datetime) -> str:
57
- return dt.astimezone().strftime("%Y-%m-%d %H:%M")
58
-
59
-
60
- def _fmt_dur(s: int) -> str:
61
- if not s:
62
- return "—"
63
- h, rem = divmod(s, 3600)
64
- m, _ = divmod(rem, 60)
65
- return f"{h}h {m:02d}m" if h else f"{m}m"
66
-
67
-
68
- def _fmt_tokens(n: int) -> str:
69
- if not n:
70
- return "0"
71
- if n >= 1_000_000:
72
- return f"{n/1_000_000:.1f}M"
73
- if n >= 1_000:
74
- return f"{n/1_000:.0f}k"
75
- return str(n)
76
-
77
-
78
- def _fmt_pr(p: Dict[str, Any]) -> str:
79
- g = {"merged": "✓", "closed": "✗"}.get(p["outcome"], "⏵")
80
- return f"#{p['num']} {g}"
81
-
82
-
83
- def render_panel(r: Dict[str, Any], description: str = "") -> str:
84
- head = f"── {r['story_id']}"
85
- if description:
86
- head += f" · {description}"
87
- head += " " + "─" * max(0, 78 - len(head))
88
-
89
- cycles = r["cycles"]
90
- span = "—"
91
- if r.get("span_start") and r.get("span_end"):
92
- span = f"{_fmt_dt(r['span_start'])} → {_fmt_dt(r['span_end'])}"
93
- elif r.get("span_start"):
94
- span = f"{_fmt_dt(r['span_start'])} → (running)"
95
-
96
- counts = f" cycles {r['count']} (✓ {r['ok_count']} ✗ {r['fail_count']} ⏵ {r['running_count']})"
97
- line_span = f" span {span}"
98
- line_dur = (f" duration {_fmt_dur(r['duration_s'])}"
99
- f" tokens in {_fmt_tokens(r['input_tokens'])}"
100
- f" out {_fmt_tokens(r['output_tokens'])}"
101
- f" cache w {_fmt_tokens(r['cache_creation_tokens'])}"
102
- f" r {_fmt_tokens(r['cache_read_tokens'])}")
103
- model = r.get("model") or "—"
104
- line_cost = f" cost ${r['cost']:.2f} model {model}"
105
-
106
- prs = r.get("prs") or []
107
- line_prs = " PRs " + (" ".join(_fmt_pr(p) for p in prs[:8]) if prs else "—")
108
-
109
- # Recent 3 cycles (oldest → newest of the matched set; mirror confirmed layout).
110
- recent = sorted(cycles, key=lambda c: c.get("start") or datetime.min.replace(tzinfo=timezone.utc))[-3:]
111
- recent_lines: List[str] = []
112
- for i, cy in enumerate(recent):
113
- label = cy.get("label", "—")
114
- glyph = _outcome_glyph(cy.get("outcome", ""))
115
- cost = cy.get("cost_list")
116
- cost_s = f"${cost:.2f}" if cost is not None else "—"
117
- prefix = " recent " if i == 0 else " "
118
- recent_lines.append(f"{prefix} {label} {glyph} {cost_s}")
119
- if not recent_lines:
120
- recent_lines.append(" recent —")
121
-
122
- return "\n".join([head, counts, line_span, line_dur, line_cost, line_prs] + recent_lines)
123
-
124
-
125
- def to_json(r: Dict[str, Any]) -> str:
126
- def conv(o: Any) -> Any:
127
- if isinstance(o, datetime):
128
- return o.astimezone(timezone.utc).isoformat()
129
- raise TypeError(f"{type(o)} not serializable")
130
-
131
- payload = {k: v for k, v in r.items() if k != "cycles"}
132
- payload["cycles"] = [
133
- {
134
- "label": cy.get("label"),
135
- "start": cy.get("start"),
136
- "end": cy.get("end"),
137
- "outcome": cy.get("outcome"),
138
- "duration_s": cy.get("duration_s"),
139
- "input_tokens": cy.get("input_tokens"),
140
- "output_tokens": cy.get("output_tokens"),
141
- "cache_creation_tokens": cy.get("cache_creation_tokens"),
142
- "cache_read_tokens": cy.get("cache_read_tokens"),
143
- "cost_list": cy.get("cost_list"),
144
- "model": cy.get("model"),
145
- "pr_num": cy.get("pr_num"),
146
- "pr_outcome": cy.get("pr_outcome"),
147
- }
148
- for cy in r.get("cycles", [])
149
- ]
150
- return json.dumps(payload, default=conv, indent=2)
151
-
152
-
153
- def _backlog_description(story_id: str) -> str:
154
- bl = rls.load_backlog()
155
- return bl.get(story_id.upper(), "")
156
-
157
-
158
- def main(argv=None) -> int:
159
- p = argparse.ArgumentParser(
160
- description="roll loop story — per-story cycle rollup")
161
- p.add_argument("story_id", help="Story ID (case-insensitive, e.g. US-LOOP-004)")
162
- p.add_argument("--days", type=int, default=30,
163
- help="event window in days (default 30)")
164
- p.add_argument("--json", action="store_true",
165
- help="emit machine-readable JSON instead of the panel")
166
- args = p.parse_args(argv)
167
-
168
- cycles = collect_cycles(args.days)
169
- r = rls.rollup_for_story(cycles, args.story_id)
170
-
171
- if args.json:
172
- print(to_json(r))
173
- return 0 if r["count"] > 0 else 2
174
-
175
- if r["count"] == 0:
176
- sys.stderr.write(
177
- f"roll loop story: no cycles found for {args.story_id} "
178
- f"in the last {args.days} days\n"
179
- f"未找到 {args.story_id} 在最近 {args.days} 天内的循环\n"
180
- )
181
- return 2
182
-
183
- print(render_panel(r, _backlog_description(args.story_id.upper())))
184
- return 0
185
-
186
-
187
- if __name__ == "__main__":
188
- try:
189
- sys.exit(main())
190
- except BrokenPipeError:
191
- pass
package/lib/roll-peer.py DELETED
@@ -1,252 +0,0 @@
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
- "agy": "purple", # Antigravity (formerly Gemini CLI)
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
- # Fixture data (test-only; opt in via ROLL_RENDER_FIXTURE=1)
54
- # Illustrative cross-agent review: claude proposes, codex reviews
55
- # ════════════════════════════════════════════════════════════════════════════
56
-
57
- _FIXTURE_SUBJECT = {
58
- "story": "US-AUTH-014",
59
- "title": "Session refresh fallback when refresh-token API 5xx",
60
- "pr": "#412",
61
- "diff_stat": "+184 −37 · 6 files",
62
- "trigger": "complexity=large",
63
- "proposer": "claude",
64
- "reviewer": "codex",
65
- }
66
-
67
- _FIXTURE_ROUNDS = [
68
- {
69
- "n": 1,
70
- "hint": "first pass — proposer ships, reviewer probes",
71
- "turns": [
72
- ("claude", "concern",
73
- "Refresh path swallows 503 silently — caller sees a stale session "
74
- "without any signal that re-auth is needed."),
75
- ("codex", "nit",
76
- "Naming: `tryRefresh` reads as best-effort, but the retry budget "
77
- "actually escalates. Suggest `refreshWithBackoff`."),
78
- ("codex", "block",
79
- "Backoff jitter uses Math.random — flakes integration tests. "
80
- "Inject the rng so tests can pin it."),
81
- ],
82
- },
83
- {
84
- "n": 2,
85
- "hint": "proposer revises, reviewer signs off",
86
- "turns": [
87
- ("claude", "ack",
88
- "Renamed to `refreshWithBackoff`; threaded `rng` through the "
89
- "config object. Added a test that pins seed 42."),
90
- ("codex", "ack",
91
- "Looks right — retries fire 3× with jitter, surfaces 503 to "
92
- "caller after budget exhausted. Approving."),
93
- ],
94
- },
95
- ]
96
-
97
- _FIXTURE_VERDICT = {
98
- "outcome": "approved",
99
- "reason": "2 rounds · 5 turns · all blocks resolved",
100
- }
101
-
102
- _FIXTURE_ARTIFACT = ".roll/peer/logs/20260519_213700_claude_codex.md"
103
- _FIXTURE_NEXT = [
104
- ("Continue execution", "claude resumes work on US-AUTH-014"),
105
- ("Inspect log", "open the artifact above to replay the transcript"),
106
- ]
107
-
108
-
109
- # ════════════════════════════════════════════════════════════════════════════
110
- # Render primitives
111
- # ════════════════════════════════════════════════════════════════════════════
112
-
113
- def _divider(char: str = "─") -> None:
114
- print(c("dim", char * min(COLS, 80)))
115
-
116
-
117
- def _eyebrow(trigger: str) -> None:
118
- left = (" " + c("blue", "PEER", bold=True) +
119
- c("dim", " · ") +
120
- c("dim", "roll peer · cross-agent review"))
121
- right = c("purple", trigger, bold=True) + " "
122
- print(row(left, right))
123
-
124
-
125
- def _subject(subj: dict) -> None:
126
- story = c("blue", subj["story"], bold=True)
127
- title = c("fg", subj["title"])
128
- pr = c("amber", subj["pr"], bold=True)
129
- diff = c("muted", subj["diff_stat"])
130
- line = " " + story + c("muted", " · ") + title
131
- print(line)
132
- print(" " + pr + c("muted", " ") + diff)
133
-
134
-
135
- def _pair_overview(subj: dict) -> None:
136
- p_name = subj["proposer"]
137
- r_name = subj["reviewer"]
138
- p_c = _agent_c(p_name)
139
- r_c = _agent_c(r_name)
140
- proposer = c("dim", "proposer ") + c(p_c, p_name, bold=True)
141
- reviewer = c("dim", "reviewer ") + c(r_c, r_name, bold=True)
142
- sep = c("muted", " → ")
143
- print(" " + proposer + sep + reviewer)
144
-
145
-
146
- def _round_header(n: int, hint: str) -> None:
147
- label = c("pink", f"ROUND {n}", bold=True)
148
- print()
149
- print(" " + label + c("muted", " · ") + c("dim", hint))
150
-
151
-
152
- def _weight_chip(weight: str) -> str:
153
- glyph, color, label = _WEIGHTS.get(weight, ("·", "muted", weight))
154
- return c(color, glyph + " " + label, bold=(weight in ("ack", "block")))
155
-
156
-
157
- def _turn(agent: str, weight: str, body: str) -> None:
158
- agent_c = _agent_c(agent)
159
- name = c(agent_c, agent, bold=True)
160
- chip = _weight_chip(weight)
161
- # First line: agent chip
162
- print(" " + name + c("muted", " ") + chip)
163
- # Body wrapped with hanging indent so long sentences stay readable.
164
- _print_wrapped(body, indent=6, width=min(COLS, 80))
165
-
166
-
167
- def _print_wrapped(s: str, *, indent: int, width: int) -> None:
168
- avail = max(20, width - indent)
169
- line = ""
170
- pad = " " * indent
171
- for word in s.split():
172
- if line and len(line) + 1 + len(word) > avail:
173
- print(pad + c("dim", line))
174
- line = word
175
- else:
176
- line = (line + " " + word) if line else word
177
- if line:
178
- print(pad + c("dim", line))
179
-
180
-
181
- def _verdict(v: dict) -> None:
182
- outcome = v["outcome"]
183
- if outcome == "approved":
184
- glyph, color, label = "✓", "green", "approved"
185
- else:
186
- glyph, color, label = "✗", "red", "changes requested"
187
- head = c(color, f"{glyph} VERDICT", bold=True) + c("muted", " · ") + c(color, label)
188
- print()
189
- print(" " + head)
190
- print(" " + c("dim", v["reason"]))
191
-
192
-
193
- def _footer(artifact: str, next_steps: list) -> None:
194
- print()
195
- print(" " + c("dim", "artifact ") + c("muted", artifact))
196
- print()
197
- print(" " + c("pink", "NEXT", bold=True) + c("dim", " · 下一步"))
198
- for i, (label, hint) in enumerate(next_steps, start=1):
199
- num = c("dim", f" {i}.")
200
- print(f"{num} {c('fg', label, bold=True)}")
201
- print(" " + c("dim", hint))
202
- _divider("═")
203
-
204
-
205
- # ════════════════════════════════════════════════════════════════════════════
206
- # Top-level render
207
- # ════════════════════════════════════════════════════════════════════════════
208
-
209
- def render_fixture() -> None:
210
- _eyebrow(_FIXTURE_SUBJECT["trigger"])
211
- _divider()
212
- print()
213
- _subject(_FIXTURE_SUBJECT)
214
- print()
215
- _pair_overview(_FIXTURE_SUBJECT)
216
- for rd in _FIXTURE_ROUNDS:
217
- _round_header(rd["n"], rd["hint"])
218
- for agent, weight, body in rd["turns"]:
219
- _turn(agent, weight, body)
220
- _verdict(_FIXTURE_VERDICT)
221
- _footer(_FIXTURE_ARTIFACT, _FIXTURE_NEXT)
222
-
223
-
224
- # ════════════════════════════════════════════════════════════════════════════
225
- # Entry point
226
- # ════════════════════════════════════════════════════════════════════════════
227
-
228
- def main() -> None:
229
- ap = argparse.ArgumentParser(add_help=False)
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
- # FIX-076: this standalone entrypoint only knows how to render the fixture
239
- # transcript (for UI tests). Real peer review is orchestrated by bin/roll
240
- # and never invokes this main(). Require an explicit opt-in so a stray
241
- # `python3 lib/roll-peer.py` invocation can't masquerade as live output.
242
- if not os.environ.get("ROLL_RENDER_FIXTURE"):
243
- print("Error: lib/roll-peer.py only renders fixture data; "
244
- "set ROLL_RENDER_FIXTURE=1 to use it (test-only).",
245
- file=sys.stderr)
246
- sys.exit(2)
247
-
248
- render_fixture()
249
-
250
-
251
- if __name__ == "__main__":
252
- main()
package/lib/roll-setup.py DELETED
@@ -1,102 +0,0 @@
1
- #!/usr/bin/env python3
2
- """roll-setup — v2 terminal view for `roll setup`.
3
-
4
- Reads a single JSON document from stdin describing the real outcomes of
5
- `bin/roll cmd_setup`'s step sequence (detect platform / install skills /
6
- sync conventions / etc). Renders the v2 UI preserving the visual style
7
- of US-VIEW-007 while reflecting actual results.
8
-
9
- Input schema matches `lib/roll-init.py` (see that file for details).
10
- """
11
- from __future__ import annotations
12
-
13
- import argparse
14
- import json
15
- import os
16
- import sys
17
-
18
- _LIB_DIR = os.path.dirname(os.path.realpath(__file__))
19
- if _LIB_DIR not in sys.path:
20
- sys.path.insert(0, _LIB_DIR)
21
- import roll_render
22
- from roll_render import c, row, COLS
23
-
24
-
25
- def _divider(char: str = "─") -> None:
26
- print(c("dim", char * min(COLS, 80)))
27
-
28
-
29
- def _step_icon(status: str) -> str:
30
- if status == "ok":
31
- return c("green", "✓")
32
- if status == "skip":
33
- return c("amber", "↷")
34
- if status == "forced":
35
- return c("blue", "~")
36
- if status == "fail":
37
- return c("red", "✗", bold=True)
38
- return c("dim", "·")
39
-
40
-
41
- def render(payload: dict) -> None:
42
- header_label = payload.get("header_label", "SETUP")
43
- subtitle = payload.get("subtitle", "初始化")
44
- right_text = payload.get("project_path") or payload.get("right", "")
45
-
46
- left = " " + c("blue", header_label, bold=True) + c("dim", " · ") + c("dim", subtitle)
47
- right = c("dim", right_text) + " " if right_text else ""
48
- print(row(left, right))
49
- _divider()
50
- print()
51
-
52
- for step in payload.get("steps", []):
53
- num = c("dim", f" {step.get('num', 0)}.")
54
- icon = _step_icon(step.get("status", "ok"))
55
- label = step.get("label", "")
56
- print(f"{num} {icon} {label}")
57
- note = step.get("note") or step.get("error")
58
- if note:
59
- tone = "red" if step.get("status") == "fail" else "dim"
60
- print(" " + c(tone, str(note)))
61
-
62
- print()
63
- _divider()
64
-
65
- footer = payload.get("footer") or {}
66
- f_status = footer.get("status", "ok")
67
- f_label = footer.get("label", "Setup complete")
68
- icon_color = "green" if f_status == "ok" else "red"
69
- msg = c(icon_color, f_label)
70
- next_hint = footer.get("hint")
71
- if next_hint:
72
- print(f" {msg} — {next_hint}")
73
- else:
74
- print(f" {msg}")
75
- _divider("═")
76
-
77
-
78
- def _read_payload() -> dict:
79
- raw = sys.stdin.read()
80
- if not raw.strip():
81
- sys.stderr.write("roll-setup.py: expected JSON on stdin\n")
82
- sys.exit(1)
83
- try:
84
- return json.loads(raw)
85
- except json.JSONDecodeError as exc:
86
- sys.stderr.write(f"roll-setup.py: invalid JSON on stdin: {exc}\n")
87
- sys.exit(1)
88
-
89
-
90
- def main() -> None:
91
- ap = argparse.ArgumentParser(add_help=False)
92
- ap.add_argument("--no-color", dest="no_color", action="store_true")
93
- args, _ = ap.parse_known_args()
94
-
95
- if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
96
- roll_render.USE_COLOR = False
97
-
98
- render(_read_payload())
99
-
100
-
101
- if __name__ == "__main__":
102
- main()