devrel-origin 0.2.14__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 (98) hide show
  1. devrel_origin/__init__.py +15 -0
  2. devrel_origin/cli/__init__.py +92 -0
  3. devrel_origin/cli/_common.py +243 -0
  4. devrel_origin/cli/analytics.py +28 -0
  5. devrel_origin/cli/argus.py +497 -0
  6. devrel_origin/cli/auth.py +227 -0
  7. devrel_origin/cli/config.py +108 -0
  8. devrel_origin/cli/content.py +259 -0
  9. devrel_origin/cli/cost.py +108 -0
  10. devrel_origin/cli/cro.py +298 -0
  11. devrel_origin/cli/deliverables.py +65 -0
  12. devrel_origin/cli/docs.py +91 -0
  13. devrel_origin/cli/doctor.py +178 -0
  14. devrel_origin/cli/experiment.py +29 -0
  15. devrel_origin/cli/growth.py +97 -0
  16. devrel_origin/cli/init.py +472 -0
  17. devrel_origin/cli/intel.py +27 -0
  18. devrel_origin/cli/kb.py +96 -0
  19. devrel_origin/cli/listen.py +31 -0
  20. devrel_origin/cli/marketing.py +66 -0
  21. devrel_origin/cli/migrate.py +45 -0
  22. devrel_origin/cli/run.py +46 -0
  23. devrel_origin/cli/sales.py +57 -0
  24. devrel_origin/cli/schedule.py +62 -0
  25. devrel_origin/cli/synthesize.py +28 -0
  26. devrel_origin/cli/triage.py +29 -0
  27. devrel_origin/cli/video.py +35 -0
  28. devrel_origin/core/__init__.py +58 -0
  29. devrel_origin/core/agent_config.py +75 -0
  30. devrel_origin/core/argus.py +964 -0
  31. devrel_origin/core/atlas.py +1450 -0
  32. devrel_origin/core/base.py +372 -0
  33. devrel_origin/core/cyra.py +563 -0
  34. devrel_origin/core/dex.py +708 -0
  35. devrel_origin/core/echo.py +614 -0
  36. devrel_origin/core/growth/__init__.py +27 -0
  37. devrel_origin/core/growth/recommendations.py +219 -0
  38. devrel_origin/core/growth/target_kinds.py +51 -0
  39. devrel_origin/core/iris.py +513 -0
  40. devrel_origin/core/kai.py +1367 -0
  41. devrel_origin/core/llm.py +542 -0
  42. devrel_origin/core/llm_backends.py +274 -0
  43. devrel_origin/core/mox.py +514 -0
  44. devrel_origin/core/nova.py +349 -0
  45. devrel_origin/core/pax.py +1205 -0
  46. devrel_origin/core/rex.py +532 -0
  47. devrel_origin/core/sage.py +486 -0
  48. devrel_origin/core/sentinel.py +385 -0
  49. devrel_origin/core/types.py +98 -0
  50. devrel_origin/core/video/__init__.py +22 -0
  51. devrel_origin/core/video/assembler.py +131 -0
  52. devrel_origin/core/video/browser_recorder.py +118 -0
  53. devrel_origin/core/video/desktop_recorder.py +254 -0
  54. devrel_origin/core/video/overlay_renderer.py +143 -0
  55. devrel_origin/core/video/script_parser.py +147 -0
  56. devrel_origin/core/video/tts_engine.py +82 -0
  57. devrel_origin/core/vox.py +268 -0
  58. devrel_origin/core/watchdog.py +321 -0
  59. devrel_origin/project/__init__.py +1 -0
  60. devrel_origin/project/config.py +75 -0
  61. devrel_origin/project/cost_sink.py +61 -0
  62. devrel_origin/project/init.py +104 -0
  63. devrel_origin/project/paths.py +75 -0
  64. devrel_origin/project/state.py +241 -0
  65. devrel_origin/project/templates/__init__.py +4 -0
  66. devrel_origin/project/templates/config.toml +24 -0
  67. devrel_origin/project/templates/devrel.gitignore +10 -0
  68. devrel_origin/project/templates/slop-blocklist.md +45 -0
  69. devrel_origin/project/templates/style.md +24 -0
  70. devrel_origin/project/templates/voice.md +29 -0
  71. devrel_origin/quality/__init__.py +66 -0
  72. devrel_origin/quality/editorial.py +357 -0
  73. devrel_origin/quality/persona.py +84 -0
  74. devrel_origin/quality/readability.py +148 -0
  75. devrel_origin/quality/slop.py +167 -0
  76. devrel_origin/quality/style.py +110 -0
  77. devrel_origin/quality/voice.py +15 -0
  78. devrel_origin/tools/__init__.py +9 -0
  79. devrel_origin/tools/analytics.py +304 -0
  80. devrel_origin/tools/api_client.py +393 -0
  81. devrel_origin/tools/apollo_client.py +305 -0
  82. devrel_origin/tools/code_validator.py +428 -0
  83. devrel_origin/tools/github_tools.py +297 -0
  84. devrel_origin/tools/instantly_client.py +412 -0
  85. devrel_origin/tools/kb_harvester.py +340 -0
  86. devrel_origin/tools/mcp_server.py +578 -0
  87. devrel_origin/tools/notifications.py +245 -0
  88. devrel_origin/tools/run_report.py +193 -0
  89. devrel_origin/tools/scheduler.py +231 -0
  90. devrel_origin/tools/search_tools.py +321 -0
  91. devrel_origin/tools/self_improve.py +168 -0
  92. devrel_origin/tools/sheets.py +236 -0
  93. devrel_origin-0.2.14.dist-info/METADATA +354 -0
  94. devrel_origin-0.2.14.dist-info/RECORD +98 -0
  95. devrel_origin-0.2.14.dist-info/WHEEL +5 -0
  96. devrel_origin-0.2.14.dist-info/entry_points.txt +2 -0
  97. devrel_origin-0.2.14.dist-info/licenses/LICENSE +21 -0
  98. devrel_origin-0.2.14.dist-info/top_level.txt +1 -0
