@seanyao/roll 0.5.0 → 2.602.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.
- package/CHANGELOG.md +717 -0
- package/LICENSE +21 -0
- package/README.md +65 -165
- package/bin/dream-test-quality-scan +110 -0
- package/bin/roll +14897 -815
- package/conventions/config.yaml +17 -1
- package/conventions/global/AGENTS.md +146 -100
- package/conventions/global/CLAUDE.md +1 -21
- package/conventions/global/GEMINI.md +8 -22
- package/conventions/global/project_rules.md +9 -0
- package/conventions/templates/backend-service/AGENTS.md +30 -81
- package/conventions/templates/backend-service/GEMINI.md +3 -3
- package/conventions/templates/backend-service/project_rules.md +16 -0
- package/conventions/templates/cli/AGENTS.md +31 -58
- package/conventions/templates/cli/CLAUDE.md +3 -5
- package/conventions/templates/cli/GEMINI.md +3 -3
- package/conventions/templates/cli/project_rules.md +16 -0
- package/conventions/templates/frontend-only/AGENTS.md +29 -64
- package/conventions/templates/frontend-only/GEMINI.md +3 -3
- package/conventions/templates/frontend-only/project_rules.md +14 -0
- package/conventions/templates/fullstack/AGENTS.md +31 -79
- package/conventions/templates/fullstack/CLAUDE.md +1 -1
- package/conventions/templates/fullstack/GEMINI.md +3 -3
- package/conventions/templates/fullstack/project_rules.md +15 -0
- package/lib/README.md +42 -0
- package/lib/__pycache__/github_sync.cpython-314.pyc +0 -0
- package/lib/__pycache__/loop-fmt.cpython-314.pyc +0 -0
- package/lib/__pycache__/loop_result_eval.cpython-314.pyc +0 -0
- package/lib/__pycache__/loop_unstick.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/prices_fetcher.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__/roll_render.cpython-314.pyc +0 -0
- package/lib/__pycache__/slides-render.cpython-314.pyc +0 -0
- package/lib/agent_usage/README.md +49 -0
- package/lib/agent_usage/__init__.py +108 -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__/pi.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/pi_emit.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 +278 -0
- package/lib/agent_usage/kimi_emit.py +123 -0
- package/lib/agent_usage/openai.py +126 -0
- package/lib/agent_usage/pi.py +200 -0
- package/lib/agent_usage/pi_emit.py +135 -0
- package/lib/agent_usage/qwen.py +128 -0
- package/lib/backfill-pi-usage.py +243 -0
- package/lib/changelog_audit.py +155 -0
- package/lib/changelog_generate.py +263 -0
- package/lib/context_feed_budget.sh +194 -0
- package/lib/github_sync.py +876 -0
- package/lib/i18n/README.md +54 -0
- package/lib/i18n/agent.sh +75 -0
- package/lib/i18n/alert.sh +20 -0
- package/lib/i18n/backlog.sh +96 -0
- package/lib/i18n/brief.sh +5 -0
- package/lib/i18n/changelog.sh +5 -0
- package/lib/i18n/ci.sh +15 -0
- package/lib/i18n/debug.sh +0 -0
- package/lib/i18n/doctor.sh +44 -0
- package/lib/i18n/dream.sh +0 -0
- package/lib/i18n/init.sh +91 -0
- package/lib/i18n/lang.sh +10 -0
- package/lib/i18n/loop.sh +140 -0
- package/lib/i18n/migrate.sh +74 -0
- package/lib/i18n/offboard.sh +31 -0
- package/lib/i18n/onboard.sh +0 -0
- package/lib/i18n/peer.sh +41 -0
- package/lib/i18n/peer_help.sh +25 -0
- package/lib/i18n/peer_reset.sh +7 -0
- package/lib/i18n/peer_status.sh +5 -0
- package/lib/i18n/prices.sh +3 -0
- package/lib/i18n/prices_refresh.sh +17 -0
- package/lib/i18n/prices_show.sh +7 -0
- package/lib/i18n/propose.sh +0 -0
- package/lib/i18n/release.sh +0 -0
- package/lib/i18n/research.sh +0 -0
- package/lib/i18n/review_pr.sh +0 -0
- package/lib/i18n/sentinel.sh +0 -0
- package/lib/i18n/setup.sh +3 -0
- package/lib/i18n/shared.sh +157 -0
- package/lib/i18n/skills/roll-brief.sh +47 -0
- package/lib/i18n/skills/roll-build.sh +97 -0
- package/lib/i18n/skills/roll-design.sh +18 -0
- package/lib/i18n/skills/roll-fix.sh +53 -0
- package/lib/i18n/skills/roll-loop.sh +28 -0
- package/lib/i18n/skills/roll-onboard.sh +33 -0
- package/lib/i18n/skills_catalog.sh +30 -0
- package/lib/i18n/slides.sh +3 -0
- package/lib/i18n/slides_build.sh +38 -0
- package/lib/i18n/slides_delete.sh +19 -0
- package/lib/i18n/slides_list.sh +14 -0
- package/lib/i18n/slides_logs.sh +12 -0
- package/lib/i18n/slides_new.sh +15 -0
- package/lib/i18n/slides_preview.sh +14 -0
- package/lib/i18n/slides_templates.sh +7 -0
- package/lib/i18n/status.sh +21 -0
- package/lib/i18n/update.sh +24 -0
- package/lib/i18n.sh +211 -0
- package/lib/loop-exit-summary.py +393 -0
- package/lib/loop-fmt.py +589 -0
- package/lib/loop_pick_agent.py +316 -0
- package/lib/loop_result_eval.py +469 -0
- package/lib/loop_unstick.py +180 -0
- package/lib/model_prices.py +186 -0
- package/lib/prices/README.md +35 -0
- package/lib/prices/snapshot-2026-05-22.json +22 -0
- package/lib/prices/snapshot-2026-05-23-deepseek.json +15 -0
- package/lib/prices/snapshot-2026-05-23-kimi.json +14 -0
- package/lib/prices_fetcher.py +285 -0
- package/lib/roll-backlog.py +225 -0
- package/lib/roll-brief.py +286 -0
- package/lib/roll-help.py +158 -0
- package/lib/roll-home.py +556 -0
- package/lib/roll-init.py +156 -0
- package/lib/roll-loop-status.py +1683 -0
- package/lib/roll-loop-story.py +191 -0
- package/lib/roll-onboard-render.py +378 -0
- package/lib/roll-peer.py +252 -0
- package/lib/roll-plan-validate.py +386 -0
- package/lib/roll-setup.py +102 -0
- package/lib/roll-status.py +367 -0
- package/lib/roll_git.py +41 -0
- package/lib/roll_render.py +414 -0
- package/lib/slides/components/README.md +123 -0
- package/lib/slides/components/cards-2.html +9 -0
- package/lib/slides/components/cards-3.html +9 -0
- package/lib/slides/components/cards-4.html +9 -0
- package/lib/slides/components/compare.html +22 -0
- package/lib/slides/components/highlight.html +9 -0
- package/lib/slides/components/pipeline.html +12 -0
- package/lib/slides/components/plain.html +7 -0
- package/lib/slides/components/quote.html +4 -0
- package/lib/slides/components/timeline.html +9 -0
- package/lib/slides/templates/introduction-v3.html +571 -0
- package/lib/slides/templates/pitch.html +0 -0
- package/lib/slides-render.py +778 -0
- package/lib/slides-validate.py +357 -0
- package/lib/test_quality_gate.py +143 -0
- package/package.json +8 -7
- package/skills/roll-.changelog/SKILL.md +406 -33
- package/skills/roll-.clarify/SKILL.md +5 -2
- package/skills/roll-.dream/SKILL.md +374 -0
- package/skills/roll-.echo/SKILL.md +5 -2
- package/skills/roll-.qa/SKILL.md +57 -3
- package/skills/roll-.review/SKILL.md +42 -3
- package/skills/roll-brief/SKILL.md +209 -0
- package/skills/roll-build/SKILL.md +308 -63
- package/skills/roll-debug/SKILL.md +341 -162
- package/skills/roll-debug/injectable-bb.js +263 -0
- package/skills/roll-deck/SKILL.md +296 -0
- package/skills/roll-design/ENGINEERING_CHECKLIST.md +1 -1
- package/skills/roll-design/SKILL.md +727 -94
- package/skills/roll-doc/SKILL.md +595 -0
- package/skills/roll-doctor/SKILL.md +192 -0
- package/skills/roll-fix/SKILL.md +149 -32
- package/skills/{roll-jot → roll-idea}/SKILL.md +18 -10
- package/skills/roll-loop/SKILL.md +578 -0
- package/skills/roll-notes/SKILL.md +103 -0
- package/skills/roll-onboard/SKILL.md +234 -0
- package/skills/roll-peer/SKILL.md +336 -0
- package/skills/roll-propose/SKILL.md +157 -0
- package/skills/roll-review-pr/SKILL.md +58 -0
- package/skills/roll-sentinel/SKILL.md +11 -2
- package/skills/roll-spar/SKILL.md +8 -6
- package/template/.github/workflows/ci.yml +5 -2
- package/template/AGENTS.md +20 -74
- package/skills/roll-research/SKILL.md +0 -307
- package/skills/roll-research/references/schema.json +0 -162
- package/skills/roll-research/scripts/md_to_pdf.py +0 -289
- package/tools/roll-fetch/SKILL.md +0 -182
- package/tools/roll-fetch/package.json +0 -15
- package/tools/roll-fetch/smart-web-fetch.js +0 -558
- package/tools/roll-probe/SKILL.md +0 -84
- /package/template/{BACKLOG.md → .roll/backlog.md} +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
roll-backlog — v2 terminal view for `roll backlog`.
|
|
4
|
+
|
|
5
|
+
Parses .roll/backlog.md and renders items grouped by type:
|
|
6
|
+
Bug Fixes (red) · User Stories (blue) · Refactors (amber) · Ideas (dim)
|
|
7
|
+
|
|
8
|
+
In-progress items get a ⏵ purple marker.
|
|
9
|
+
Blocked and Deferred items appear in their own sections below.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
from typing import List, NamedTuple, Optional
|
|
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 as rr
|
|
22
|
+
from roll_render import c, pad, row, trunc, strw, COLS
|
|
23
|
+
|
|
24
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
25
|
+
# BACKLOG parsing
|
|
26
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
27
|
+
|
|
28
|
+
class Item(NamedTuple):
|
|
29
|
+
id: str
|
|
30
|
+
desc: str
|
|
31
|
+
status: str # raw status cell content
|
|
32
|
+
reason: str # extracted reason from Blocked/Deferred status
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_ID_RE = re.compile(r"\[([^\]]+)\]\([^)]+\)") # [US-XXX](link)
|
|
36
|
+
_REASON_RE = re.compile(r"\[([^\]]+)\]") # [reason text]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_id(cell: str) -> str:
|
|
40
|
+
m = _ID_RE.search(cell)
|
|
41
|
+
if m:
|
|
42
|
+
return m.group(1)
|
|
43
|
+
return cell.strip()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_reason(status_cell: str) -> str:
|
|
47
|
+
# Skip the leading emoji word, extract first [...] block
|
|
48
|
+
m = _REASON_RE.search(status_cell)
|
|
49
|
+
return m.group(1) if m else ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_backlog(path: str) -> List[Item]:
|
|
53
|
+
items: List[Item] = []
|
|
54
|
+
with open(path, encoding="utf-8") as f:
|
|
55
|
+
for line in f:
|
|
56
|
+
line = line.rstrip("\n")
|
|
57
|
+
if not line.startswith("|"):
|
|
58
|
+
continue
|
|
59
|
+
parts = [p.strip() for p in line.split("|")]
|
|
60
|
+
if len(parts) < 4:
|
|
61
|
+
continue
|
|
62
|
+
id_cell = parts[1]
|
|
63
|
+
desc_cell = parts[2]
|
|
64
|
+
status_cell = parts[3] if len(parts) > 3 else ""
|
|
65
|
+
item_id = _parse_id(id_cell)
|
|
66
|
+
if not re.match(r"(US|FIX|REFACTOR|IDEA)-", item_id):
|
|
67
|
+
continue
|
|
68
|
+
reason = _parse_reason(status_cell) if ("Blocked" in status_cell or "Deferred" in status_cell) else ""
|
|
69
|
+
items.append(Item(item_id, desc_cell, status_cell, reason))
|
|
70
|
+
return items
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def classify(items: List[Item]):
|
|
74
|
+
todo_fix: List[Item] = []
|
|
75
|
+
todo_us: List[Item] = []
|
|
76
|
+
todo_ref: List[Item] = []
|
|
77
|
+
todo_idea: List[Item] = []
|
|
78
|
+
in_progress: List[Item] = []
|
|
79
|
+
blocked: List[Item] = []
|
|
80
|
+
deferred: List[Item] = []
|
|
81
|
+
|
|
82
|
+
for it in items:
|
|
83
|
+
st = it.status
|
|
84
|
+
if "In Progress" in st:
|
|
85
|
+
in_progress.append(it)
|
|
86
|
+
elif "Blocked" in st:
|
|
87
|
+
blocked.append(it)
|
|
88
|
+
elif "Deferred" in st:
|
|
89
|
+
deferred.append(it)
|
|
90
|
+
elif "Todo" in st:
|
|
91
|
+
if it.id.startswith("FIX-"):
|
|
92
|
+
todo_fix.append(it)
|
|
93
|
+
elif it.id.startswith("US-"):
|
|
94
|
+
todo_us.append(it)
|
|
95
|
+
elif it.id.startswith("REFACTOR-"):
|
|
96
|
+
todo_ref.append(it)
|
|
97
|
+
elif it.id.startswith("IDEA-"):
|
|
98
|
+
todo_idea.append(it)
|
|
99
|
+
|
|
100
|
+
return todo_fix, todo_us, todo_ref, todo_idea, in_progress, blocked, deferred
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
104
|
+
# Rendering
|
|
105
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
106
|
+
|
|
107
|
+
_MAX_DESC = 62
|
|
108
|
+
|
|
109
|
+
BG_RUN = "\033[48;2;40;20;70m" # faint purple bg for in-progress row
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _render_item_row(it: Item, color: str, *, glyph: str = " ", bg: str = "") -> None:
|
|
113
|
+
"""Print one item line: glyph · ID · description."""
|
|
114
|
+
id_str = c(color, pad(it.id, 16))
|
|
115
|
+
desc = trunc(it.desc, _MAX_DESC)
|
|
116
|
+
desc_str = c(color, desc) if color != "dim" else c("dim", desc)
|
|
117
|
+
line = f" {glyph} {id_str} {desc_str}"
|
|
118
|
+
if bg and rr.USE_COLOR:
|
|
119
|
+
print(bg + line + rr.RESET)
|
|
120
|
+
else:
|
|
121
|
+
print(line)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _render_group(title_en: str, title_zh: str, color: str, items: List[Item]) -> None:
|
|
125
|
+
if not items:
|
|
126
|
+
return
|
|
127
|
+
n = len(items)
|
|
128
|
+
header = c(color, f" {title_en}", bold=True) + c("muted", " · ") + c("dim", title_zh) + c("muted", f" ({n})")
|
|
129
|
+
print(header)
|
|
130
|
+
for it in items:
|
|
131
|
+
_render_item_row(it, color)
|
|
132
|
+
print()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _render_in_progress(items: List[Item]) -> None:
|
|
136
|
+
for it in items:
|
|
137
|
+
glyph = c("purple", "⏵")
|
|
138
|
+
id_str = c("purple", pad(it.id, 16), bold=True)
|
|
139
|
+
desc = trunc(it.desc, _MAX_DESC)
|
|
140
|
+
desc_str = c("purple", desc)
|
|
141
|
+
line = f" {glyph} {id_str} {desc_str}"
|
|
142
|
+
if rr.USE_COLOR:
|
|
143
|
+
print(BG_RUN + line + rr.RESET)
|
|
144
|
+
else:
|
|
145
|
+
print(f" ⏵ {it.id} {it.desc}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def render(path: str) -> None:
|
|
149
|
+
items = parse_backlog(path)
|
|
150
|
+
todo_fix, todo_us, todo_ref, todo_idea, in_progress, blocked, deferred = classify(items)
|
|
151
|
+
|
|
152
|
+
todo_total = len(todo_fix) + len(todo_us) + len(todo_ref) + len(todo_idea)
|
|
153
|
+
blocked_count = len(blocked)
|
|
154
|
+
deferred_count = len(deferred)
|
|
155
|
+
|
|
156
|
+
# ── Header ──────────────────────────────────────────────────────────────
|
|
157
|
+
print()
|
|
158
|
+
pending_total = todo_total + len(in_progress)
|
|
159
|
+
tags = c("fg", f"{pending_total} Pending", bold=True)
|
|
160
|
+
if blocked_count:
|
|
161
|
+
tags += c("muted", " · ") + c("amber", f"{blocked_count} Blocked")
|
|
162
|
+
if deferred_count:
|
|
163
|
+
tags += c("muted", " · ") + c("dim", f"{deferred_count} Deferred")
|
|
164
|
+
header_left = " " + c("pink", "BACKLOG", bold=True) + c("muted", " · ") + c("dim", "待处理任务")
|
|
165
|
+
print(row(header_left, " " + tags))
|
|
166
|
+
print()
|
|
167
|
+
|
|
168
|
+
# ── In-progress (shown first, above groups) ──────────────────────────────
|
|
169
|
+
if in_progress:
|
|
170
|
+
_render_in_progress(in_progress)
|
|
171
|
+
print()
|
|
172
|
+
|
|
173
|
+
# ── Todo groups in priority order ────────────────────────────────────────
|
|
174
|
+
_render_group("Bug Fixes", "缺陷修复", "red", todo_fix)
|
|
175
|
+
_render_group("User Stories", "用户故事", "blue", todo_us)
|
|
176
|
+
_render_group("Refactors", "重构", "amber", todo_ref)
|
|
177
|
+
_render_group("Ideas", "创意", "dim", todo_idea)
|
|
178
|
+
|
|
179
|
+
if todo_total == 0 and len(in_progress) == 0:
|
|
180
|
+
print(c("green", " ✓ Nothing pending — backlog is clear 暂无待处理任务"))
|
|
181
|
+
print()
|
|
182
|
+
|
|
183
|
+
# ── Blocked ──────────────────────────────────────────────────────────────
|
|
184
|
+
if blocked:
|
|
185
|
+
print(c("amber", " Blocked", bold=True) + c("muted", " · ") + c("dim", "已阻塞") + c("muted", f" ({blocked_count})"))
|
|
186
|
+
for it in blocked:
|
|
187
|
+
id_str = c("amber", pad(it.id, 16))
|
|
188
|
+
desc = trunc(it.desc, 50)
|
|
189
|
+
reason_str = c("muted", f" ({it.reason})") if it.reason else ""
|
|
190
|
+
print(f" 🔒 {id_str} {c('dim', desc)}{reason_str}")
|
|
191
|
+
print()
|
|
192
|
+
|
|
193
|
+
# ── Deferred ─────────────────────────────────────────────────────────────
|
|
194
|
+
if deferred:
|
|
195
|
+
print(c("dim", f" Deferred · 已推迟 ({deferred_count})"))
|
|
196
|
+
for it in deferred:
|
|
197
|
+
id_str = c("dim", pad(it.id, 16))
|
|
198
|
+
desc = trunc(it.desc, 50)
|
|
199
|
+
reason_str = c("muted", f" ({it.reason})") if it.reason else ""
|
|
200
|
+
print(f" ⏸ {id_str} {c('dim', desc)}{reason_str}")
|
|
201
|
+
print()
|
|
202
|
+
|
|
203
|
+
# ── Footer ───────────────────────────────────────────────────────────────
|
|
204
|
+
print(c("muted", " ") + c("dim", "triage: ") + c("blue", "roll backlog block/defer/unblock <pattern> [reason]"))
|
|
205
|
+
print()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
209
|
+
# Entry
|
|
210
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
211
|
+
|
|
212
|
+
def main() -> None:
|
|
213
|
+
args = sys.argv[1:]
|
|
214
|
+
no_color = "--no-color" in args or not sys.stdout.isatty() or os.getenv("NO_COLOR")
|
|
215
|
+
rr.USE_COLOR = not no_color
|
|
216
|
+
|
|
217
|
+
backlog = ".roll/backlog.md"
|
|
218
|
+
if not os.path.isfile(backlog):
|
|
219
|
+
print(f"Error: {backlog} not found — run 'roll init' first", file=sys.stderr)
|
|
220
|
+
sys.exit(1)
|
|
221
|
+
render(backlog)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if __name__ == "__main__":
|
|
225
|
+
main()
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
roll-brief — v2 terminal view for `roll brief`.
|
|
4
|
+
|
|
5
|
+
Parses the latest .roll/briefs/<date>.md and renders it as three sections:
|
|
6
|
+
SUMMARY — eyebrow + shipped/watch/decide counts
|
|
7
|
+
HIGHLIGHTS — completed story list
|
|
8
|
+
DECIDE — action-required items with D1/D2/... numbering
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from typing import Dict, List, Tuple
|
|
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 as rr
|
|
22
|
+
from roll_render import c, pad, row, trunc, strw, COLS
|
|
23
|
+
|
|
24
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
25
|
+
# Brief parsing — section-based
|
|
26
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
27
|
+
|
|
28
|
+
def _age_str(mtime: float) -> str:
|
|
29
|
+
age_s = int(time.time() - mtime)
|
|
30
|
+
if age_s < 3600:
|
|
31
|
+
return f"{age_s // 60}m ago"
|
|
32
|
+
if age_s < 86400:
|
|
33
|
+
return f"{age_s // 3600}h ago"
|
|
34
|
+
return f"{age_s // 86400}d ago"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _split_sections(lines: List[str]) -> Dict[str, List[str]]:
|
|
38
|
+
"""Split markdown into {section_heading: [body_lines], ...}.
|
|
39
|
+
Key '' holds lines before the first section header.
|
|
40
|
+
Key '@title' holds the # title line.
|
|
41
|
+
"""
|
|
42
|
+
sections: Dict[str, List[str]] = {"": []}
|
|
43
|
+
current = ""
|
|
44
|
+
for line in lines:
|
|
45
|
+
if line.startswith("# "):
|
|
46
|
+
sections["@title"] = [line]
|
|
47
|
+
elif line.startswith("## "):
|
|
48
|
+
current = line[3:].strip()
|
|
49
|
+
sections[current] = []
|
|
50
|
+
else:
|
|
51
|
+
sections[current].append(line)
|
|
52
|
+
return sections
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_table(body: List[str]) -> List[Tuple[str, str]]:
|
|
56
|
+
"""Extract (id, desc) pairs from a markdown table body."""
|
|
57
|
+
rows = []
|
|
58
|
+
for line in body:
|
|
59
|
+
if not line.strip().startswith("|"):
|
|
60
|
+
continue
|
|
61
|
+
parts = [p.strip() for p in line.strip().split("|")]
|
|
62
|
+
if len(parts) < 3:
|
|
63
|
+
continue
|
|
64
|
+
cell0 = parts[1]
|
|
65
|
+
cell1 = parts[2] if len(parts) > 2 else ""
|
|
66
|
+
# skip separator rows like |---|---| and header rows
|
|
67
|
+
if re.match(r"[-:]+$", cell0.replace(" ", "")):
|
|
68
|
+
continue
|
|
69
|
+
if cell0.lower() in ("编号", "story", "id", ""):
|
|
70
|
+
continue
|
|
71
|
+
rows.append((cell0, cell1))
|
|
72
|
+
return rows
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _parse_numbered_list(body: List[str]) -> List[str]:
|
|
76
|
+
"""Extract items from a numbered markdown list."""
|
|
77
|
+
items = []
|
|
78
|
+
for line in body:
|
|
79
|
+
m = re.match(r"^\d+\.\s+(.*)", line.rstrip())
|
|
80
|
+
if m:
|
|
81
|
+
text = m.group(1)
|
|
82
|
+
text = re.sub(r"\*\*(.*?)\*\*", r"\1", text)
|
|
83
|
+
items.append(text.strip())
|
|
84
|
+
return items
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Brief:
|
|
88
|
+
def __init__(self) -> None:
|
|
89
|
+
self.path: str = ""
|
|
90
|
+
self.mtime: float = 0.0
|
|
91
|
+
self.title_date: str = ""
|
|
92
|
+
self.coverage: str = ""
|
|
93
|
+
self.shipped: List[Tuple[str, str]] = []
|
|
94
|
+
self.in_progress: List[Tuple[str, str]] = []
|
|
95
|
+
self.pending_count: int = 0
|
|
96
|
+
self.decide: List[str] = []
|
|
97
|
+
self.alert_count: int = 0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_brief(path: str) -> Brief:
|
|
101
|
+
b = Brief()
|
|
102
|
+
b.path = path
|
|
103
|
+
b.mtime = os.path.getmtime(path)
|
|
104
|
+
|
|
105
|
+
with open(path, encoding="utf-8") as f:
|
|
106
|
+
raw = f.read()
|
|
107
|
+
|
|
108
|
+
lines = raw.splitlines()
|
|
109
|
+
sections = _split_sections(lines)
|
|
110
|
+
|
|
111
|
+
# Title date
|
|
112
|
+
title_lines = sections.get("@title", [])
|
|
113
|
+
if title_lines:
|
|
114
|
+
m = re.search(r"(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})", title_lines[0])
|
|
115
|
+
if m:
|
|
116
|
+
b.title_date = m.group(1)
|
|
117
|
+
|
|
118
|
+
# Coverage from preamble blockquote
|
|
119
|
+
for line in sections.get("", []):
|
|
120
|
+
if line.startswith("> ") and "覆盖" in line:
|
|
121
|
+
b.coverage = line[2:].strip()
|
|
122
|
+
|
|
123
|
+
# 已完成
|
|
124
|
+
for key, body in sections.items():
|
|
125
|
+
if key.startswith("已完成"):
|
|
126
|
+
b.shipped = _parse_table(body)
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
# 进行中
|
|
130
|
+
for key, body in sections.items():
|
|
131
|
+
if key.startswith("进行中"):
|
|
132
|
+
b.in_progress = _parse_table(body)
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
# Pending count from section header
|
|
136
|
+
for key in sections:
|
|
137
|
+
m = re.search(r"待处理.*?(\d+)", key)
|
|
138
|
+
if m:
|
|
139
|
+
b.pending_count = int(m.group(1))
|
|
140
|
+
|
|
141
|
+
# 需人工介入
|
|
142
|
+
for key, body in sections.items():
|
|
143
|
+
if key.startswith("需人工介入"):
|
|
144
|
+
b.decide = _parse_numbered_list(body)
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
# Footer alert count
|
|
148
|
+
for line in lines:
|
|
149
|
+
if line.startswith("*状态") or ("告警" in line and line.startswith("*")):
|
|
150
|
+
m = re.search(r"告警\s*(\d+)", line)
|
|
151
|
+
if m:
|
|
152
|
+
b.alert_count = int(m.group(1))
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
return b
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
159
|
+
# Rendering
|
|
160
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
161
|
+
|
|
162
|
+
_MAX_DESC = 60
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _render_eyebrow(b: Brief) -> None:
|
|
166
|
+
brief_file = os.path.basename(b.path)
|
|
167
|
+
age = _age_str(b.mtime)
|
|
168
|
+
left = " " + c("pink", "BRIEF", bold=True) + c("muted", " · ") + c("dim", "简报")
|
|
169
|
+
right = c("dim", brief_file) + c("muted", " · ") + c("amber", age)
|
|
170
|
+
print(row(left, " " + right))
|
|
171
|
+
if b.title_date:
|
|
172
|
+
cov = (c("muted", " · ") + c("dim", b.coverage)) if b.coverage else ""
|
|
173
|
+
print(" " + c("dim", b.title_date) + cov)
|
|
174
|
+
print()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _render_summary(b: Brief) -> None:
|
|
178
|
+
n_shipped = len(b.shipped)
|
|
179
|
+
n_watch = len(b.in_progress)
|
|
180
|
+
n_decide = len(b.decide)
|
|
181
|
+
|
|
182
|
+
tags = c("green", f"✓ {n_shipped} Shipped") + c("muted", " · ")
|
|
183
|
+
tags += (c("amber", f"! {n_watch} Watch") if n_watch else c("dim", f"! {n_watch} Watch"))
|
|
184
|
+
tags += c("muted", " · ")
|
|
185
|
+
tags += (c("amber", f"⚠ {n_decide} Decide") if n_decide else c("dim", f"⚠ {n_decide} Decide"))
|
|
186
|
+
|
|
187
|
+
left = " " + c("fg", "SUMMARY", bold=True) + c("muted", " · ") + c("dim", "摘要")
|
|
188
|
+
print(row(left, " " + tags))
|
|
189
|
+
print()
|
|
190
|
+
|
|
191
|
+
print(" " + c("green", "✓") + c("muted", " ") +
|
|
192
|
+
c("fg", f"{n_shipped} Shipped", bold=True) +
|
|
193
|
+
c("dim", f" · 已完成 {n_shipped} 项"))
|
|
194
|
+
|
|
195
|
+
if n_watch:
|
|
196
|
+
print(" " + c("amber", "!") + c("muted", " ") +
|
|
197
|
+
c("amber", f"{n_watch} Watch", bold=True) +
|
|
198
|
+
c("dim", f" · 进行中 {n_watch} 项"))
|
|
199
|
+
else:
|
|
200
|
+
print(" " + c("dim", "!") + c("muted", " ") + c("dim", f"{n_watch} Watch"))
|
|
201
|
+
|
|
202
|
+
if n_decide:
|
|
203
|
+
print(" " + c("amber", "⚠") + c("muted", " ") +
|
|
204
|
+
c("amber", f"{n_decide} Decide", bold=True) +
|
|
205
|
+
c("dim", f" · 需人工介入 {n_decide} 项"))
|
|
206
|
+
else:
|
|
207
|
+
print(" " + c("dim", "⚠") + c("muted", " ") + c("dim", f"{n_decide} Decide"))
|
|
208
|
+
|
|
209
|
+
if b.pending_count:
|
|
210
|
+
print(" " + c("dim", f"· {b.pending_count} 项待处理"))
|
|
211
|
+
|
|
212
|
+
print()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _render_highlights(b: Brief) -> None:
|
|
216
|
+
if not b.shipped:
|
|
217
|
+
return
|
|
218
|
+
n = len(b.shipped)
|
|
219
|
+
header = (c("blue", " HIGHLIGHTS", bold=True) +
|
|
220
|
+
c("muted", " · ") + c("dim", "已完成") + c("muted", f" ({n})"))
|
|
221
|
+
print(header)
|
|
222
|
+
for (id_cell, desc) in b.shipped:
|
|
223
|
+
desc_t = trunc(desc, _MAX_DESC)
|
|
224
|
+
print(" " + c("muted", "— ") + c("blue", pad(id_cell, 18)) + " " + c("dim", desc_t))
|
|
225
|
+
print()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _render_decide(b: Brief) -> None:
|
|
229
|
+
if not b.decide:
|
|
230
|
+
return
|
|
231
|
+
n = len(b.decide)
|
|
232
|
+
header = (c("amber", " DECIDE", bold=True) +
|
|
233
|
+
c("muted", " · ") + c("dim", "需人工介入") + c("muted", f" ({n})"))
|
|
234
|
+
print(header)
|
|
235
|
+
print()
|
|
236
|
+
for idx, item in enumerate(b.decide, start=1):
|
|
237
|
+
label = c("amber", f" D{idx}", bold=True)
|
|
238
|
+
body = trunc(item, COLS - 8)
|
|
239
|
+
print(f"{label} {c('fg', body)}")
|
|
240
|
+
print()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _render_footer() -> None:
|
|
244
|
+
parts = [
|
|
245
|
+
c("dim", "next: ") + c("blue", "roll loop now"),
|
|
246
|
+
c("dim", "regen: ") + c("blue", "roll brief --regen"),
|
|
247
|
+
c("dim", "alerts: ") + c("blue", "roll alert"),
|
|
248
|
+
]
|
|
249
|
+
print(" " + c("muted", " · ").join(parts))
|
|
250
|
+
print()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def render(path: str) -> None:
|
|
254
|
+
b = parse_brief(path)
|
|
255
|
+
print()
|
|
256
|
+
_render_eyebrow(b)
|
|
257
|
+
_render_summary(b)
|
|
258
|
+
_render_highlights(b)
|
|
259
|
+
_render_decide(b)
|
|
260
|
+
_render_footer()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
264
|
+
# Entry
|
|
265
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
266
|
+
|
|
267
|
+
def main() -> None:
|
|
268
|
+
args = sys.argv[1:]
|
|
269
|
+
no_color = "--no-color" in args or not sys.stdout.isatty() or os.getenv("NO_COLOR")
|
|
270
|
+
rr.USE_COLOR = not no_color
|
|
271
|
+
|
|
272
|
+
briefs_dir = ".roll/briefs"
|
|
273
|
+
briefs = sorted(
|
|
274
|
+
f for f in os.listdir(briefs_dir) if f.endswith(".md")
|
|
275
|
+
) if os.path.isdir(briefs_dir) else []
|
|
276
|
+
|
|
277
|
+
if not briefs:
|
|
278
|
+
print("No brief yet — run 'roll brief --regen' 暂无简报", file=sys.stderr)
|
|
279
|
+
sys.exit(1)
|
|
280
|
+
|
|
281
|
+
latest = os.path.join(briefs_dir, briefs[-1])
|
|
282
|
+
render(latest)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
main()
|
package/lib/roll-help.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
roll-help — render the `roll --help` page.
|
|
4
|
+
|
|
5
|
+
Compact wordmark + grouped commands (AUTONOMY / PROJECT / MACHINE) + examples.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python3 lib/roll-help.py # live
|
|
9
|
+
python3 lib/roll-help.py --no-color
|
|
10
|
+
python3 lib/roll-help.py # static layout, no fixture needed
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
import argparse, os, re, sys
|
|
15
|
+
from pathlib import Path
|
|
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 COLS, c, row, section_head, strw, pad
|
|
22
|
+
|
|
23
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
24
|
+
# Version
|
|
25
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
26
|
+
def _roll_version() -> str:
|
|
27
|
+
roll_bin = Path(_LIB_DIR).parent / "bin" / "roll"
|
|
28
|
+
if roll_bin.exists():
|
|
29
|
+
for line in roll_bin.open(errors="ignore"):
|
|
30
|
+
m = re.match(r'^VERSION="([^"]+)"', line)
|
|
31
|
+
if m:
|
|
32
|
+
return m.group(1)
|
|
33
|
+
return "—"
|
|
34
|
+
|
|
35
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
36
|
+
# Command table
|
|
37
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
38
|
+
# (name, args_hint, en_desc, zh_desc, star)
|
|
39
|
+
AUTONOMY = [
|
|
40
|
+
("loop", "<on|off|now|status|…>", "manage the autonomous BACKLOG executor", "管理自主执行循环", True),
|
|
41
|
+
("brief", "", "show latest owner brief", "查看最新简报", True),
|
|
42
|
+
("backlog", "[block|defer|…]", "view and manage pending tasks", "查看和管理待处理任务", True),
|
|
43
|
+
("peer", "", "cross-agent negotiation & review", "跨 Agent 协商对审", False),
|
|
44
|
+
("alert", "", "view and clear loop alerts", "查看 / 清除 loop 告警", False),
|
|
45
|
+
("feedback", "--type bug|idea|ux …", "open a GitHub issue for this project", "为本项目提交反馈", False),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
PROJECT = [
|
|
49
|
+
("init", "", "create AGENTS.md + .roll/backlog.md + .roll/features/", "初始化项目工作流文件", False),
|
|
50
|
+
("status", "", "show current state and drift", "显示当前状态和漂移项", False),
|
|
51
|
+
("agent", "[use <name>]", "per-project agent selection", "切换项目 agent", False),
|
|
52
|
+
("ci", "[--wait]", "show or wait for current commit's CI status", "查看 / 等待 CI 状态", False),
|
|
53
|
+
("release", "", "run the release script (human-only)", "执行发版脚本(仅人工)", False),
|
|
54
|
+
("review-pr", "<number>", "AI-powered code review for a PR", "AI 代码评审", False),
|
|
55
|
+
("slides", "build <slug>", "render a deck.md to HTML and open in browser", "渲染 deck.md 为 HTML 并打开", False),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
MACHINE = [
|
|
59
|
+
("setup", "[-f]", "first-time install or re-sync", "首次安装或重新同步", False),
|
|
60
|
+
("update", "", "upgrade to latest + re-sync", "升级到最新版并重新同步", False),
|
|
61
|
+
("version", "", "print installed roll version", "显示已安装版本", False),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
EXAMPLES = [
|
|
65
|
+
("roll loop on", "启用自主执行循环"),
|
|
66
|
+
("roll brief", "查看最新简报"),
|
|
67
|
+
("roll backlog defer US-DOC '过早引入'", "推迟一类任务"),
|
|
68
|
+
("roll agent use kimi", "切换当前项目到 kimi"),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
72
|
+
# Render
|
|
73
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
74
|
+
def _hr() -> None:
|
|
75
|
+
print(c("faint", "─" * COLS))
|
|
76
|
+
|
|
77
|
+
def _cmd_block(entries: list) -> None:
|
|
78
|
+
"""Render a command group — two lines per command (EN + ZH)."""
|
|
79
|
+
for name, args, en_desc, zh_desc, star in entries:
|
|
80
|
+
star_mark = c("amber", " ★") if star else " "
|
|
81
|
+
en_line = (
|
|
82
|
+
" " +
|
|
83
|
+
c("blue", name, bold=True) +
|
|
84
|
+
star_mark +
|
|
85
|
+
" " +
|
|
86
|
+
(c("dim", args + " ") if args else " ") +
|
|
87
|
+
c("fg", en_desc)
|
|
88
|
+
)
|
|
89
|
+
zh_line = " " + " " * (strw(name) + 2 + 2) + c("dim", zh_desc)
|
|
90
|
+
print(en_line)
|
|
91
|
+
print(zh_line)
|
|
92
|
+
|
|
93
|
+
def render(version: str) -> None:
|
|
94
|
+
# ── Wordmark ──────────────────────────────────────────────────────────────
|
|
95
|
+
print()
|
|
96
|
+
left = (" " + c("fg", "roll", bold=True) + c("muted", " · ") +
|
|
97
|
+
c("dim", "autonomous delivery for software teams"))
|
|
98
|
+
right = c("yellow", f"v{version}") + " "
|
|
99
|
+
print(row(left, right))
|
|
100
|
+
print(" " + c("dim", "自主交付,人只做三件事:提需求、审核、发版"))
|
|
101
|
+
print()
|
|
102
|
+
print(" " + c("dim", "usage ") + c("fg", "roll") + c("dim", " <command> [options]"))
|
|
103
|
+
print()
|
|
104
|
+
_hr()
|
|
105
|
+
print()
|
|
106
|
+
|
|
107
|
+
# ── AUTONOMY ──────────────────────────────────────────────────────────────
|
|
108
|
+
section_head("AUTONOMY", "日常使用", "★ = most used")
|
|
109
|
+
print()
|
|
110
|
+
_cmd_block(AUTONOMY)
|
|
111
|
+
print()
|
|
112
|
+
_hr()
|
|
113
|
+
print()
|
|
114
|
+
|
|
115
|
+
# ── PROJECT ───────────────────────────────────────────────────────────────
|
|
116
|
+
section_head("PROJECT", "项目内", "per-repo setup and CI")
|
|
117
|
+
print()
|
|
118
|
+
_cmd_block(PROJECT)
|
|
119
|
+
print()
|
|
120
|
+
_hr()
|
|
121
|
+
print()
|
|
122
|
+
|
|
123
|
+
# ── MACHINE ───────────────────────────────────────────────────────────────
|
|
124
|
+
section_head("MACHINE", "全局", "install, upgrade, version")
|
|
125
|
+
print()
|
|
126
|
+
_cmd_block(MACHINE)
|
|
127
|
+
print()
|
|
128
|
+
_hr()
|
|
129
|
+
print()
|
|
130
|
+
|
|
131
|
+
# ── Examples ──────────────────────────────────────────────────────────────
|
|
132
|
+
print(" " + c("muted", "examples"))
|
|
133
|
+
print()
|
|
134
|
+
for cmd, zh in EXAMPLES:
|
|
135
|
+
print(" " + c("blue", cmd) + " " + c("dim", zh))
|
|
136
|
+
print()
|
|
137
|
+
print(" " + c("dim", "docs: ") + c("blue", "github.com/seanyao/roll") +
|
|
138
|
+
c("muted", " · ") +
|
|
139
|
+
c("dim", "issues: ") + c("blue", "github.com/seanyao/roll/issues"))
|
|
140
|
+
print()
|
|
141
|
+
|
|
142
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
143
|
+
# Entry
|
|
144
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
145
|
+
def main() -> None:
|
|
146
|
+
ap = argparse.ArgumentParser(add_help=False)
|
|
147
|
+
ap.add_argument("--no-color", dest="no_color", action="store_true")
|
|
148
|
+
ap.add_argument("--en", action="store_true")
|
|
149
|
+
ap.add_argument("--zh", action="store_true")
|
|
150
|
+
args, _ = ap.parse_known_args()
|
|
151
|
+
|
|
152
|
+
if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
|
|
153
|
+
roll_render.USE_COLOR = False
|
|
154
|
+
|
|
155
|
+
render(_roll_version())
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
main()
|