@seanyao/roll 2026.518.2 → 2026.518.4
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 +26 -0
- package/bin/roll +233 -92
- package/conventions/global/AGENTS.md +1 -0
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_render.cpython-314.pyc +0 -0
- package/lib/roll-backlog.py +262 -0
- package/lib/roll-brief.py +286 -0
- package/lib/roll-help.py +157 -0
- package/lib/roll-home.py +493 -0
- package/lib/roll-loop-status.py +16 -8
- package/lib/roll-setup.py +62 -0
- package/lib/roll-status.py +368 -0
- package/package.json +1 -1
- package/skills/roll-fix/SKILL.md +25 -0
package/lib/roll-loop-status.py
CHANGED
|
@@ -315,7 +315,8 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
|
|
|
315
315
|
for cy in cycles:
|
|
316
316
|
# Path 1: usage event written by loop-fmt at result time.
|
|
317
317
|
ue = cy.get("usage_event")
|
|
318
|
-
if isinstance(ue, dict) and (ue.get("input_tokens") or ue.get("output_tokens")
|
|
318
|
+
if isinstance(ue, dict) and (ue.get("input_tokens") or ue.get("output_tokens")
|
|
319
|
+
or ue.get("cost_reported_usd")):
|
|
319
320
|
cy["tokens"] = mp.total_tokens(
|
|
320
321
|
input_tokens=ue.get("input_tokens", 0),
|
|
321
322
|
output_tokens=ue.get("output_tokens", 0),
|
|
@@ -323,13 +324,20 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
|
|
|
323
324
|
cache_read_tokens=ue.get("cache_read_tokens", 0),
|
|
324
325
|
)
|
|
325
326
|
cy["model"] = ue.get("model")
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
327
|
+
# FIX-060: cost_reported_usd is the authoritative cumulative session
|
|
328
|
+
# cost written by loop-fmt; use it directly instead of recomputing
|
|
329
|
+
# from the last individual API-call token counts (which would only
|
|
330
|
+
# reflect one small call and badly undercount the true cycle cost).
|
|
331
|
+
if ue.get("cost_reported_usd"):
|
|
332
|
+
cy["cost_list"] = float(ue["cost_reported_usd"])
|
|
333
|
+
else:
|
|
334
|
+
cy["cost_list"] = mp.compute_list_cost(
|
|
335
|
+
ue.get("model"),
|
|
336
|
+
input_tokens=ue.get("input_tokens", 0),
|
|
337
|
+
output_tokens=ue.get("output_tokens", 0),
|
|
338
|
+
cache_creation_tokens=ue.get("cache_creation_tokens", 0),
|
|
339
|
+
cache_read_tokens=ue.get("cache_read_tokens", 0),
|
|
340
|
+
)
|
|
333
341
|
if ue.get("duration_ms") and not cy.get("duration_s"):
|
|
334
342
|
cy["duration_s"] = int(ue["duration_ms"] / 1000)
|
|
335
343
|
continue
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""roll-setup — v2 terminal view for `roll setup`."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
_LIB_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
9
|
+
if _LIB_DIR not in sys.path:
|
|
10
|
+
sys.path.insert(0, _LIB_DIR)
|
|
11
|
+
from roll_render import c, row, COLS
|
|
12
|
+
|
|
13
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
14
|
+
# Demo data
|
|
15
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
_DEMO_STEPS = [
|
|
18
|
+
"Detect platform & shell",
|
|
19
|
+
"Fetch latest roll version",
|
|
20
|
+
"Install skills to ~/.claude",
|
|
21
|
+
"Symlink bin/roll to PATH",
|
|
22
|
+
"Check for config drift",
|
|
23
|
+
"Apply convention templates",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
27
|
+
# Rendering
|
|
28
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
|
|
30
|
+
def _divider(char: str = "─") -> None:
|
|
31
|
+
print(c("dim", char * min(COLS, 80)))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def render_demo() -> None:
|
|
35
|
+
left = " " + c("blue", "SETUP", bold=True) + c("dim", " · ") + c("dim", "初始化")
|
|
36
|
+
right = c("dim", "--demo")
|
|
37
|
+
print(row(left, " " + right))
|
|
38
|
+
_divider()
|
|
39
|
+
print()
|
|
40
|
+
|
|
41
|
+
for i, label in enumerate(_DEMO_STEPS, start=1):
|
|
42
|
+
num = c("dim", f" {i}.")
|
|
43
|
+
icon = c("green", "✓")
|
|
44
|
+
print(f"{num} {icon} {label}")
|
|
45
|
+
|
|
46
|
+
print()
|
|
47
|
+
_divider()
|
|
48
|
+
msg = c("green", "Setup complete")
|
|
49
|
+
print(f" {msg} — run {c('fg', 'roll init', bold=True)} inside a project to begin")
|
|
50
|
+
_divider("═")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
54
|
+
# Entry point
|
|
55
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
56
|
+
|
|
57
|
+
def main() -> None:
|
|
58
|
+
render_demo()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
main()
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
roll-status — render the `roll status` page.
|
|
4
|
+
|
|
5
|
+
One-screen sync health: global conventions, AI clients table (with drift fix hints),
|
|
6
|
+
project templates, and this-project metrics.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 lib/roll-status.py # live data
|
|
10
|
+
python3 lib/roll-status.py --no-color
|
|
11
|
+
python3 lib/roll-status.py --demo # render with fixture data
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
import argparse, os, re, subprocess, sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
_LIB_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
20
|
+
if _LIB_DIR not in sys.path:
|
|
21
|
+
sys.path.insert(0, _LIB_DIR)
|
|
22
|
+
import roll_render
|
|
23
|
+
from roll_render import COLS, c, row, section_head, strw, pad
|
|
24
|
+
|
|
25
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
26
|
+
# Paths
|
|
27
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
28
|
+
def _roll_home() -> Path:
|
|
29
|
+
return Path(os.environ.get("ROLL_HOME") or os.path.expanduser("~/.roll"))
|
|
30
|
+
|
|
31
|
+
def _global_dir() -> Path:
|
|
32
|
+
return _roll_home() / "conventions" / "global"
|
|
33
|
+
|
|
34
|
+
def _templates_dir() -> Path:
|
|
35
|
+
return _roll_home() / "conventions" / "templates"
|
|
36
|
+
|
|
37
|
+
def _config_path() -> Path:
|
|
38
|
+
return _roll_home() / "config.yaml"
|
|
39
|
+
|
|
40
|
+
def _shared_root() -> Path:
|
|
41
|
+
return Path(os.environ.get("ROLL_SHARED_ROOT") or os.path.expanduser("~/.shared/roll"))
|
|
42
|
+
|
|
43
|
+
def _project_slug() -> str:
|
|
44
|
+
path = os.path.realpath(os.getcwd())
|
|
45
|
+
try:
|
|
46
|
+
common = subprocess.check_output(
|
|
47
|
+
["git", "-C", path, "rev-parse", "--git-common-dir"],
|
|
48
|
+
stderr=subprocess.DEVNULL, text=True,
|
|
49
|
+
).strip()
|
|
50
|
+
if common.endswith("/.git"):
|
|
51
|
+
path = common[:-5]
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
import hashlib
|
|
55
|
+
base = re.sub(r"[^A-Za-z0-9]+", "-", os.path.basename(path)).strip("-")
|
|
56
|
+
h = hashlib.md5(path.encode()).hexdigest()[:6]
|
|
57
|
+
return f"{base}-{h}"
|
|
58
|
+
|
|
59
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
60
|
+
# Data loaders
|
|
61
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
62
|
+
CONVENTION_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md", ".cursor-rules", "project_rules.md"]
|
|
63
|
+
TEMPLATES = ["fullstack", "frontend-only", "backend-service", "cli"]
|
|
64
|
+
|
|
65
|
+
def _global_conventions() -> List[Tuple[str, bool]]:
|
|
66
|
+
gd = _global_dir()
|
|
67
|
+
return [(f, (gd / f).exists()) for f in CONVENTION_FILES]
|
|
68
|
+
|
|
69
|
+
def _parse_ai_entries() -> List[Dict[str, str]]:
|
|
70
|
+
cfg = _config_path()
|
|
71
|
+
if not cfg.exists():
|
|
72
|
+
return []
|
|
73
|
+
entries = []
|
|
74
|
+
for line in cfg.read_text(errors="ignore").splitlines():
|
|
75
|
+
m = re.match(r"^ai_[a-z]+:\s*(.+)", line)
|
|
76
|
+
if not m:
|
|
77
|
+
continue
|
|
78
|
+
val = m.group(1).strip().replace("~", str(Path.home()))
|
|
79
|
+
parts = val.split("|")
|
|
80
|
+
if len(parts) < 3:
|
|
81
|
+
continue
|
|
82
|
+
ai_dir, cfg_file, src_file = parts[0].strip(), parts[1].strip(), parts[2].strip()
|
|
83
|
+
name = os.path.basename(ai_dir).lstrip(".")
|
|
84
|
+
if name in ("workspace", "agent"):
|
|
85
|
+
name = os.path.basename(os.path.dirname(ai_dir)).lstrip(".")
|
|
86
|
+
entries.append({"name": name, "ai_dir": ai_dir, "cfg_file": cfg_file, "src_file": src_file})
|
|
87
|
+
return entries
|
|
88
|
+
|
|
89
|
+
def _ai_sync_status(entry: Dict[str, str]) -> str:
|
|
90
|
+
ai_dir = Path(entry["ai_dir"])
|
|
91
|
+
cfg_file = ai_dir / entry["cfg_file"]
|
|
92
|
+
roll_md = ai_dir / "roll.md"
|
|
93
|
+
src = _global_dir() / entry["src_file"]
|
|
94
|
+
|
|
95
|
+
if not cfg_file.exists():
|
|
96
|
+
return "missing"
|
|
97
|
+
if not roll_md.exists():
|
|
98
|
+
return "out-of-sync"
|
|
99
|
+
try:
|
|
100
|
+
if src.exists() and roll_md.read_bytes() != src.read_bytes():
|
|
101
|
+
return "out-of-sync"
|
|
102
|
+
except Exception:
|
|
103
|
+
return "out-of-sync"
|
|
104
|
+
try:
|
|
105
|
+
if "@roll.md" not in cfg_file.read_text(errors="ignore"):
|
|
106
|
+
return "out-of-sync"
|
|
107
|
+
except Exception:
|
|
108
|
+
return "out-of-sync"
|
|
109
|
+
return "sync"
|
|
110
|
+
|
|
111
|
+
def _ai_skill_count(entry: Dict[str, str]) -> int:
|
|
112
|
+
skills_dir = Path(entry["ai_dir"]) / "skills"
|
|
113
|
+
if not skills_dir.exists():
|
|
114
|
+
return 0
|
|
115
|
+
try:
|
|
116
|
+
return sum(1 for p in skills_dir.iterdir() if p.name.startswith("roll-") and (p.is_symlink() or p.is_dir()))
|
|
117
|
+
except Exception:
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
def _template_count(tpl: str) -> int:
|
|
121
|
+
d = _templates_dir() / tpl
|
|
122
|
+
if not d.exists():
|
|
123
|
+
return 0
|
|
124
|
+
try:
|
|
125
|
+
return sum(1 for p in d.rglob("*") if p.is_file())
|
|
126
|
+
except Exception:
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
def _skills_installed() -> int:
|
|
130
|
+
sd = _roll_home() / "skills"
|
|
131
|
+
if not sd.exists():
|
|
132
|
+
return 0
|
|
133
|
+
try:
|
|
134
|
+
return sum(1 for p in sd.iterdir() if p.is_dir())
|
|
135
|
+
except Exception:
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
def _launchd_state(service: str, slug: str) -> str:
|
|
139
|
+
label = f"com.roll.{service}.{slug}"
|
|
140
|
+
plist = Path(os.path.expanduser("~/Library/LaunchAgents")) / f"{label}.plist"
|
|
141
|
+
if not plist.exists():
|
|
142
|
+
return "not-installed"
|
|
143
|
+
try:
|
|
144
|
+
out = subprocess.check_output(
|
|
145
|
+
["launchctl", "list", label], stderr=subprocess.DEVNULL, text=True,
|
|
146
|
+
)
|
|
147
|
+
return "enabled" if out.strip() else "installed-off"
|
|
148
|
+
except Exception:
|
|
149
|
+
return "installed-off"
|
|
150
|
+
|
|
151
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
152
|
+
# Demo fixture
|
|
153
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
154
|
+
def _demo_data() -> Dict[str, Any]:
|
|
155
|
+
return dict(
|
|
156
|
+
conventions=[
|
|
157
|
+
("AGENTS.md", True), ("CLAUDE.md", True), ("GEMINI.md", False),
|
|
158
|
+
(".cursor-rules", True), ("project_rules.md", False),
|
|
159
|
+
],
|
|
160
|
+
ai_clients=[
|
|
161
|
+
{"name": "claude", "cfg_file": "CLAUDE.md", "path": "~/.claude/CLAUDE.md", "sync": "sync", "skills": 12},
|
|
162
|
+
{"name": "cursor", "cfg_file": "AGENTS.md", "path": "~/.cursor/AGENTS.md", "sync": "out-of-sync", "skills": 12},
|
|
163
|
+
{"name": "gemini", "cfg_file": "GEMINI.md", "path": "~/.gemini/GEMINI.md", "sync": "missing", "skills": 0},
|
|
164
|
+
],
|
|
165
|
+
templates=[
|
|
166
|
+
("fullstack", 14), ("frontend-only", 9), ("backend-service", 11), ("cli", 7),
|
|
167
|
+
],
|
|
168
|
+
skills_installed=12,
|
|
169
|
+
project_has_agents=True,
|
|
170
|
+
project_has_backlog=True,
|
|
171
|
+
project_features_count=23,
|
|
172
|
+
loop_state="enabled",
|
|
173
|
+
dream_state="not-installed",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
177
|
+
# Render helpers
|
|
178
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
179
|
+
def _hr() -> None:
|
|
180
|
+
print(c("faint", "─" * COLS))
|
|
181
|
+
|
|
182
|
+
def _render_health(d: Dict[str, Any]) -> None:
|
|
183
|
+
clients = d["ai_clients"]
|
|
184
|
+
synced = sum(1 for x in clients if x["sync"] == "sync")
|
|
185
|
+
total = len(clients)
|
|
186
|
+
skills = d["skills_installed"]
|
|
187
|
+
tpls = len([t for t in d["templates"] if t[1] > 0])
|
|
188
|
+
|
|
189
|
+
has_drift = synced < total
|
|
190
|
+
if has_drift:
|
|
191
|
+
dot = c("amber", "!")
|
|
192
|
+
word = c("amber", "drift", bold=True)
|
|
193
|
+
detail = (c("dim", f" {synced}/{total} AI clients in sync") + c("muted", " · ") +
|
|
194
|
+
c("dim", f"{skills} skills") + c("muted", " · ") +
|
|
195
|
+
c("dim", f"{tpls} templates"))
|
|
196
|
+
else:
|
|
197
|
+
dot = c("green", "●")
|
|
198
|
+
word = c("green", "healthy", bold=True)
|
|
199
|
+
detail = (c("dim", f" {synced}/{total} AI clients in sync") + c("muted", " · ") +
|
|
200
|
+
c("dim", f"{skills} skills mounted") + c("muted", " · ") +
|
|
201
|
+
c("dim", f"{tpls} templates present"))
|
|
202
|
+
|
|
203
|
+
print()
|
|
204
|
+
print(" " + dot + " " + word + detail)
|
|
205
|
+
print()
|
|
206
|
+
_hr()
|
|
207
|
+
print()
|
|
208
|
+
|
|
209
|
+
def _render_global_conventions(conventions: list) -> None:
|
|
210
|
+
section_head("GLOBAL CONVENTIONS", "全局约定", "~/.roll/conventions/global/")
|
|
211
|
+
print()
|
|
212
|
+
for fname, exists in conventions:
|
|
213
|
+
if exists:
|
|
214
|
+
print(" " + c("green", "+") + " " + c("fg", fname))
|
|
215
|
+
else:
|
|
216
|
+
print(" " + c("red", "−") + " " + c("dim", fname) + " " + c("red", "missing"))
|
|
217
|
+
print()
|
|
218
|
+
_hr()
|
|
219
|
+
print()
|
|
220
|
+
|
|
221
|
+
def _render_ai_clients(clients: list) -> None:
|
|
222
|
+
section_head("AI CLIENTS", "AI 客户端同步", "convention · path · sync · skills")
|
|
223
|
+
print()
|
|
224
|
+
|
|
225
|
+
# Header
|
|
226
|
+
hdr = (" " + pad(c("dim", "name"), 14) +
|
|
227
|
+
pad(c("dim", "convention"), 14) +
|
|
228
|
+
pad(c("dim", "sync"), 14) +
|
|
229
|
+
c("dim", "skills"))
|
|
230
|
+
print(hdr)
|
|
231
|
+
print(" " + c("faint", "─" * (COLS - 4)))
|
|
232
|
+
|
|
233
|
+
for cl in clients:
|
|
234
|
+
sync_s = cl["sync"]
|
|
235
|
+
name = cl["name"]
|
|
236
|
+
cfg = cl["cfg_file"]
|
|
237
|
+
path = cl.get("path", "")
|
|
238
|
+
sk = cl.get("skills", 0)
|
|
239
|
+
|
|
240
|
+
if sync_s == "sync":
|
|
241
|
+
sync_col = c("green", "✓ in sync")
|
|
242
|
+
name_col = c("fg", name)
|
|
243
|
+
elif sync_s == "out-of-sync":
|
|
244
|
+
sync_col = c("amber", "~ out of sync")
|
|
245
|
+
name_col = c("amber", name)
|
|
246
|
+
else:
|
|
247
|
+
sync_col = c("red", "− missing")
|
|
248
|
+
name_col = c("red", name)
|
|
249
|
+
|
|
250
|
+
row_line = (" " + pad(name_col, 14) +
|
|
251
|
+
pad(c("dim", cfg), 14) +
|
|
252
|
+
pad(sync_col, 14) +
|
|
253
|
+
c("dim", str(sk)))
|
|
254
|
+
print(row_line)
|
|
255
|
+
|
|
256
|
+
if sync_s in ("out-of-sync", "missing"):
|
|
257
|
+
hint = (" " + c("dim", "fix: ") +
|
|
258
|
+
c("blue", f"roll setup -f {name}"))
|
|
259
|
+
print(hint)
|
|
260
|
+
|
|
261
|
+
print()
|
|
262
|
+
_hr()
|
|
263
|
+
print()
|
|
264
|
+
|
|
265
|
+
def _render_templates(templates: list) -> None:
|
|
266
|
+
section_head("PROJECT TEMPLATES", "项目模板", "~/.roll/conventions/templates/")
|
|
267
|
+
print()
|
|
268
|
+
parts = []
|
|
269
|
+
for tpl, count in templates:
|
|
270
|
+
if count > 0:
|
|
271
|
+
parts.append(c("fg", tpl) + c("dim", f" {count}f"))
|
|
272
|
+
else:
|
|
273
|
+
parts.append(c("red", "−") + " " + c("dim", tpl + " missing"))
|
|
274
|
+
print(" " + c("muted", " · ").join(parts))
|
|
275
|
+
print()
|
|
276
|
+
_hr()
|
|
277
|
+
print()
|
|
278
|
+
|
|
279
|
+
def _render_this_project(d: Dict[str, Any]) -> None:
|
|
280
|
+
section_head("THIS PROJECT", "本项目", os.path.basename(os.getcwd()))
|
|
281
|
+
print()
|
|
282
|
+
|
|
283
|
+
def _file_row(label: str, exists: bool, detail: str = "") -> None:
|
|
284
|
+
if exists:
|
|
285
|
+
sym = c("green", "+")
|
|
286
|
+
lbl = c("fg", label)
|
|
287
|
+
else:
|
|
288
|
+
sym = c("red", "−")
|
|
289
|
+
lbl = c("dim", label) + " " + c("red", "missing")
|
|
290
|
+
line = " " + sym + " " + lbl
|
|
291
|
+
if detail and exists:
|
|
292
|
+
line += c("dim", f" {detail}")
|
|
293
|
+
print(line)
|
|
294
|
+
|
|
295
|
+
_file_row("AGENTS.md", d["project_has_agents"])
|
|
296
|
+
_file_row("BACKLOG.md", d["project_has_backlog"])
|
|
297
|
+
_file_row("docs/features/", d["project_features_count"] > 0,
|
|
298
|
+
f"{d['project_features_count']} feature docs")
|
|
299
|
+
|
|
300
|
+
# Loop & dream launchd
|
|
301
|
+
for svc, state_key in [("loop", "loop_state"), ("dream", "dream_state")]:
|
|
302
|
+
state = d.get(state_key, "not-installed")
|
|
303
|
+
if state == "enabled":
|
|
304
|
+
dot = c("green", "●")
|
|
305
|
+
word = c("green", f"{svc} · launchd enabled")
|
|
306
|
+
elif state == "installed-off":
|
|
307
|
+
dot = c("amber", "⚠")
|
|
308
|
+
word = c("amber", f"{svc} · launchd off")
|
|
309
|
+
else:
|
|
310
|
+
dot = c("red", "○")
|
|
311
|
+
word = c("dim", f"{svc} · launchd not installed")
|
|
312
|
+
print(" " + dot + " " + word)
|
|
313
|
+
|
|
314
|
+
print()
|
|
315
|
+
|
|
316
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
317
|
+
# Live data collection
|
|
318
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
319
|
+
def _live_data() -> Dict[str, Any]:
|
|
320
|
+
slug = _project_slug()
|
|
321
|
+
entries = _parse_ai_entries()
|
|
322
|
+
ai_clients = []
|
|
323
|
+
for e in entries:
|
|
324
|
+
ai_clients.append({
|
|
325
|
+
"name": e["name"],
|
|
326
|
+
"cfg_file": e["cfg_file"],
|
|
327
|
+
"path": str(Path(e["ai_dir"]) / e["cfg_file"]).replace(str(Path.home()), "~"),
|
|
328
|
+
"sync": _ai_sync_status(e),
|
|
329
|
+
"skills": _ai_skill_count(e),
|
|
330
|
+
})
|
|
331
|
+
templates = [(t, _template_count(t)) for t in TEMPLATES]
|
|
332
|
+
feat_dir = Path("docs/features")
|
|
333
|
+
return dict(
|
|
334
|
+
conventions = _global_conventions(),
|
|
335
|
+
ai_clients = ai_clients,
|
|
336
|
+
templates = templates,
|
|
337
|
+
skills_installed = _skills_installed(),
|
|
338
|
+
project_has_agents = Path("AGENTS.md").exists(),
|
|
339
|
+
project_has_backlog = Path("BACKLOG.md").exists(),
|
|
340
|
+
project_features_count = sum(1 for _ in feat_dir.glob("*.md")) if feat_dir.exists() else 0,
|
|
341
|
+
loop_state = _launchd_state("loop", slug),
|
|
342
|
+
dream_state = _launchd_state("dream", slug),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
346
|
+
# Entry
|
|
347
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
348
|
+
def main() -> None:
|
|
349
|
+
ap = argparse.ArgumentParser(add_help=False)
|
|
350
|
+
ap.add_argument("--demo", action="store_true")
|
|
351
|
+
ap.add_argument("--no-color", dest="no_color", action="store_true")
|
|
352
|
+
ap.add_argument("--en", action="store_true")
|
|
353
|
+
ap.add_argument("--zh", action="store_true")
|
|
354
|
+
args, _ = ap.parse_known_args()
|
|
355
|
+
|
|
356
|
+
if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
|
|
357
|
+
roll_render.USE_COLOR = False
|
|
358
|
+
|
|
359
|
+
d = _demo_data() if args.demo else _live_data()
|
|
360
|
+
|
|
361
|
+
_render_health(d)
|
|
362
|
+
_render_global_conventions(d["conventions"])
|
|
363
|
+
_render_ai_clients(d["ai_clients"])
|
|
364
|
+
_render_templates(d["templates"])
|
|
365
|
+
_render_this_project(d)
|
|
366
|
+
|
|
367
|
+
if __name__ == "__main__":
|
|
368
|
+
main()
|
package/package.json
CHANGED
package/skills/roll-fix/SKILL.md
CHANGED
|
@@ -367,6 +367,31 @@ A minor change is only "done" when all are true:
|
|
|
367
367
|
- [ ] Online verification performed
|
|
368
368
|
- [ ] **Verification Gate passed** (fresh evidence for tests, build, fix confirmation, no regression)
|
|
369
369
|
|
|
370
|
+
## Rubric
|
|
371
|
+
|
|
372
|
+
Quality evaluation for a completed fix. Score each dimension independently.
|
|
373
|
+
|
|
374
|
+
| 维度 | ❌ Miss (0) | ⚠️ Partial (1) | ✅ Hit (2) |
|
|
375
|
+
|------|------------|----------------|-----------|
|
|
376
|
+
| **根因定位** | 只修了表象,未说明根因 | 描述了直接原因 | 追溯根本原因并有代码/日志证据 |
|
|
377
|
+
| **最小范围** | 改动超出 fix 边界,含机会主义修改 | 范围合理但有冗余改动 | 最小改动,非 fix 相关代码零触碰 |
|
|
378
|
+
| **回归测试** | 无测试,或测试与 bug 无关 | 有测试但未复现原始 bug | 先写复现测试(RED)再修复(GREEN) |
|
|
379
|
+
| **验证证据** | 仅声称通过,无实际输出 | 有部分截图/日志但不完整 | 贴出完整命令输出,覆盖 fix + 回归 |
|
|
380
|
+
| **无新破坏** | CI 红,或已知回归未处理 | CI 绿但有 warning 未说明 | CI 全绿,覆盖率不降,warning 清零 |
|
|
381
|
+
|
|
382
|
+
**评分解读**
|
|
383
|
+
|
|
384
|
+
| 总分 | 结论 |
|
|
385
|
+
|------|------|
|
|
386
|
+
| 9–10 | Exemplary — 可作为参考案例 |
|
|
387
|
+
| 7–8 | Acceptable — 可交付,有小瑕疵 |
|
|
388
|
+
| 5–6 | Needs Work — 需补充证据或补测试 |
|
|
389
|
+
| ≤ 4 | Redo — 根因或验证存在根本缺失 |
|
|
390
|
+
|
|
391
|
+
> 用法:fix 完成后由 `$roll-eval`(或人工)对照此表打分,结果写入 `roll-notes`。
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
370
395
|
## TCR Patterns for Common Fixes
|
|
371
396
|
|
|
372
397
|
### Pattern: Bug Fix with Regression Test
|