feat(x): resolve X handles for person/brand topics via agent WebSearch

When a topic is a person/brand (e.g. "Dor Brothers", "Jason Calacanis"),
the agent now resolves their X handle via WebSearch before running the
script, then passes --x-handle to search their posts unfiltered (no
topic keywords required). This finds posts the entity made without
mentioning their own name.

- SKILL.md + OpenClaw variant: Step 0.5 handle resolution instructions
- last30days.py: --x-handle CLI arg, passed through to _run_supplemental()
- bird_x.search_handles(): topic is now Optional[str] for unfiltered mode
- schema.py: resolved_x_handle field on Report
- render.py: show resolved handle in stats output

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Van Horn
2026-02-25 20:00:49 -08:00
parent bed0557b65
commit 4f584a4e96
7 changed files with 402 additions and 9 deletions
+7 -4
View File
@@ -272,7 +272,7 @@ def search_x(
def search_handles(
handles: List[str],
topic: str,
topic: Optional[str],
from_date: str,
count_per: int = 5,
) -> List[Dict[str, Any]]:
@@ -283,7 +283,7 @@ def search_handles(
Args:
handles: List of X handles to search (without @)
topic: Search topic (core subject, not full verbose query)
topic: Search topic (core subject), or None for unfiltered search
from_date: Start date (YYYY-MM-DD)
count_per: Results to request per handle
@@ -291,11 +291,14 @@ def search_handles(
List of raw item dicts (same format as parse_bird_response output).
"""
all_items = []
core_topic = _extract_core_subject(topic)
core_topic = _extract_core_subject(topic) if topic else None
for handle in handles:
handle = handle.lstrip("@")
query = f"from:{handle} {core_topic} since:{from_date}"
if core_topic:
query = f"from:{handle} {core_topic} since:{from_date}"
else:
query = f"from:{handle} since:{from_date}"
cmd = [
"node", str(_BIRD_SEARCH_MJS),
+6 -1
View File
@@ -117,6 +117,8 @@ def render_compact(report: schema.Report, limit: int = 15, missing_keys: str = "
lines.append(f"**OpenAI Model:** {report.openai_model_used}")
if report.xai_model_used:
lines.append(f"**xAI Model:** {report.xai_model_used}")
if report.resolved_x_handle:
lines.append(f"**Resolved X Handle:** @{report.resolved_x_handle}")
lines.append("")
# Coverage note for partial coverage
@@ -330,7 +332,10 @@ def render_source_status(report: schema.Report, source_info: dict = None) -> str
if report.x_error:
lines.append(f" ❌ X: error — {report.x_error}")
elif report.x:
lines.append(f" ✅ X: {len(report.x)} posts")
x_line = f" ✅ X: {len(report.x)} posts"
if report.resolved_x_handle:
x_line += f" (via @{report.resolved_x_handle} + keyword search)"
lines.append(x_line)
elif report.mode in ("both", "x-only", "all", "x-web"):
lines.append(" ⚠️ X: 0 posts found")
else:
+5
View File
@@ -288,6 +288,8 @@ class Report:
web_error: Optional[str] = None
youtube_error: Optional[str] = None
hackernews_error: Optional[str] = None
# Handle resolution
resolved_x_handle: Optional[str] = None
# Cache info
from_cache: bool = False
cache_age_hours: Optional[float] = None
@@ -312,6 +314,8 @@ class Report:
'prompt_pack': self.prompt_pack,
'context_snippet_md': self.context_snippet_md,
}
if self.resolved_x_handle:
d['resolved_x_handle'] = self.resolved_x_handle
if self.reddit_error:
d['reddit_error'] = self.reddit_error
if self.x_error:
@@ -472,6 +476,7 @@ class Report:
web_error=data.get('web_error'),
youtube_error=data.get('youtube_error'),
hackernews_error=data.get('hackernews_error'),
resolved_x_handle=data.get('resolved_x_handle'),
from_cache=data.get('from_cache', False),
cache_age_hours=data.get('cache_age_hours'),
)