@seanyao/roll 2026.517.2 → 2026.517.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/lib/loop-fmt.py CHANGED
@@ -1,180 +1,329 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- loop-fmt.py — stream-json → human-readable formatter for roll loop tmux output.
3
+ loop-fmt.py — 3-tier stream-json → tmux formatter for roll loop.
4
4
 
5
- Reads stream-json lines from stdin, emits colored, human-readable events.
6
- Skips noise (system/init, hook_started, rate_limit_event) and abbreviates
7
- tool results so the window stays readable.
8
-
9
- Color codes: no external deps, plain ANSI.
5
+ Tier 3 (suppressed): init, thinking, Read/Glob/Grep, non-error results, plain Bash
6
+ Tier 2 (muted): Edit/Write path
7
+ Tier 1 (signal): tcr commit, story skill, peer verdict, ci gate, pr merge, errors
10
8
  """
11
-
12
9
  import sys
13
10
  import json
14
11
  import re
15
- import textwrap
16
-
17
- # ANSI colors
18
- CYAN = "\033[36m"
19
- GREEN = "\033[32m"
20
- YELLOW = "\033[33m"
21
- RED = "\033[31m"
22
- GRAY = "\033[90m"
23
- BOLD = "\033[1m"
24
- RESET = "\033[0m"
25
- DIM = "\033[2m"
26
-
27
- SKIP_SUBTYPES = {"hook_started", "hook_response", "hook_stop_hook_execution",
28
- "hook_stop_hook_active_hooks_ran"}
29
-
30
- def trunc(s, n=120):
12
+ import os
13
+ import threading
14
+ import time
15
+ from datetime import datetime, timezone
16
+
17
+ _SPIN_ENABLED = os.environ.get("LOOP_FMT_NO_SPIN", "0") != "1"
18
+ SPIN_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
19
+
20
+ DARK_GRAY = "\033[90m"
21
+ CYAN = "\033[36m"
22
+ WHITE = "\033[97m"
23
+ GREEN = "\033[32m"
24
+ RED = "\033[31m"
25
+ YELLOW = "\033[33m"
26
+ RESET = "\033[0m"
27
+
28
+
29
+ class Spinner:
30
+ """Animated wait indicator for long-running operations.
31
+
32
+ In production (LOOP_FMT_NO_SPIN=0): background thread writes frames using \\r.
33
+ In test mode (LOOP_FMT_NO_SPIN=1): writes a static ⏳ line to stdout instead.
34
+ """
35
+ def __init__(self):
36
+ self._thread = None
37
+ self._running = False
38
+ self._label = ""
39
+ self._lock = threading.Lock()
40
+
41
+ @property
42
+ def active(self):
43
+ return self._running
44
+
45
+ def start(self, label):
46
+ with self._lock:
47
+ if self._running:
48
+ self._label = label # update without restart
49
+ return
50
+ self._label = label
51
+ self._running = True
52
+ if _SPIN_ENABLED:
53
+ self._thread = threading.Thread(target=self._run, daemon=True)
54
+ self._thread.start()
55
+ else:
56
+ sys.stdout.write(f" {YELLOW}⏳ {label}...{RESET}\n")
57
+ sys.stdout.flush()
58
+
59
+ def stop(self):
60
+ with self._lock:
61
+ was_running = self._running
62
+ self._running = False
63
+ if self._thread:
64
+ self._thread.join(timeout=0.3)
65
+ self._thread = None
66
+ if _SPIN_ENABLED and was_running:
67
+ sys.stdout.write(f"\r{' ' * 60}\r")
68
+ sys.stdout.flush()
69
+
70
+ def _run(self):
71
+ i = 0
72
+ while self._running:
73
+ with self._lock:
74
+ label = self._label
75
+ frame = SPIN_FRAMES[i % len(SPIN_FRAMES)]
76
+ sys.stdout.write(f"\r {YELLOW}{frame} {label}...{RESET}")
77
+ sys.stdout.flush()
78
+ time.sleep(0.12)
79
+ i += 1
80
+ sys.stdout.write(f"\r{' ' * 60}\r")
81
+ sys.stdout.flush()
82
+
83
+ SUPPRESS_TOOLS = {"Read", "Glob", "Grep", "ReadMcpResourceTool", "ListMcpResourcesTool",
84
+ "WebFetch", "WebSearch", "TaskCreate", "TaskGet", "TaskList",
85
+ "TaskUpdate", "TaskOutput", "TaskStop"}
86
+
87
+ def now_hms():
88
+ return datetime.now(timezone.utc).strftime("%H:%M:%S")
89
+
90
+ def trunc(s, n=60):
31
91
  s = str(s).replace("\n", " ").strip()
32
92
  return s[:n] + "…" if len(s) > n else s
33
93
 
34
- def fmt_tool_input(name, inp):
35
- if not isinstance(inp, dict):
36
- return trunc(str(inp), 80)
37
- if name == "Bash":
38
- cmd = inp.get("command", "")
39
- # show first non-empty line
40
- lines = [l.strip() for l in cmd.splitlines() if l.strip()]
41
- return trunc(lines[0] if lines else cmd, 100)
42
- if name in ("Read", "Write", "Edit"):
43
- path = inp.get("file_path", inp.get("path", ""))
44
- extra = ""
45
- if name == "Edit":
46
- old = inp.get("old_string", "")
47
- extra = f" ({trunc(old, 40)})"
48
- return f"{path}{extra}"
49
- if name in ("Glob", "Grep"):
50
- return trunc(inp.get("pattern", inp.get("query", str(inp))), 80)
51
- if name == "Skill":
52
- return inp.get("skill", "") + (" " + inp.get("args", "") if inp.get("args") else "")
53
- if name == "Agent":
54
- return trunc(inp.get("description", str(inp)), 80)
55
- return trunc(json.dumps(inp, ensure_ascii=False), 80)
56
-
57
- def fmt_tool_result(content):
58
- if isinstance(content, list):
59
- parts = []
60
- for c in content:
61
- if isinstance(c, dict):
62
- t = c.get("type", "")
63
- if t == "text":
64
- parts.append(c.get("text", ""))
65
- elif t == "image":
66
- parts.append("[image]")
67
- else:
68
- parts.append(str(c))
69
- text = " ".join(parts)
70
- else:
71
- text = str(content) if content is not None else ""
72
- # strip ansi for length check
73
- clean = re.sub(r'\033\[[0-9;]*m', '', text)
74
- lines = [l for l in clean.splitlines() if l.strip()]
75
- if not lines:
76
- return "(empty)"
77
- # show first 3 lines, trim long lines
78
- out = []
79
- for l in lines[:3]:
80
- out.append(" " + trunc(l, 100))
81
- if len(lines) > 3:
82
- out.append(f" {DIM}… ({len(lines)-3} more lines){RESET}")
83
- return "\n".join(out)
84
-
85
- def process_line(line):
86
- line = line.rstrip()
87
- if not line:
88
- return
89
- try:
90
- ev = json.loads(line)
91
- except json.JSONDecodeError:
92
- # plain text passthrough
93
- print(line)
94
- return
95
-
96
- etype = ev.get("type", "")
97
-
98
- # ── system events ──────────────────────────────────────────────
99
- if etype == "system":
100
- subtype = ev.get("subtype", "")
101
- if subtype in SKIP_SUBTYPES:
94
+ def step(category, label, detail="", ok=True):
95
+ cat_color = CYAN
96
+ label_color = GREEN if ok and category in ("ci", "pr") else (RED if not ok else WHITE)
97
+ arrow = f"{DARK_GRAY}→{RESET}"
98
+ cat = f" {cat_color}{category:<6}{RESET}"
99
+ lbl = f" {label_color}{label:<14}{RESET}"
100
+ det = f" {DARK_GRAY}{detail}{RESET}" if detail else ""
101
+ return f"{arrow}{cat}{lbl}{det}"
102
+
103
+ def stamp(text, muted=False):
104
+ ts = f"{DARK_GRAY}{now_hms()}{RESET}"
105
+ body = f"{DARK_GRAY}{text}{RESET}" if muted else text
106
+ return f"{ts} {body}"
107
+
108
+ class LoopFmt:
109
+ def __init__(self):
110
+ self.last_bash_cmd = ""
111
+ self.tcr_count = 0
112
+ self.last_test_count = None
113
+ self.cycle_num = None
114
+ self.pending_commit = False
115
+ self.pending_pr = False
116
+ self.pending_ci = False
117
+ self.pending_story = False
118
+ self.spinner = Spinner()
119
+
120
+ def _extract_cycle_num(self, text):
121
+ m = re.search(r'cycle[#\s]+(\d+)', text, re.IGNORECASE)
122
+ return m.group(1) if m else "?"
123
+
124
+ def process(self, line):
125
+ line = line.rstrip()
126
+ if not line:
102
127
  return
103
- if subtype == "init":
104
- model = ev.get("model", "")
105
- tools = ev.get("tools", [])
106
- tool_list = ", ".join(tools[:6])
107
- if len(tools) > 6:
108
- tool_list += f" +{len(tools)-6}"
109
- print(f"{DIM}[init] model={model} tools={tool_list}{RESET}")
128
+
129
+ # Plain text passthrough
130
+ try:
131
+ ev = json.loads(line)
132
+ except json.JSONDecodeError:
133
+ self._handle_plain(line)
110
134
  return
111
- # unknown system — show raw briefly
112
- print(f"{DIM}[sys/{subtype}]{RESET}")
113
- return
114
135
 
115
- # ── rate limit ────────────────────────────────────────────────
116
- if etype == "rate_limit_event":
117
- return
136
+ etype = ev.get("type", "")
137
+ if etype == "system":
138
+ return # Tier 3: suppress all system events
139
+ if etype == "assistant":
140
+ self._handle_assistant(ev)
141
+ elif etype == "user":
142
+ self._handle_user(ev)
143
+ elif etype == "result":
144
+ self._handle_result(ev)
145
+ # All other types: suppress
146
+
147
+ def _handle_plain(self, line):
148
+ # [loop] cycle N: ... → Tier 1 stamp
149
+ m = re.search(r'\[loop\]\s+cycle\s+(\d+)[:\s]', line)
150
+ if m:
151
+ self.cycle_num = m.group(1)
152
+ self.tcr_count = 0
153
+ print(stamp(f"cycle #{self.cycle_num} — picking story"))
154
+ return
155
+ # Other plain text: suppress
118
156
 
119
- # ── assistant ────────────────────────────────────────────────
120
- if etype == "assistant":
157
+ def _handle_assistant(self, ev):
121
158
  msg = ev.get("message", {})
122
159
  for blk in msg.get("content", []):
123
160
  btype = blk.get("type", "")
124
- if btype == "tool_use":
125
- name = blk.get("name", "?")
126
- inp = blk.get("input", {})
127
- summary = fmt_tool_input(name, inp)
128
- print(f"{CYAN}→ {BOLD}{name}{RESET}{CYAN}: {summary}{RESET}")
161
+ if btype == "thinking":
162
+ return # Tier 3
129
163
  elif btype == "text":
130
- text = blk.get("text", "").strip()
131
- if text:
132
- # wrap long text
133
- for l in textwrap.wrap(text, 120):
134
- print(f"{GREEN}{l}{RESET}")
135
- elif btype == "thinking":
136
- thought = blk.get("thinking", "").strip()
137
- if thought:
138
- first = trunc(thought, 80)
139
- print(f"{DIM}[thinking] {first}{RESET}")
140
- return
141
-
142
- # ── user (tool results) ───────────────────────────────────────
143
- if etype == "user":
164
+ self._handle_text(blk.get("text", ""))
165
+ elif btype == "tool_use":
166
+ self._handle_tool_use(blk)
167
+
168
+ def _handle_text(self, text):
169
+ text = text.strip()
170
+ if not text:
171
+ return
172
+ # Peer verdict detection
173
+ for verdict in ("AGREE", "REFINE", "OBJECT", "ESCALATE"):
174
+ if verdict in text:
175
+ m = re.search(r'round\s+(\d+)[/\\](\d+)', text, re.IGNORECASE)
176
+ round_str = f"round {m.group(1)}/{m.group(2)}" if m else "round ?"
177
+ # agent names — look for common patterns
178
+ agents = "claude → peer"
179
+ m2 = re.search(r'(\w+)\s*→\s*(\w+)', text)
180
+ if m2:
181
+ agents = f"{m2.group(1)} → {m2.group(2)}"
182
+ print(step("peer", agents, f"{round_str} · {verdict}"))
183
+ return
184
+ # All other text: Tier 3, suppress
185
+
186
+ def _handle_tool_use(self, blk):
187
+ name = blk.get("name", "")
188
+ inp = blk.get("input", {})
189
+
190
+ if name in SUPPRESS_TOOLS:
191
+ return # Tier 3
192
+
193
+ if name in ("Edit", "Write"):
194
+ path = inp.get("file_path") or inp.get("path", "")
195
+ print(f" {DARK_GRAY}✏ {path}{RESET}")
196
+ return # Tier 2
197
+
198
+ if name == "Bash":
199
+ cmd = inp.get("command", "")
200
+ first_line = next((l.strip() for l in cmd.splitlines() if l.strip()), cmd)
201
+ self.last_bash_cmd = first_line
202
+ if re.search(r'git commit.*tcr:', cmd):
203
+ self.pending_commit = True
204
+ elif re.search(r'gh pr (create|merge)', cmd):
205
+ self.pending_pr = True
206
+ self.spinner.start("merging PR")
207
+ elif re.search(r'(roll ci|npm run ci|_ci_wait|ci:local)', cmd):
208
+ self.pending_ci = True
209
+ self.spinner.start("waiting for CI")
210
+ return # Wait for result
211
+
212
+ if name == "Skill":
213
+ skill = inp.get("skill", "")
214
+ args = inp.get("args", "").strip()
215
+ if skill in ("roll-build", "roll-fix"):
216
+ us_id = args.split()[0] if args else "?"
217
+ print()
218
+ print(stamp(f"cycle #{self.cycle_num or '?'} — picking story"))
219
+ print(step("story", us_id, trunc(args, 60)))
220
+ self.pending_story = True
221
+ self.spinner.start("executing story")
222
+ return
223
+
224
+ # All other tools (Agent, ToolSearch, etc.): suppress
225
+
226
+ def _handle_user(self, ev):
144
227
  msg = ev.get("message", {})
145
228
  for blk in msg.get("content", []):
146
- if blk.get("type") == "tool_result":
147
- is_err = blk.get("is_error", False)
148
- content = blk.get("content", "")
149
- result_text = fmt_tool_result(content)
150
- prefix = f"{RED} ✗{RESET}" if is_err else f"{GRAY} ↩{RESET}"
151
- print(f"{prefix} {result_text}")
152
- return
153
-
154
- # ── result (final) ───────────────────────────────────────────
155
- if etype == "result":
229
+ if blk.get("type") != "tool_result":
230
+ continue
231
+ is_err = blk.get("is_error", False)
232
+ content = blk.get("content", "")
233
+ text = self._extract_text(content)
234
+
235
+ # Scan for test count (bats ok N pattern)
236
+ m = re.search(r'\bok\s+(\d+)', text)
237
+ if m:
238
+ self.last_test_count = int(m.group(1))
239
+
240
+ if is_err:
241
+ tool_name = "tool"
242
+ lines = [l for l in text.splitlines() if l.strip()][:3]
243
+ detail = " | ".join(lines)
244
+ print(step("error", tool_name, trunc(detail, 80), ok=False))
245
+ self.pending_commit = self.pending_pr = self.pending_ci = False
246
+ return
247
+
248
+ if self.pending_commit:
249
+ self.pending_commit = False
250
+ # Extract hash and message from git commit output: [branch hash] msg
251
+ m = re.search(r'\[[\w/\-]+ ([0-9a-f]{7,})\]\s*tcr:\s*(.+)', text)
252
+ if m:
253
+ commit_hash = m.group(1)[:7]
254
+ commit_msg = m.group(2).strip()
255
+ self.tcr_count += 1
256
+ test_part = f" · {self.last_test_count} tests" if self.last_test_count else ""
257
+ print(step("tcr", commit_hash, f"{commit_msg}{test_part}"))
258
+ return
259
+
260
+ if self.pending_story:
261
+ self.pending_story = False
262
+ self.spinner.stop()
263
+ return # story result content suppressed; TCR events showed the work
264
+
265
+ if self.pending_pr:
266
+ self.spinner.stop()
267
+ self.pending_pr = False
268
+ m = re.search(r'#(\d+)', text)
269
+ if m:
270
+ pr_num = f"#{m.group(1)}"
271
+ branch = re.search(r'loop/[\w\-]+', self.last_bash_cmd)
272
+ branch_str = branch.group(0) if branch else ""
273
+ detail = f"auto-merged · {branch_str}" if branch_str else "auto-merged"
274
+ print(step("pr", pr_num, detail, ok=True))
275
+ return
276
+
277
+ if self.pending_ci:
278
+ self.spinner.stop()
279
+ self.pending_ci = False
280
+ has_green = re.search(r'(green|pass|success|all tests)', text, re.IGNORECASE)
281
+ has_red = re.search(r'(red|fail|error)', text, re.IGNORECASE)
282
+ m_dur = re.search(r'(\d+(?:\.\d+)?)\s*s\b', text)
283
+ m_test = re.search(r'(\d+)\s+tests?', text)
284
+ dur_str = f"{m_dur.group(1)}s" if m_dur else ""
285
+ test_str = f"{m_test.group(1)} tests" if m_test else (f"{self.last_test_count} tests" if self.last_test_count else "")
286
+ detail = " · ".join(filter(None, [dur_str, test_str]))
287
+ if has_green and not has_red:
288
+ print(step("ci", "green", detail, ok=True))
289
+ else:
290
+ print(step("ci", "red", detail, ok=False))
291
+ return
292
+
293
+ # Non-matching result: suppress (Tier 3)
294
+
295
+ def _extract_text(self, content):
296
+ if isinstance(content, str):
297
+ return content
298
+ if isinstance(content, list):
299
+ parts = []
300
+ for c in content:
301
+ if isinstance(c, dict) and c.get("type") == "text":
302
+ parts.append(c.get("text", ""))
303
+ return "\n".join(parts)
304
+ return str(content) if content else ""
305
+
306
+ def _handle_result(self, ev):
156
307
  dur_ms = ev.get("duration_ms", 0)
157
308
  cost_usd = ev.get("total_cost_usd", 0)
158
- turns = ev.get("num_turns", "?")
159
309
  dur_s = dur_ms / 1000
160
- cost_str = f"${cost_usd:.4f}" if cost_usd else ""
310
+ cost_str = f"${cost_usd:.2f}" if cost_usd else ""
311
+ tcr_str = f"{self.tcr_count} tcr" if self.tcr_count else ""
312
+ parts = [p for p in [tcr_str, f"{dur_s:.0f}s", cost_str] if p]
313
+ detail = " · ".join(parts)
161
314
  subtype = ev.get("subtype", "")
162
315
  if subtype == "error_max_turns":
163
- print(f"{RED}✗ max turns reached {dur_s:.1f}s{RESET}")
316
+ print(step("error", "max-turns", f"{dur_s:.0f}s", ok=False))
164
317
  else:
165
- cost_part = f" {YELLOW}{cost_str}{RESET}" if cost_str else ""
166
- print(f"\n{GREEN}{BOLD}✓ done{RESET} {dur_s:.1f}s {GRAY}{turns} turns{RESET}{cost_part}")
167
- return
168
-
169
- # ── fallback ────────────────────────────────────────────────
170
- print(f"{DIM}{trunc(line, 160)}{RESET}")
318
+ cycle_str = f"cycle #{self.cycle_num}" if self.cycle_num else "cycle done"
319
+ print(stamp(f"{cycle_str} done · {detail}" if detail else f"{cycle_str} — done", muted=True))
171
320
 
172
321
 
173
322
  def main():
323
+ fmt = LoopFmt()
174
324
  for line in sys.stdin:
175
- process_line(line)
325
+ fmt.process(line)
176
326
  sys.stdout.flush()
177
327
 
178
-
179
328
  if __name__ == "__main__":
180
329
  main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "2026.517.2",
3
+ "version": "2026.517.4",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "scripts": {
6
6
  "test": "bash tests/run.sh"
@@ -416,6 +416,10 @@ prompt 会包含:
416
416
  (即使没有 deep doc 也要列)
417
417
  - Feature 名跟 `docs/features/<file>.md` 文件名一致时,加链接到该 md
418
418
  - 没有对应 deep doc 的 Feature,**只写 plain text 不加链接**
419
+ - **Planning distinction(US-DOC-011)**:
420
+ - 该 Feature 下**所有** Story 均为 `📋 Todo` → 在描述末尾追加 `*(规划中)*`
421
+ - 只要有 **≥1 个** `✅ Done` Story → 正常展示,**不加**任何标记
422
+ - 一眼可见:规划中的 Feature 在每个 Epic 分组的末尾列出
419
423
  - 描述写 1 句话 **产品视角**:用户能用它做什么,避免实现细节
420
424
  - 分组用 BACKLOG 的 Epic 名,原序,不重排
421
425
  - Core Highlights 从所有 Features 里挑 3-5 个最能代表产品定位的,
@@ -127,6 +127,18 @@ find docs/ -maxdepth 1 -name '*.md' 2>/dev/null
127
127
 
128
128
  Flag any `.md` file directly in `docs/` root (allowed subdirs: `guide/`, `domain/`, `features/`, `practices/`, `briefs/`, `dream/`).
129
129
 
130
+ **Check D — features.md Feature Coverage (US-DOC-009):**
131
+
132
+ Dependency gate: skip when `docs/features.md` does not exist.
133
+
134
+ Parse BACKLOG.md for all `### Feature: <name>` groups that contain ≥1 ✅ Done story. Parse `docs/features.md` for Feature names. If any Feature group with Done stories is absent from `docs/features.md`, the catalog is stale — flag as REFACTOR:
135
+
136
+ ```markdown
137
+ | REFACTOR-XXX | features.md 功能目录落后于 BACKLOG,N 个已完成功能区未收录,用户无法通过产品目录发现这些功能 — flagged by dream YYYY-MM-DD | 📋 Todo |
138
+ ```
139
+
140
+ The catalog is auto-updated by `scripts/release.sh` at release time (Section 8 of roll-.changelog). Between releases, this check surfaces the coverage gap so it isn't silently skipped.
141
+
130
142
  **REFACTOR entry format for doc findings:**
131
143
 
132
144
  ```markdown
@@ -137,7 +149,8 @@ Flag any `.md` file directly in `docs/` root (allowed subdirs: `guide/`, `domain
137
149
 
138
150
  ```markdown
139
151
  ## 文档覆盖度
140
- {发现内容 或 "文档结构符合规范,无缺口。"}
152
+ - features.md 功能区覆盖:{N}/{M} 个已完成功能区已收录(缺失:{列表 或 ""}
153
+ {其他发现内容 或 "文档结构符合规范,无缺口。"}
141
154
  ```
142
155
 
143
156
  ### Scan 6 — 文档新鲜度 (Doc Freshness)
@@ -45,7 +45,9 @@ $roll-.review unstaged
45
45
  $roll-.review files src/utils.ts
46
46
  ```
47
47
 
48
- ## Review Dimensions (6 Core Dimensions)
48
+ ## Review Dimensions (7 Core Dimensions)
49
+
50
+ Original 6 dimensions plus Reuse (added in REFACTOR-022, simplify three-axis integration):
49
51
 
50
52
  ```
51
53
  ┌─────────────────────────────────────────────────────────┐
@@ -54,13 +56,42 @@ $roll-.review files src/utils.ts
54
56
  │ ✅ Correctness - Logic is correct, no bugs │
55
57
  │ ✅ Security - No vulnerabilities, input valid. │
56
58
  │ ✅ Maintainability - Clear naming, sound structure │
59
+ │ Quality anti-patterns (check each): │
60
+ │ □ Redundant state / cached values that could be │
61
+ │ derived directly │
62
+ │ □ Parameter sprawl — new param vs. restructure │
63
+ │ □ Copy-paste with slight variation (near-dup) │
64
+ │ □ Leaky abstraction — exposes internal details │
65
+ │ □ Stringly-typed — raw string where constant │
66
+ │ / enum exists │
67
+ │ □ Unnecessary JSX nesting (no layout value) │
68
+ │ □ Nested conditionals ≥3 deep (ternary chains, │
69
+ │ nested if/else) — flatten with early return │
70
+ │ □ Unnecessary comments explaining WHAT │
57
71
  │ ✅ Performance - No performance pitfalls │
72
+ │ Efficiency anti-patterns (check each): │
73
+ │ □ Redundant computation / repeated file read / │
74
+ │ duplicate API call / N+1 pattern │
75
+ │ □ Missed concurrency — independent ops sequential │
76
+ │ □ Hot-path bloat — blocking work in startup or │
77
+ │ per-request path │
78
+ │ □ Loop no-op updates — missing change-detection │
79
+ │ guard │
80
+ │ □ TOCTOU existence pre-check — operate directly + │
81
+ │ handle error instead │
82
+ │ □ Memory — unbounded structures / missing cleanup │
83
+ │ □ Overly broad op — reading full file for a slice │
58
84
  │ ✅ Testability - Easy to test, edge cases covered │
59
85
  │ ✅ Scope - Focused on current task, no │
60
86
  │ unrelated changes │
87
+ │ ✅ Reuse - No new code duplicating existing │
88
+ │ □ New function duplicates existing utility/helper │
89
+ │ □ Inline logic replaceable by existing tool │
61
90
  └─────────────────────────────────────────────────────────┘
62
91
  ```
63
92
 
93
+ **Usage in TCR**: Each micro-step review is a lightweight self-check against this checklist — no sub-agents, zero extra token cost. The three-axis deep review with parallel agents runs once per Story in `$roll-build` Phase 7.
94
+
64
95
  ## Severity Levels and Decisions
65
96
 
66
97
  | Level | Definition | Decision |