emdash-core 0.1.7__py3-none-any.whl

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.
Files changed (187) hide show
  1. emdash_core/__init__.py +3 -0
  2. emdash_core/agent/__init__.py +37 -0
  3. emdash_core/agent/agents.py +225 -0
  4. emdash_core/agent/code_reviewer.py +476 -0
  5. emdash_core/agent/compaction.py +143 -0
  6. emdash_core/agent/context_manager.py +140 -0
  7. emdash_core/agent/events.py +338 -0
  8. emdash_core/agent/handlers.py +224 -0
  9. emdash_core/agent/inprocess_subagent.py +377 -0
  10. emdash_core/agent/mcp/__init__.py +50 -0
  11. emdash_core/agent/mcp/client.py +346 -0
  12. emdash_core/agent/mcp/config.py +302 -0
  13. emdash_core/agent/mcp/manager.py +496 -0
  14. emdash_core/agent/mcp/tool_factory.py +213 -0
  15. emdash_core/agent/prompts/__init__.py +38 -0
  16. emdash_core/agent/prompts/main_agent.py +104 -0
  17. emdash_core/agent/prompts/subagents.py +131 -0
  18. emdash_core/agent/prompts/workflow.py +136 -0
  19. emdash_core/agent/providers/__init__.py +34 -0
  20. emdash_core/agent/providers/base.py +143 -0
  21. emdash_core/agent/providers/factory.py +80 -0
  22. emdash_core/agent/providers/models.py +220 -0
  23. emdash_core/agent/providers/openai_provider.py +463 -0
  24. emdash_core/agent/providers/transformers_provider.py +217 -0
  25. emdash_core/agent/research/__init__.py +81 -0
  26. emdash_core/agent/research/agent.py +143 -0
  27. emdash_core/agent/research/controller.py +254 -0
  28. emdash_core/agent/research/critic.py +428 -0
  29. emdash_core/agent/research/macros.py +469 -0
  30. emdash_core/agent/research/planner.py +449 -0
  31. emdash_core/agent/research/researcher.py +436 -0
  32. emdash_core/agent/research/state.py +523 -0
  33. emdash_core/agent/research/synthesizer.py +594 -0
  34. emdash_core/agent/reviewer_profile.py +475 -0
  35. emdash_core/agent/rules.py +123 -0
  36. emdash_core/agent/runner.py +601 -0
  37. emdash_core/agent/session.py +262 -0
  38. emdash_core/agent/spec_schema.py +66 -0
  39. emdash_core/agent/specification.py +479 -0
  40. emdash_core/agent/subagent.py +397 -0
  41. emdash_core/agent/subagent_prompts.py +13 -0
  42. emdash_core/agent/toolkit.py +482 -0
  43. emdash_core/agent/toolkits/__init__.py +64 -0
  44. emdash_core/agent/toolkits/base.py +96 -0
  45. emdash_core/agent/toolkits/explore.py +47 -0
  46. emdash_core/agent/toolkits/plan.py +55 -0
  47. emdash_core/agent/tools/__init__.py +141 -0
  48. emdash_core/agent/tools/analytics.py +436 -0
  49. emdash_core/agent/tools/base.py +131 -0
  50. emdash_core/agent/tools/coding.py +484 -0
  51. emdash_core/agent/tools/github_mcp.py +592 -0
  52. emdash_core/agent/tools/history.py +13 -0
  53. emdash_core/agent/tools/modes.py +153 -0
  54. emdash_core/agent/tools/plan.py +206 -0
  55. emdash_core/agent/tools/plan_write.py +135 -0
  56. emdash_core/agent/tools/search.py +412 -0
  57. emdash_core/agent/tools/spec.py +341 -0
  58. emdash_core/agent/tools/task.py +262 -0
  59. emdash_core/agent/tools/task_output.py +204 -0
  60. emdash_core/agent/tools/tasks.py +454 -0
  61. emdash_core/agent/tools/traversal.py +588 -0
  62. emdash_core/agent/tools/web.py +179 -0
  63. emdash_core/analytics/__init__.py +5 -0
  64. emdash_core/analytics/engine.py +1286 -0
  65. emdash_core/api/__init__.py +5 -0
  66. emdash_core/api/agent.py +308 -0
  67. emdash_core/api/agents.py +154 -0
  68. emdash_core/api/analyze.py +264 -0
  69. emdash_core/api/auth.py +173 -0
  70. emdash_core/api/context.py +77 -0
  71. emdash_core/api/db.py +121 -0
  72. emdash_core/api/embed.py +131 -0
  73. emdash_core/api/feature.py +143 -0
  74. emdash_core/api/health.py +93 -0
  75. emdash_core/api/index.py +162 -0
  76. emdash_core/api/plan.py +110 -0
  77. emdash_core/api/projectmd.py +210 -0
  78. emdash_core/api/query.py +320 -0
  79. emdash_core/api/research.py +122 -0
  80. emdash_core/api/review.py +161 -0
  81. emdash_core/api/router.py +76 -0
  82. emdash_core/api/rules.py +116 -0
  83. emdash_core/api/search.py +119 -0
  84. emdash_core/api/spec.py +99 -0
  85. emdash_core/api/swarm.py +223 -0
  86. emdash_core/api/tasks.py +109 -0
  87. emdash_core/api/team.py +120 -0
  88. emdash_core/auth/__init__.py +17 -0
  89. emdash_core/auth/github.py +389 -0
  90. emdash_core/config.py +74 -0
  91. emdash_core/context/__init__.py +52 -0
  92. emdash_core/context/models.py +50 -0
  93. emdash_core/context/providers/__init__.py +11 -0
  94. emdash_core/context/providers/base.py +74 -0
  95. emdash_core/context/providers/explored_areas.py +183 -0
  96. emdash_core/context/providers/touched_areas.py +360 -0
  97. emdash_core/context/registry.py +73 -0
  98. emdash_core/context/reranker.py +199 -0
  99. emdash_core/context/service.py +260 -0
  100. emdash_core/context/session.py +352 -0
  101. emdash_core/core/__init__.py +104 -0
  102. emdash_core/core/config.py +454 -0
  103. emdash_core/core/exceptions.py +55 -0
  104. emdash_core/core/models.py +265 -0
  105. emdash_core/core/review_config.py +57 -0
  106. emdash_core/db/__init__.py +67 -0
  107. emdash_core/db/auth.py +134 -0
  108. emdash_core/db/models.py +91 -0
  109. emdash_core/db/provider.py +222 -0
  110. emdash_core/db/providers/__init__.py +5 -0
  111. emdash_core/db/providers/supabase.py +452 -0
  112. emdash_core/embeddings/__init__.py +24 -0
  113. emdash_core/embeddings/indexer.py +534 -0
  114. emdash_core/embeddings/models.py +192 -0
  115. emdash_core/embeddings/providers/__init__.py +7 -0
  116. emdash_core/embeddings/providers/base.py +112 -0
  117. emdash_core/embeddings/providers/fireworks.py +141 -0
  118. emdash_core/embeddings/providers/openai.py +104 -0
  119. emdash_core/embeddings/registry.py +146 -0
  120. emdash_core/embeddings/service.py +215 -0
  121. emdash_core/graph/__init__.py +26 -0
  122. emdash_core/graph/builder.py +134 -0
  123. emdash_core/graph/connection.py +692 -0
  124. emdash_core/graph/schema.py +416 -0
  125. emdash_core/graph/writer.py +667 -0
  126. emdash_core/ingestion/__init__.py +7 -0
  127. emdash_core/ingestion/change_detector.py +150 -0
  128. emdash_core/ingestion/git/__init__.py +5 -0
  129. emdash_core/ingestion/git/commit_analyzer.py +196 -0
  130. emdash_core/ingestion/github/__init__.py +6 -0
  131. emdash_core/ingestion/github/pr_fetcher.py +296 -0
  132. emdash_core/ingestion/github/task_extractor.py +100 -0
  133. emdash_core/ingestion/orchestrator.py +540 -0
  134. emdash_core/ingestion/parsers/__init__.py +10 -0
  135. emdash_core/ingestion/parsers/base_parser.py +66 -0
  136. emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
  137. emdash_core/ingestion/parsers/class_extractor.py +154 -0
  138. emdash_core/ingestion/parsers/function_extractor.py +202 -0
  139. emdash_core/ingestion/parsers/import_analyzer.py +119 -0
  140. emdash_core/ingestion/parsers/python_parser.py +123 -0
  141. emdash_core/ingestion/parsers/registry.py +72 -0
  142. emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
  143. emdash_core/ingestion/parsers/typescript_parser.py +278 -0
  144. emdash_core/ingestion/repository.py +346 -0
  145. emdash_core/models/__init__.py +38 -0
  146. emdash_core/models/agent.py +68 -0
  147. emdash_core/models/index.py +77 -0
  148. emdash_core/models/query.py +113 -0
  149. emdash_core/planning/__init__.py +7 -0
  150. emdash_core/planning/agent_api.py +413 -0
  151. emdash_core/planning/context_builder.py +265 -0
  152. emdash_core/planning/feature_context.py +232 -0
  153. emdash_core/planning/feature_expander.py +646 -0
  154. emdash_core/planning/llm_explainer.py +198 -0
  155. emdash_core/planning/similarity.py +509 -0
  156. emdash_core/planning/team_focus.py +821 -0
  157. emdash_core/server.py +153 -0
  158. emdash_core/sse/__init__.py +5 -0
  159. emdash_core/sse/stream.py +196 -0
  160. emdash_core/swarm/__init__.py +17 -0
  161. emdash_core/swarm/merge_agent.py +383 -0
  162. emdash_core/swarm/session_manager.py +274 -0
  163. emdash_core/swarm/swarm_runner.py +226 -0
  164. emdash_core/swarm/task_definition.py +137 -0
  165. emdash_core/swarm/worker_spawner.py +319 -0
  166. emdash_core/swarm/worktree_manager.py +278 -0
  167. emdash_core/templates/__init__.py +10 -0
  168. emdash_core/templates/defaults/agent-builder.md.template +82 -0
  169. emdash_core/templates/defaults/focus.md.template +115 -0
  170. emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
  171. emdash_core/templates/defaults/pr-review.md.template +80 -0
  172. emdash_core/templates/defaults/project.md.template +85 -0
  173. emdash_core/templates/defaults/research_critic.md.template +112 -0
  174. emdash_core/templates/defaults/research_planner.md.template +85 -0
  175. emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
  176. emdash_core/templates/defaults/reviewer.md.template +81 -0
  177. emdash_core/templates/defaults/spec.md.template +41 -0
  178. emdash_core/templates/defaults/tasks.md.template +78 -0
  179. emdash_core/templates/loader.py +296 -0
  180. emdash_core/utils/__init__.py +45 -0
  181. emdash_core/utils/git.py +84 -0
  182. emdash_core/utils/image.py +502 -0
  183. emdash_core/utils/logger.py +51 -0
  184. emdash_core-0.1.7.dist-info/METADATA +35 -0
  185. emdash_core-0.1.7.dist-info/RECORD +187 -0
  186. emdash_core-0.1.7.dist-info/WHEEL +4 -0
  187. emdash_core-0.1.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,475 @@
