@seanyao/roll 3.609.2 → 3.610.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 +44 -1
- package/README.md +5 -6
- package/dist/roll.mjs +16146 -16186
- package/package.json +3 -2
- package/skills/README.md +14 -1
- package/skills/docs/skill-authoring.md +66 -0
- package/skills/reports/skill-audit-summary.md +53 -0
- package/skills/roll-.changelog/SKILL.md +25 -443
- package/skills/roll-.changelog/references/full-contract.md +462 -0
- package/skills/roll-.clarify/SKILL.md +6 -4
- package/skills/roll-.dream/SKILL.md +26 -353
- package/skills/roll-.dream/references/full-contract.md +365 -0
- package/skills/roll-.echo/SKILL.md +6 -4
- package/skills/roll-.qa/SKILL.md +25 -236
- package/skills/roll-.qa/references/full-contract.md +256 -0
- package/skills/roll-.review/SKILL.md +6 -2
- package/skills/roll-brief/SKILL.md +6 -8
- package/skills/roll-build/SKILL.md +28 -864
- package/skills/roll-build/references/full-contract.md +883 -0
- package/skills/roll-debug/SKILL.md +26 -585
- package/skills/roll-debug/references/full-contract.md +607 -0
- package/skills/roll-design/SKILL.md +28 -903
- package/skills/roll-design/references/full-contract.md +923 -0
- package/skills/roll-doc/SKILL.md +25 -574
- package/skills/roll-doc/references/full-contract.md +594 -0
- package/skills/roll-doctor/SKILL.md +21 -2
- package/skills/roll-fix/SKILL.md +28 -621
- package/skills/roll-fix/references/full-contract.md +640 -0
- package/skills/roll-idea/SKILL.md +6 -2
- package/skills/roll-loop/SKILL.md +27 -543
- package/skills/roll-loop/references/full-contract.md +555 -0
- package/skills/roll-notes/SKILL.md +6 -2
- package/skills/roll-onboard/SKILL.md +6 -2
- package/skills/roll-peer/SKILL.md +27 -316
- package/skills/roll-peer/references/full-contract.md +329 -0
- package/skills/roll-propose/SKILL.md +6 -8
- package/skills/roll-review-pr/SKILL.md +6 -2
- package/skills/roll-sentinel/SKILL.md +26 -344
- package/skills/roll-sentinel/references/full-contract.md +363 -0
- package/skills/roll-spar/SKILL.md +27 -269
- package/skills/roll-spar/references/full-contract.md +288 -0
- package/skills/route-cases/skills.json +235 -0
- package/skills/scripts/audit-skills.mjs +272 -0
- package/skills/scripts/test-audit-skills.mjs +39 -0
- package/skills/tests/fixtures/skill-audit/block-skill/SKILL.md +12 -0
- package/skills/tests/fixtures/skill-audit/minimal-skill/SKILL.md +8 -0
- package/skills/tests/fixtures/skill-audit/quoted-skill/SKILL.md +10 -0
- package/skills/tests/fixtures/skill-audit/route-cases.json +21 -0
- package/skills/tests/fixtures/skill-audit/spoke-skill/SKILL.md +12 -0
- package/skills/tests/fixtures/skill-audit/spoke-skill/references/runbook.md +3 -0
- package/bin/roll +0 -15361
- package/lib/backfill-pi-usage.py +0 -243
- package/lib/changelog_audit.py +0 -149
- package/lib/changelog_generate.py +0 -470
- package/lib/consistency_check.py +0 -409
- package/lib/context_feed_budget.sh +0 -194
- package/lib/github_sync.py +0 -876
- package/lib/i18n/slides.sh +0 -3
- package/lib/i18n/slides_build.sh +0 -38
- package/lib/i18n/slides_delete.sh +0 -19
- package/lib/i18n/slides_list.sh +0 -14
- package/lib/i18n/slides_logs.sh +0 -12
- package/lib/i18n/slides_new.sh +0 -15
- package/lib/i18n/slides_preview.sh +0 -14
- package/lib/i18n/slides_templates.sh +0 -7
- package/lib/i18n.sh +0 -211
- package/lib/loop-exit-summary.py +0 -393
- package/lib/loop-fmt.py +0 -589
- package/lib/loop_pick_agent.py +0 -316
- package/lib/loop_result_eval.py +0 -469
- package/lib/loop_unstick.py +0 -180
- package/lib/model_prices.py +0 -194
- package/lib/prices_fetcher.py +0 -534
- package/lib/roll-backlog.py +0 -225
- package/lib/roll-brief.py +0 -286
- package/lib/roll-help.py +0 -158
- package/lib/roll-home.py +0 -556
- package/lib/roll-init.py +0 -156
- package/lib/roll-loop-status.py +0 -1691
- package/lib/roll-loop-story.py +0 -191
- package/lib/roll-peer.py +0 -252
- package/lib/roll-setup.py +0 -102
- package/lib/roll-status.py +0 -367
- package/lib/roll_git.py +0 -41
- package/lib/roll_render.py +0 -414
- package/lib/slides/components/README.md +0 -123
- package/lib/slides/components/cards-2.html +0 -9
- package/lib/slides/components/cards-3.html +0 -9
- package/lib/slides/components/cards-4.html +0 -9
- package/lib/slides/components/compare.html +0 -22
- package/lib/slides/components/highlight.html +0 -9
- package/lib/slides/components/pipeline.html +0 -12
- package/lib/slides/components/plain.html +0 -7
- package/lib/slides/components/quote.html +0 -4
- package/lib/slides/components/timeline.html +0 -9
- package/lib/slides/templates/introduction-v3.html +0 -571
- package/lib/slides/templates/pitch.html +0 -0
- package/lib/slides-render.py +0 -778
- package/lib/slides-validate.py +0 -357
- package/lib/test_quality_gate.py +0 -143
- package/skills/roll-deck/SKILL.md +0 -296
- /package/skills/roll-debug/{injectable-bb.js → assets/injectable-bb.js} +0 -0
- /package/skills/roll-design/{ENGINEERING_CHECKLIST.md → references/engineering-checklist.md} +0 -0
package/lib/roll-loop-story.py
DELETED
|
@@ -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()
|