|
118 | 118 | _SERVICE_EMAILS: dict[str, tuple[str, int]] = { |
119 | 119 | "cursoragent@cursor.com": ("cursor", 1), |
120 | 120 | "devin-ai-integration[bot]@users.noreply.github.com": ("devin", 1), |
| 121 | + # Primarily Claude's co-author identity; as AUTHOR it is tier 1 like every |
| 122 | + # other service e-mail here. |
| 123 | + "noreply@anthropic.com": ("claude", 1), |
| 124 | +} |
| 125 | + |
| 126 | +# Agent-vendor e-mail domains → the exact agent identities each vendor emits. |
| 127 | +# A domain alone is never enough — real humans work at these companies — so an |
| 128 | +# identity here counts only when the LOCAL PART fully matches the vendor's |
| 129 | +# allowlisted agent identity (anchored fullmatch, never a substring scan, and |
| 130 | +# per vendor, never a flat token set): ``claude@anthropic.com`` and the |
| 131 | +# ``claude-<model>`` slugs match; ``jane@anthropic.com``, |
| 132 | +# ``jean-claude@anthropic.com`` and ``codex@anthropic.com`` never do. |
| 133 | +_VENDOR_DOMAIN_LOCALS: dict[str, tuple[re.Pattern[str], str]] = { |
| 134 | + "anthropic.com": (re.compile(r"claude(?:-(?:opus|sonnet|haiku)(?:-[\w.]+)?)?"), "claude"), |
| 135 | + "openai.com": (re.compile(r"codex"), "codex"), |
| 136 | + "cursor.com": (re.compile(r"cursor(?:agent)?"), "cursor"), |
| 137 | + "devin.ai": (re.compile(r"devin"), "devin"), |
121 | 138 | } |
122 | 139 |
|
123 | 140 | # Commit-message footers → agent (exact service phrases only). |
|
158 | 175 |
|
159 | 176 | _AIDER_NAME_RE = re.compile(r"\(aider\)\s*$") |
160 | 177 |
|
| 178 | +# Any ``Co-authored-by:`` trailer's e-mail, for registry-based identity |
| 179 | +# matching: agent service identities the explicit patterns above don't name |
| 180 | +# (bot noreply logins, exact service e-mails, vendor-domain identities) are |
| 181 | +# resolved through the same :meth:`AgentProvenanceClassifier._identity_match` |
| 182 | +# registry — one list, no parallel patterns. |
| 183 | +_COAUTHOR_EMAIL_RE = re.compile(r"^co-authored-by:[^<\n]*<([^>\s]+)>", re.IGNORECASE | re.MULTILINE) |
| 184 | + |
161 | 185 |
|
162 | 186 | def _normalize_agent_token(value: str, *, allow_slug: bool) -> str | None: |
163 | 187 | """Resolve a free-text tool/agent name to a canonical label. |
@@ -274,6 +298,10 @@ def _identity_match(self, email: str) -> tuple[str, int] | None: |
274 | 298 | m = self._bot_email_re.match(e) |
275 | 299 | if m: |
276 | 300 | return (_BOT_LOGINS[m.group(1).lower()], 1) |
| 301 | + local, _, domain = e.partition("@") |
| 302 | + vendor = _VENDOR_DOMAIN_LOCALS.get(domain) |
| 303 | + if vendor and vendor[0].fullmatch(local): |
| 304 | + return (vendor[1], 1) |
277 | 305 | return None |
278 | 306 |
|
279 | 307 | def classify( |
@@ -328,6 +356,10 @@ def classify( |
328 | 356 | for pat, agent in self._coauthors: |
329 | 357 | if pat.search(message): |
330 | 358 | return AgentProvenance(agent, 3, "coauthor_trailer", "high") |
| 359 | + for m in _COAUTHOR_EMAIL_RE.finditer(message): |
| 360 | + hit = self._identity_match(m.group(1)) |
| 361 | + if hit: |
| 362 | + return AgentProvenance(hit[0], 3, "coauthor_trailer", "high") |
331 | 363 | return _HUMAN |
332 | 364 |
|
333 | 365 |
|
|
0 commit comments