1
+ """Reviewer profile agent for analyzing repository reviewers and generating profiles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections import Counter
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any, Optional
10
+
11
+ from rich.console import Console
12
+ from rich.progress import Progress, SpinnerColumn, TextColumn
13
+
14
+ from .toolkit import AgentToolkit
15
+ from .providers import get_provider
16
+ from .providers.factory import DEFAULT_MODEL
17
+ from ..graph.connection import get_connection
18
+ from ..utils.logger import log
19
+
20
+
21
+ @dataclass
22
+ class ReviewerData:
23
+ """Data about a reviewer's patterns."""
24
+
25
+ username: str
26
+ review_count: int
27
+ prs_reviewed: list[int] = field(default_factory=list)
28
+ review_comments: list[dict] = field(default_factory=list)
29
+ review_verdicts: list[str] = field(default_factory=list) # APPROVED, CHANGES_REQUESTED, etc.
30
+
31
+
32
+ @dataclass
33
+ class ContributorData:
34
+ """Data about a cross-team contributor."""
35
+
36
+ name: str
37
+ email: str
38
+ communities_touched: int
39
+ commit_count: int
40
+
41
+
42
+ SYNTHESIS_PROMPT = """You are analyzing code review patterns from a repository to create a reviewer profile template.
43
+
44
+ Based on the following data about top reviewers and cross-team contributors, create a comprehensive reviewer profile that captures:
45
+
46
+ 1. **Review Focus Areas**: What aspects of code do reviewers commonly focus on?
47
+ 2. **Feedback Patterns**: What types of issues do they commonly point out?
48
+ 3. **Code Quality Expectations**: What standards do they enforce?
49
+ 4. **Style Preferences**: What coding patterns do they prefer?
50
+ 5. **Tone & Communication**: How do they phrase their feedback?
51
+ 6. **Example Comments**: Representative examples of good review comments
52
+ 7. **Review Checklist**: Key items reviewers check before approving
53
+
54
+ IMPORTANT:
55
+ - Extract patterns from the actual review comments provided
56
+ - Be specific about the types of issues raised
57
+ - Capture the tone and phrasing style
58
+ - Generate a checklist based on what reviewers actually check
59
+ - The output should be markdown that can be used as a template for future reviews
60
+
61
+ OUTPUT FORMAT:
62
+ Return a complete markdown document that follows this structure:
63
+
64
+ # Reviewer Profile
65
+
66
+ ## Identity
67
+ - Primary reviewers analyzed: {list of usernames}
68
+ - Cross-team contributors analyzed: {list of names}
69
+ - PRs analyzed: {count}
70
+
71
+ ## Review Focus Areas
72
+ {bullet points of what reviewers focus on}
73
+
74
+ ## Feedback Patterns
75
+ ### What they commonly comment on:
76
+ {patterns}
77
+
78
+ ### Code quality expectations:
79
+ {expectations}
80
+
81
+ ### Style preferences:
82
+ {preferences}
83
+
84
+ ## Tone & Communication
85
+ {description of tone and communication style}
86
+
87
+ ## Example Comments
88
+ {3-5 representative example comments from the data}
89
+
90
+ ## Review Checklist
91
+ {checklist items based on what reviewers check}
92
+ """
93
+
94
+
95
+ class ReviewerProfileAgent:
96
+ """Agent that analyzes repository reviewers and generates a profile template."""
97
+
98
+ def __init__(
99
+ self,
100
+ model: str = DEFAULT_MODEL,
101
+ verbose: bool = True,
102
+ ):
103
+ self.provider = get_provider(model)
104
+ self.toolkit = AgentToolkit(enable_session=False)
105
+ self.model = model
106
+ self.verbose = verbose
107
+ self.console = Console()
108
+
109
+ # Graph connection for Neo4j queries
110
+ try:
111
+ self.graph = get_connection()
112
+ except Exception:
113
+ self.graph = None
114
+ log.warning("Neo4j connection not available - cross-team contributor analysis will be skipped")
115
+
116
+ def analyze(
117
+ self,
118
+ top_n_reviewers: int = 5,
119
+ top_n_contributors: int = 5,
120
+ max_prs: int = 100,
121
+ ) -> str:
122
+ """Analyze repository reviewers and generate a profile.
123
+
124
+ Args:
125
+ top_n_reviewers: Number of top reviewers to analyze
126
+ top_n_contributors: Number of cross-team contributors to include
127
+ max_prs: Maximum PRs to fetch for analysis
128
+
129
+ Returns:
130
+ Generated reviewer profile as markdown
131
+ """
132
+ with Progress(
133
+ SpinnerColumn(),
134
+ TextColumn("[progress.description]{task.description}"),
135
+ console=self.console,
136
+ disable=not self.verbose,
137
+ ) as progress:
138
+ # 1. Fetch all PRs
139
+ task = progress.add_task("Fetching PRs...", total=None)
140
+ prs = self._fetch_all_prs(max_prs=max_prs)
141
+ progress.update(task, description=f"Fetched {len(prs)} PRs")
142
+
143
+ # 2. Count reviews, get top reviewers
144
+ progress.update(task, description="Identifying top reviewers...")
145
+ top_reviewers = self._get_top_reviewers(prs, top_n_reviewers)
146
+ progress.update(task, description=f"Found {len(top_reviewers)} top reviewers")
147
+
148
+ # 3. Fetch review comments for each top reviewer
149
+ progress.update(task, description="Fetching review comments...")
150
+ reviewer_data = self._fetch_reviewer_details(top_reviewers, prs)
151
+
152
+ # 4. Query Neo4j for multi-community contributors
153
+ cross_team = []
154
+ if self.graph:
155
+ progress.update(task, description="Finding cross-team contributors...")
156
+ cross_team = self._get_cross_team_contributors(top_n_contributors)
157
+
158
+ # 5. Synthesize with LLM
159
+ progress.update(task, description="Synthesizing reviewer profile...")
160
+ profile = self._synthesize_profile(reviewer_data, cross_team, len(prs))
161
+
162
+ progress.update(task, description="Done!")
163
+
164
+ return profile
165
+
166
+ def _fetch_all_prs(self, max_prs: int = 100) -> list[dict]:
167
+ """Fetch PRs from the repository.
168
+
169
+ Args:
170
+ max_prs: Maximum number of PRs to fetch
171
+
172
+ Returns:
173
+ List of PR data dictionaries
174
+ """
175
+ all_prs = []
176
+
177
+ # Fetch closed/merged PRs (more likely to have reviews)
178
+ for state in ["closed", "open"]:
179
+ result = self.toolkit.execute(
180
+ "github_list_prs",
181
+ state=state,
182
+ per_page=min(100, max_prs - len(all_prs)),
183
+ )
184
+
185
+ if result.success:
186
+ prs = result.data.get("prs", [])
187
+ all_prs.extend(prs)
188
+
189
+ if len(all_prs) >= max_prs:
190
+ break
191
+
192
+ return all_prs[:max_prs]
193
+
194
+ def _get_top_reviewers(
195
+ self,
196
+ prs: list[dict],
197
+ top_n: int = 5,
198
+ ) -> list[tuple[str, int, list[int]]]:
199
+ """Identify top reviewers by counting reviews across PRs.
200
+
201
+ Args:
202
+ prs: List of PR data
203
+ top_n: Number of top reviewers to return
204
+
205
+ Returns:
206
+ List of (username, review_count, pr_numbers) tuples
207
+ """
208
+ reviewer_counts: Counter = Counter()
209
+ reviewer_prs: dict[str, list[int]] = {}
210
+
211
+ for pr in prs:
212
+ pr_number = pr.get("number")
213
+ if not pr_number:
214
+ continue
215
+
216
+ # Fetch PR details to get reviewers
217
+ details = self.toolkit.execute(
218
+ "github_pr_details",
219
+ pull_number=pr_number,
220
+ include_diff=False,
221
+ include_comments=False,
222
+ include_reviews=True,
223
+ include_review_comments=False,
224
+ )
225
+
226
+ if not details.success:
227
+ continue
228
+
229
+ reviews = details.data.get("reviews", [])
230
+ if not isinstance(reviews, list):
231
+ continue
232
+
233
+ seen_reviewers = set()
234
+ for review in reviews:
235
+ if not isinstance(review, dict):
236
+ continue
237
+ user = review.get("user", {})
238
+ if isinstance(user, dict):
239
+ username = user.get("login")
240
+ if username and username not in seen_reviewers:
241
+ reviewer_counts[username] += 1
242
+ if username not in reviewer_prs:
243
+ reviewer_prs[username] = []
244
+ reviewer_prs[username].append(pr_number)
245
+ seen_reviewers.add(username)
246
+
247
+ # Get top N reviewers
248
+ top = reviewer_counts.most_common(top_n)
249
+ return [(username, count, reviewer_prs.get(username, [])) for username, count in top]
250
+
251
+ def _fetch_reviewer_details(
252
+ self,
253
+ top_reviewers: list[tuple[str, int, list[int]]],
254
+ prs: list[dict],
255
+ ) -> list[ReviewerData]:
256
+ """Fetch detailed review data for top reviewers.
257
+
258
+ Args:
259
+ top_reviewers: List of (username, count, pr_numbers) tuples
260
+ prs: List of PR data
261
+
262
+ Returns:
263
+ List of ReviewerData objects
264
+ """
265
+ reviewer_data = []
266
+
267
+ for username, count, pr_numbers in top_reviewers:
268
+ data = ReviewerData(
269
+ username=username,
270
+ review_count=count,
271
+ prs_reviewed=pr_numbers,
272
+ )
273
+
274
+ # Fetch review comments for each PR they reviewed
275
+ for pr_number in pr_numbers[:10]: # Limit to 10 PRs per reviewer
276
+ details = self.toolkit.execute(
277
+ "github_pr_details",
278
+ pull_number=pr_number,
279
+ include_diff=False,
280
+ include_comments=False,
281
+ include_reviews=True,
282
+ include_review_comments=True,
283
+ )
284
+
285
+ if not details.success:
286
+ continue
287
+
288
+ # Get their reviews
289
+ reviews = details.data.get("reviews", [])
290
+ for review in reviews:
291
+ if not isinstance(review, dict):
292
+ continue
293
+ user = review.get("user", {})
294
+ if isinstance(user, dict) and user.get("login") == username:
295
+ state = review.get("state")
296
+ if state:
297
+ data.review_verdicts.append(state)
298
+ body = review.get("body")
299
+ if body:
300
+ data.review_comments.append({
301
+ "type": "review",
302
+ "pr": pr_number,
303
+ "body": body,
304
+ "state": state,
305
+ })
306
+
307
+ # Get their inline comments
308
+ review_comments = details.data.get("review_comments", [])
309
+ for comment in review_comments:
310
+ if not isinstance(comment, dict):
311
+ continue
312
+ user = comment.get("user", {})
313
+ if isinstance(user, dict) and user.get("login") == username:
314
+ data.review_comments.append({
315
+ "type": "inline",
316
+ "pr": pr_number,
317
+ "path": comment.get("path"),
318
+ "line": comment.get("line") or comment.get("position"),
319
+ "body": comment.get("body"),
320
+ })
321
+
322
+ reviewer_data.append(data)
323
+
324
+ return reviewer_data
325
+
326
+ def _get_cross_team_contributors(self, top_n: int = 5) -> list[ContributorData]:
327
+ """Query Neo4j for contributors who touch multiple communities.
328
+
329
+ Args:
330
+ top_n: Number of contributors to return
331
+
332
+ Returns:
333
+ List of ContributorData objects
334
+ """
335
+ if not self.graph:
336
+ return []
337
+
338
+ try:
339
+ with self.graph.session() as session:
340
+ result = session.run(
341
+ """
342
+ MATCH (c:GitCommit)-[:AUTHORED_BY]->(a:Author)
343
+ MATCH (c)-[:COMMIT_MODIFIES]->(f:File)
344
+ MATCH (f)-[:CONTAINS_CLASS|CONTAINS_FUNCTION]->(entity)
345
+ WHERE entity.community IS NOT NULL
346
+ WITH a, count(DISTINCT entity.community) as communities, count(DISTINCT c) as commits
347
+ WHERE communities >= 2
348
+ RETURN a.name as name, a.email as email, communities, commits
349
+ ORDER BY communities DESC, commits DESC
350
+ LIMIT $top_n
351
+ """,
352
+ top_n=top_n,
353
+ )
354
+
355
+ contributors = []
356
+ for record in result:
357
+ contributors.append(ContributorData(
358
+ name=record["name"],
359
+ email=record["email"],
360
+ communities_touched=record["communities"],
361
+ commit_count=record["commits"],
362
+ ))
363
+
364
+ return contributors
365
+
366
+ except Exception as e:
367
+ log.warning(f"Failed to query cross-team contributors: {e}")
368
+ return []
369
+
370
+ def _synthesize_profile(
371
+ self,
372
+ reviewer_data: list[ReviewerData],
373
+ cross_team: list[ContributorData],
374
+ pr_count: int,
375
+ ) -> str:
376
+ """Use LLM to synthesize a reviewer profile from the data.
377
+
378
+ Args:
379
+ reviewer_data: Data about top reviewers
380
+ cross_team: Data about cross-team contributors
381
+ pr_count: Total number of PRs analyzed
382
+
383
+ Returns:
384
+ Generated reviewer profile markdown
385
+ """
386
+ # Build context for the LLM
387
+ context = {
388
+ "pr_count": pr_count,
389
+ "reviewers": [],
390
+ "cross_team_contributors": [],
391
+ }
392
+
393
+ for data in reviewer_data:
394
+ context["reviewers"].append({
395
+ "username": data.username,
396
+ "review_count": data.review_count,
397
+ "verdicts": dict(Counter(data.review_verdicts)),
398
+ "sample_comments": data.review_comments[:20], # Limit to 20 comments
399
+ })
400
+
401
+ for contrib in cross_team:
402
+ context["cross_team_contributors"].append({
403
+ "name": contrib.name,
404
+ "communities_touched": contrib.communities_touched,
405
+ "commit_count": contrib.commit_count,
406
+ })
407
+
408
+ # Call LLM
409
+ response = self.provider.chat(
410
+ [
411
+ {"role": "system", "content": SYNTHESIS_PROMPT},
412
+ {"role": "user", "content": f"Analyze this reviewer data and generate a profile:\n\n{json.dumps(context, indent=2)}"},
413
+ ]
414
+ )
415
+
416
+ return response.content or ""
417
+
418
+ def save_template(
419
+ self,
420
+ profile: str,
421
+ output_path: Optional[Path] = None,
422
+ ) -> Path:
423
+ """Save the generated profile to .emdash-rules/reviewer.md.template.
424
+
425
+ Args:
426
+ profile: Generated profile markdown
427
+ output_path: Optional custom output path
428
+
429
+ Returns:
430
+ Path where the template was saved
431
+ """
432
+ if output_path is None:
433
+ output_path = Path.cwd() / ".emdash-rules" / "reviewer.md.template"
434
+
435
+ # Ensure directory exists
436
+ output_path.parent.mkdir(parents=True, exist_ok=True)
437
+
438
+ # Write the template
439
+ with open(output_path, "w", encoding="utf-8") as f:
440
+ f.write(profile)
441
+
442
+ return output_path
443
+
444
+ # Alias for API compatibility
445
+ def build(
446
+ self,
447
+ top_reviewers: int = 5,
448
+ top_contributors: int = 10,
449
+ max_prs: int = 50,
450
+ ) -> dict:
451
+ """Build reviewer profile (API compatibility method).
452
+
453
+ Args:
454
+ top_reviewers: Number of top reviewers to analyze
455
+ top_contributors: Number of cross-team contributors to include
456
+ max_prs: Maximum PRs to fetch for analysis
457
+
458
+ Returns:
459
+ Dictionary with profile results
460
+ """
461
+ profile = self.analyze(
462
+ top_n_reviewers=top_reviewers,
463
+ top_n_contributors=top_contributors,
464
+ max_prs=max_prs,
465
+ )
466
+ return {
467
+ "profile": profile,
468
+ "reviewers_analyzed": top_reviewers,
469
+ "contributors_analyzed": top_contributors,
470
+ "prs_analyzed": max_prs,
471
+ }
472
+
473
+
474
+ # Alias for backwards compatibility with API
475
+ ReviewerProfileBuilder = ReviewerProfileAgent
@@ -0,0 +1,123 @@
1
+ """Rules loader from .emdash/rules/*.md files.
2
+
3
+ Allows users to define custom rules and guidelines that are
4
+ injected into agent system prompts.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from ..utils.logger import log
11
+
12
+
13
+ def load_rules(rules_dir: Optional[Path] = None) -> str:
14
+ """Load rules from .emdash/rules/ directory.
15
+
16
+ Rules files are markdown that get concatenated and injected
17
+ into the agent's system prompt.
18
+
19
+ Example rules file:
20
+
21
+ ```markdown
22
+ # Code Review Guidelines
23
+
24
+ - Always check for security implications
25
+ - Prefer composition over inheritance
26
+ - Document all public APIs
27
+ ```
28
+
29
+ Args:
30
+ rules_dir: Directory containing rule .md files.
31
+ Defaults to .emdash/rules/ in cwd.
32
+
33
+ Returns:
34
+ Combined rules as a string
35
+ """
36
+ if rules_dir is None:
37
+ rules_dir = Path.cwd() / ".emdash" / "rules"
38
+
39
+ if not rules_dir.exists():
40
+ return ""
41
+
42
+ rules_parts = []
43
+
44
+ # Load all .md files in order
45
+ for md_file in sorted(rules_dir.glob("*.md")):
46
+ try:
47
+ content = md_file.read_text().strip()
48
+ if content:
49
+ rules_parts.append(content)
50
+ log.debug(f"Loaded rules from: {md_file.name}")
51
+ except Exception as e:
52
+ log.warning(f"Failed to load rules from {md_file}: {e}")
53
+
54
+ if rules_parts:
55
+ combined = "\n\n---\n\n".join(rules_parts)
56
+ log.info(f"Loaded {len(rules_parts)} rule files")
57
+ return combined
58
+
59
+ return ""
60
+
61
+
62
+ def get_rules_for_agent(
63
+ agent_name: str,
64
+ rules_dir: Optional[Path] = None,
65
+ ) -> str:
66
+ """Get rules specific to an agent.
67
+
68
+ Looks for:
69
+ 1. Agent-specific rules in {rules_dir}/{agent_name}.md
70
+ 2. General rules in {rules_dir}/*.md
71
+
72
+ Args:
73
+ agent_name: Name of the agent
74
+ rules_dir: Optional rules directory
75
+
76
+ Returns:
77
+ Combined rules string
78
+ """
79
+ if rules_dir is None:
80
+ rules_dir = Path.cwd() / ".emdash" / "rules"
81
+
82
+ parts = []
83
+
84
+ # Load general rules first
85
+ general_rules = load_rules(rules_dir)
86
+ if general_rules:
87
+ parts.append(general_rules)
88
+
89
+ # Look for agent-specific rules
90
+ agent_rules_file = rules_dir / f"{agent_name}.md"
91
+ if agent_rules_file.exists():
92
+ try:
93
+ agent_rules = agent_rules_file.read_text().strip()
94
+ if agent_rules:
95
+ parts.append(f"# Agent-Specific Rules: {agent_name}\n\n{agent_rules}")
96
+ log.debug(f"Loaded agent-specific rules for: {agent_name}")
97
+ except Exception as e:
98
+ log.warning(f"Failed to load agent rules: {e}")
99
+
100
+ return "\n\n---\n\n".join(parts)
101
+
102
+
103
+ def format_rules_for_prompt(rules: str) -> str:
104
+ """Format rules for inclusion in a system prompt.
105
+
106
+ Args:
107
+ rules: Raw rules content
108
+
109
+ Returns:
110
+ Formatted rules section
111
+ """
112
+ if not rules:
113
+ return ""
114
+
115
+ return f"""
116
+ ## Project Guidelines
117
+
118
+ The following rules and guidelines should be followed:
119
+
120
+ {rules}
121
+
122
+ ---
123
+ """