@@ -0,0 +1,486 @@
1
+ """
2
+ Sage — Community Manager Agent
3
+
4
+ Triages GitHub issues, analyzes sentiment, flags at-risk contributors,
5
+ and identifies community champions.
6
+ """
7
+
8
+ import logging
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ from pathlib import Path
12
+ from typing import Any, Optional
13
+
14
+ from devrel_origin.tools.api_client import PostHogClient
15
+ from devrel_origin.tools.github_tools import GitHubIssue, GitHubTools
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ # Shared keyword vocabularies for triage classification. Single source of
21
+ # truth — avoids divergence between sentiment, category, and priority logic.
22
+ CHURN_SIGNALS: tuple[str, ...] = (
23
+ "switching to",
24
+ "give up",
25
+ "moving away",
26
+ "nth time",
27
+ )
28
+
29
+ FRUSTRATION_SIGNALS: tuple[str, ...] = (
30
+ "broken",
31
+ "terrible",
32
+ "worst",
33
+ "!!!",
34
+ )
35
+
36
+ BUG_KEYWORDS: tuple[str, ...] = (
37
+ "bug",
38
+ "error",
39
+ "crash",
40
+ "broken",
41
+ "fix",
42
+ )
43
+
44
+ CRITICAL_KEYWORDS: tuple[str, ...] = (
45
+ "data loss",
46
+ "security",
47
+ "vulnerability",
48
+ "crash",
49
+ )
50
+
51
+
52
+ class IssuePriority(Enum):
53
+ CRITICAL = "critical" # Data loss, security, complete breakage
54
+ HIGH = "high" # Feature broken, no workaround
55
+ MEDIUM = "medium" # Feature degraded, workaround exists
56
+ LOW = "low" # Enhancement, cosmetic, nice-to-have
57
+
58
+
59
+ class SentimentScore(Enum):
60
+ POSITIVE = "positive"
61
+ NEUTRAL = "neutral"
62
+ FRUSTRATED = "frustrated"
63
+ CHURNING = "churning" # Signals intent to leave
64
+
65
+
66
+ @dataclass
67
+ class TriagedIssue:
68
+ """A GitHub issue that has been triaged and categorized."""
69
+
70
+ issue_number: int
71
+ title: str
72
+ author: str
73
+ priority: IssuePriority
74
+ sentiment: SentimentScore
75
+ category: str # bug, feature_request, question, docs, performance
76
+ product_area: str # analytics, replay, flags, experiments, surveys, etc.
77
+ summary: str
78
+ suggested_response: str
79
+ churn_risk: bool = False
80
+ champion_signal: bool = False
81
+
82
+
83
+ @dataclass
84
+ class TriageReport:
85
+ """Weekly triage summary."""
86
+
87
+ week_of: str
88
+ total_issues: int
89
+ issues: list[TriagedIssue] = field(default_factory=list)
90
+ churn_risks: list[str] = field(default_factory=list)
91
+ champions: list[str] = field(default_factory=list)
92
+ sentiment_breakdown: dict[str, int] = field(default_factory=dict)
93
+ category_breakdown: dict[str, int] = field(default_factory=dict)
94
+ product_area_breakdown: dict[str, int] = field(default_factory=dict)
95
+
96
+
97
+ class Sage:
98
+ """
99
+ Community Manager agent for GitHub issue triage and community health.
100
+
101
+ Capabilities:
102
+ - Triage incoming GitHub issues by priority, category, and product area
103
+ - Analyze author sentiment and flag churn risks
104
+ - Identify community champions (frequent contributors, helpful commenters)
105
+ - Generate suggested first responses for each issue
106
+ - Produce weekly triage reports with actionable recommendations
107
+
108
+ Tools:
109
+ 1. github_issues_fetch — Pull recent issues from devrel-ai-agents repo
110
+ 2. github_comments_fetch — Pull comments on specific issues
111
+ 3. github_user_history — Analyze a user's contribution history
112
+ 4. sentiment_analyzer — Classify text sentiment
113
+ 5. issue_categorizer — Map issues to product areas and types
114
+ 6. churn_detector — Flag users showing departure signals
115
+ 7. champion_identifier — Score users on community contribution
116
+ 8. response_generator — Draft empathetic, helpful first responses
117
+ 9. label_suggester — Suggest GitHub labels based on content
118
+ 10. duplicate_detector — Find similar existing issues
119
+ 11. priority_scorer — Score issue urgency based on impact/frequency
120
+ 12. escalation_router — Route critical issues to the right team
121
+ 13. weekly_report_compiler — Aggregate triage data into reports
122
+ 14. notification_dispatcher — Alert team members about urgent issues
123
+ """
124
+
125
+ SYSTEM_PROMPT = """You are Sage, a community manager for OpenClaw, an open-source
126
+ system of 10 specialized AI agents that replaces a full DevRel + Sales team for DevTools
127
+ companies. OpenClaw provides orchestration (Atlas), an agent SDK, MCP tools,
128
+ a knowledge base, scoring/eval, prompt optimization, onboarding/docs, and security —
129
+ built on Claude SDK + MCP.
130
+
131
+ Your mission is to make every developer who interacts with OpenClaw feel heard,
132
+ helped, and valued. You triage issues with empathy and precision.
133
+
134
+ Triage principles:
135
+ 1. EMPATHY FIRST — Acknowledge the person's frustration before diving into technical details
136
+ 2. FAST RESPONSE — First response within 4 hours for critical, 24 hours for all others
137
+ 3. ACCURATE ROUTING — Tag the right product area and team so nothing falls through cracks
138
+ 4. CHURN PREVENTION — Flag users who show signs of giving up (repeated issues, frustrated tone)
139
+ 5. CHAMPION CULTIVATION — Recognize users who help others, submit quality bug reports, or contribute PRs
140
+
141
+ Sentiment signals:
142
+ - CHURNING: "I'm switching to...", "This is the Nth time...", "I give up"
143
+ - FRUSTRATED: Multiple exclamation marks, ALL CAPS, words like "broken", "terrible"
144
+ - NEUTRAL: Standard bug reports, feature requests
145
+ - POSITIVE: "Love OpenClaw", "Great work", "This is exactly what I needed"
146
+
147
+ Champion signals:
148
+ - Helps other users in issues/Discourse
149
+ - Submits well-structured bug reports with reproduction steps
150
+ - Opens PRs or suggests fixes
151
+ - Shares OpenClaw content on social media"""
152
+
153
+ def __init__(
154
+ self,
155
+ api_client: PostHogClient,
156
+ knowledge_base_path: Path,
157
+ github_tools: Optional["GitHubTools"] = None,
158
+ ):
159
+ self.api_client = api_client
160
+ self.knowledge_base_path = knowledge_base_path
161
+ self.github_tools = github_tools
162
+
163
+ async def execute(
164
+ self,
165
+ task: str,
166
+ context: Optional[dict[str, Any]] = None,
167
+ ) -> dict[str, Any]:
168
+ """
169
+ Execute a community management task.
170
+
171
+ Fetches GitHub issues, analyzes them, and produces triage output
172
+ with sentiment analysis and risk flags.
173
+ """
174
+ logger.info(f"Sage executing: {task[:80]}...")
175
+
176
+ # Fetch issues from GitHub (graceful fallback if no tools)
177
+ raw_issues: list[GitHubIssue] = []
178
+ if self.github_tools:
179
+ try:
180
+ raw_issues = await self.github_tools.fetch_recent_issues(days=7)
181
+ # Filter out PRs
182
+ raw_issues = [i for i in raw_issues if not i.is_pull_request]
183
+ except Exception as exc:
184
+ logger.warning(f"GitHub fetch failed: {exc}")
185
+
186
+ # Triage each issue
187
+ triaged: list[TriagedIssue] = []
188
+ for issue in raw_issues:
189
+ triaged.append(
190
+ await self.triage_issue(
191
+ issue_number=issue.number,
192
+ title=issue.title,
193
+ body=issue.body,
194
+ author=issue.author,
195
+ comments_count=getattr(issue, "comments_count", 0) or 0,
196
+ reactions_total=getattr(issue, "reactions_total", 0) or 0,
197
+ )
198
+ )
199
+
200
+ # Build breakdowns
201
+ sentiment_breakdown: dict[str, int] = {}
202
+ category_breakdown: dict[str, int] = {}
203
+ product_area_breakdown: dict[str, int] = {}
204
+ churn_risks: list[str] = []
205
+
206
+ for t in triaged:
207
+ sentiment_breakdown[t.sentiment.value] = (
208
+ sentiment_breakdown.get(t.sentiment.value, 0) + 1
209
+ )
210
+ category_breakdown[t.category] = category_breakdown.get(t.category, 0) + 1
211
+ product_area_breakdown[t.product_area] = (
212
+ product_area_breakdown.get(t.product_area, 0) + 1
213
+ )
214
+ if t.churn_risk:
215
+ churn_risks.append(t.author)
216
+
217
+ return {
218
+ "agent": "sage",
219
+ "task": task,
220
+ "issues": [
221
+ {
222
+ "number": t.issue_number,
223
+ "title": t.title,
224
+ "author": t.author,
225
+ "priority": t.priority.value,
226
+ "sentiment": t.sentiment.value,
227
+ "category": t.category,
228
+ "product_area": t.product_area,
229
+ "summary": t.summary,
230
+ "suggested_response": t.suggested_response,
231
+ "churn_risk": t.churn_risk,
232
+ }
233
+ for t in triaged
234
+ ],
235
+ "churn_risks": churn_risks,
236
+ "champions": self._identify_champions(triaged),
237
+ "sentiment_breakdown": sentiment_breakdown,
238
+ "category_breakdown": category_breakdown,
239
+ "product_area_breakdown": product_area_breakdown,
240
+ "status": "triaged",
241
+ }
242
+
243
+ async def triage_issue(
244
+ self,
245
+ issue_number: int,
246
+ title: str,
247
+ body: str,
248
+ author: str,
249
+ comments_count: int = 0,
250
+ reactions_total: int = 0,
251
+ ) -> TriagedIssue:
252
+ """Triage a single GitHub issue."""
253
+ # Analyze sentiment
254
+ sentiment = self._analyze_sentiment(body)
255
+
256
+ # Detect churn risk
257
+ churn_risk = sentiment == SentimentScore.CHURNING
258
+
259
+ # Categorize
260
+ category = self._categorize_issue(title, body)
261
+ product_area = self._detect_product_area(title, body)
262
+ priority = self._score_priority(title, body, sentiment)
263
+
264
+ # Champion detection — high-engagement issues / PR-referencing bodies
265
+ # are candidate champions for downstream identification.
266
+ champion = self._detect_champion_signal(
267
+ body=body,
268
+ comments_count=comments_count,
269
+ reactions_total=reactions_total,
270
+ )
271
+
272
+ return TriagedIssue(
273
+ issue_number=issue_number,
274
+ title=title,
275
+ author=author,
276
+ priority=priority,
277
+ sentiment=sentiment,
278
+ category=category,
279
+ product_area=product_area,
280
+ summary=f"[{priority.value.upper()}] {category} in {product_area}",
281
+ suggested_response=self._draft_response(
282
+ title,
283
+ category,
284
+ priority,
285
+ sentiment,
286
+ author,
287
+ ),
288
+ churn_risk=churn_risk,
289
+ champion_signal=champion,
290
+ )
291
+
292
+ # Module-level threshold map — tuned high enough that random noise
293
+ # doesn't trigger champion-flagging, low enough that genuine community
294
+ # engagement is caught.
295
+ CHAMPION_THRESHOLDS = {
296
+ "comments_count": 3,
297
+ "reactions_total": 5,
298
+ }
299
+
300
+ def _detect_champion_signal(
301
+ self,
302
+ body: str,
303
+ comments_count: int = 0,
304
+ reactions_total: int = 0,
305
+ ) -> bool:
306
+ """Return True if the issue shows champion-grade engagement.
307
+
308
+ A "champion signal" means the user is going beyond just filing —
309
+ attracting community discussion (comments), strong reactions, or
310
+ referencing a PR they opened to fix the issue. Used by
311
+ ``_identify_champions`` downstream.
312
+ """
313
+ if (comments_count or 0) >= self.CHAMPION_THRESHOLDS["comments_count"]:
314
+ return True
315
+ if (reactions_total or 0) >= self.CHAMPION_THRESHOLDS["reactions_total"]:
316
+ return True
317
+ body_lower = (body or "").lower()
318
+ if "pr #" in body_lower or "#pull" in body_lower or "pull/" in body_lower:
319
+ return True
320
+ return False
321
+
322
+ def _analyze_sentiment(self, text: str) -> SentimentScore:
323
+ """Rule-based sentiment pre-filter before LLM analysis."""
324
+ text_lower = text.lower()
325
+
326
+ if any(signal in text_lower for signal in CHURN_SIGNALS):
327
+ return SentimentScore.CHURNING
328
+ if any(signal in text_lower for signal in FRUSTRATION_SIGNALS):
329
+ return SentimentScore.FRUSTRATED
330
+ if any(word in text_lower for word in ["love", "great", "awesome", "thanks"]):
331
+ return SentimentScore.POSITIVE
332
+ return SentimentScore.NEUTRAL
333
+
334
+ def _categorize_issue(self, title: str, body: str) -> str:
335
+ """Categorize issue type based on content."""
336
+ text = f"{title} {body}".lower()
337
+ if any(w in text for w in BUG_KEYWORDS):
338
+ return "bug"
339
+ if any(w in text for w in ["feature", "request", "would be nice", "suggestion"]):
340
+ return "feature_request"
341
+ if any(w in text for w in ["how to", "question", "help", "?"]):
342
+ return "question"
343
+ if any(w in text for w in ["docs", "documentation", "readme", "typo"]):
344
+ return "docs"
345
+ if any(w in text for w in ["slow", "performance", "latency", "timeout"]):
346
+ return "performance"
347
+ return "bug"
348
+
349
+ def _detect_product_area(self, title: str, body: str) -> str:
350
+ """Map issue to OpenClaw product area."""
351
+ text = f"{title} {body}".lower()
352
+ areas = {
353
+ "orchestration": [
354
+ "orchestrat",
355
+ "atlas",
356
+ "pipeline",
357
+ "weekly cycle",
358
+ "delegation",
359
+ "hub",
360
+ "spoke",
361
+ ],
362
+ "agent_sdk": [
363
+ "agent sdk",
364
+ "sdk",
365
+ "claude sdk",
366
+ "agent framework",
367
+ "base agent",
368
+ "execute",
369
+ ],
370
+ "mcp_tools": ["mcp", "tool", "json-rpc", "stdio", "tool definition", "manifest"],
371
+ "knowledge_base": [
372
+ "knowledge base",
373
+ "knowledge",
374
+ "docs",
375
+ "markdown",
376
+ "rglob",
377
+ "indexing",
378
+ ],
379
+ "scoring_eval": [
380
+ "score",
381
+ "scoring",
382
+ "eval",
383
+ "evaluation",
384
+ "metrics",
385
+ "benchmark",
386
+ "test",
387
+ ],
388
+ "prompt_optimization": ["prompt", "template", "optimization", "tuning", "generation"],
389
+ "onboarding_docs": [
390
+ "onboarding",
391
+ "documentation",
392
+ "tutorial",
393
+ "guide",
394
+ "getting started",
395
+ "setup",
396
+ ],
397
+ "security": [
398
+ "security",
399
+ "auth",
400
+ "permission",
401
+ "token",
402
+ "secret",
403
+ "vulnerability",
404
+ "access",
405
+ ],
406
+ }
407
+ for area, keywords in areas.items():
408
+ if any(kw in text for kw in keywords):
409
+ return area
410
+ return "orchestration" # default
411
+
412
+ @staticmethod
413
+ def _identify_champions(triaged: list[TriagedIssue]) -> list[str]:
414
+ """Identify community champions from triaged issues.
415
+
416
+ Champions are users with positive sentiment, helpful contributions,
417
+ or multiple quality issue reports.
418
+ """
419
+ author_signals: dict[str, int] = {}
420
+ for issue in triaged:
421
+ author = issue.author
422
+ score = 0
423
+ if issue.sentiment == SentimentScore.POSITIVE:
424
+ score += 2
425
+ if issue.champion_signal:
426
+ score += 3
427
+ if issue.category in ("feature_request", "question"):
428
+ score += 1 # Engaged users file features/questions
429
+ if score > 0:
430
+ author_signals[author] = author_signals.get(author, 0) + score
431
+
432
+ # Champions = authors with score >= 3
433
+ return [author for author, score in author_signals.items() if score >= 3]
434
+
435
+ def _score_priority(self, title: str, body: str, sentiment: SentimentScore) -> IssuePriority:
436
+ """Score issue priority based on content and sentiment."""
437
+ text = f"{title} {body}".lower()
438
+ if any(w in text for w in CRITICAL_KEYWORDS):
439
+ return IssuePriority.CRITICAL
440
+ if sentiment == SentimentScore.CHURNING:
441
+ return IssuePriority.HIGH
442
+ if any(w in text for w in ["broken", "cannot", "unable", "blocking"]):
443
+ return IssuePriority.HIGH
444
+ if sentiment == SentimentScore.FRUSTRATED:
445
+ return IssuePriority.MEDIUM
446
+ return IssuePriority.LOW
447
+
448
+ def _draft_response(
449
+ self,
450
+ title: str,
451
+ category: str,
452
+ priority: IssuePriority,
453
+ sentiment: SentimentScore = SentimentScore.NEUTRAL,
454
+ author: str = "",
455
+ ) -> str:
456
+ """Draft a suggested first response.
457
+
458
+ Branch order matters: a CHURNING user with a CRITICAL bug should
459
+ receive the empathetic response, not the templated critical one,
460
+ so we check sentiment first.
461
+ """
462
+ # CHURNING comes BEFORE CRITICAL on purpose — frustrated users
463
+ # need empathy first, not a templated triage notice.
464
+ if sentiment == SentimentScore.CHURNING:
465
+ handle = f"@{author} " if author else ""
466
+ return (
467
+ f"Hey {handle}— I hear you, and I'm sorry this has been frustrating. "
468
+ f"This is on me to help you fix. Can you share: (1) what version "
469
+ f"you're on, (2) the exact error or behavior you're seeing, and "
470
+ f"(3) what you've already tried? I'll dig in personally."
471
+ )
472
+ if priority == IssuePriority.CRITICAL:
473
+ return (
474
+ "Thanks for reporting this — we're treating this as critical priority. "
475
+ "Our team is investigating now. Could you share your OpenClaw version "
476
+ "and any relevant error logs?"
477
+ )
478
+ if category == "question":
479
+ return (
480
+ "Great question! Let me point you to the relevant docs. "
481
+ "If those don't fully answer it, let us know and we'll dig deeper."
482
+ )
483
+ return (
484
+ "Thanks for raising this! We've added it to our triage queue. "
485
+ "A team member will follow up shortly."
486
+ )