feat(x): X search via Grok CLI plus first-party evidence fixes (#991)

Grok CLI X search with no X credential, first-party evidence fixes, skip-floor when unidentified, and thin-retry handle protection.
This commit is contained in:
Matt Van Horn
2026-08-14 00:55:57 -07:00
committed by GitHub
parent ddc761d2c2
commit 698bfd0c3d
36 changed files with 3573 additions and 60 deletions
+4 -2
View File
@@ -150,7 +150,7 @@ python3 skills/last30days/scripts/last30days.py "MCP servers" \
| Techmeme | `techmeme-pp-cli` on PATH (auto-installed via `... install techmeme --cli-only`) | always on if `techmeme-pp-cli` on PATH; searches Techmeme's live archive and keeps only headlines dated within the research window (undated headlines flow through as low-confidence) | yes (free, keyless) |
| Trustpilot | `trustpilot-pp-cli` on PATH (NOT auto-installed; install on demand via `npx -y @mvanhorn/printing-press-library@0.1.16 install trustpilot --cli-only`) + (`INCLUDE_SOURCES` contains `trustpilot` **or** an explicit `--trustpilot-domain` / plan-level `trustpilot_domain`) | **opt-in, off by default**; `--trustpilot-domain=<domain>` (and per-entity `trustpilot_domain` in `--competitors-plan`) auto-activates the source for that run and bypasses the brand-shape gate. Persist with `INCLUDE_SOURCES=trustpilot` to skip per-run auto-enable. `EXCLUDE_SOURCES=trustpilot` still wins. Bare company names auto-resolve to the review-page domain via the CLI's search only when the source is already active. The session warms once before the search fan-out; a stale session does a ~10s headless-Chrome WAF-cookie harvest (set `LAST30DAYS_TRUSTPILOT_NO_BROWSER=1` to disable in cron/CI) | yes (no API key; cookie-replay after the one-time harvest) |
| Amazon | `brightdata` CLI on PATH **and logged in** (NOT auto-installed: `npm i -g @brightdata/cli` then `brightdata login`) + (`INCLUDE_SOURCES` contains `amazon` **or** `--search` includes `amazon`) | product records with live rating, rating count, and price, plus a capped sample of recent written reviews woven as buyer voice; the emoji footer shows each product's all-time-vs-last-30-days drift | **opt-in, off by default**. Free tier is 5,000 requests/month (~$7.50 equivalent); a typical run spends 4 (1 product search + up to 3 review pulls) regardless of how many reviews come back, since billing is per request. Past the free tier it bills the account balance at $1.50 per 1,000 records (~$0.32 for a default run). `--amazon-query=<keyword>` sets the product keyword when it differs from the topic; `LAST30DAYS_AMAZON_DOMAIN` selects a non-US marketplace. `EXCLUDE_SOURCES=amazon` wins. Never auto-fires: the model requests it per run or the user enables it durably |
| X / Twitter | one of: `AUTH_TOKEN` + `CT0` (browser cookies, Bird CLI), `XAI_API_KEY`, `XQUIK_API_KEY`, `SCRAPECREATORS_API_KEY`, or `FROM_BROWSER` (cookie-jar auth) | X items in results | cookie-jar / Bird = free; Xquik / xAI / ScrapeCreators = key-based |
| X / Twitter | one of: a signed-in `grok` CLI (no X credential), `AUTH_TOKEN` + `CT0` (browser cookies, Bird CLI), `XAI_API_KEY`, `XQUIK_API_KEY`, `SCRAPECREATORS_API_KEY`, or `FROM_BROWSER` (cookie-jar auth) | X items in results | grok = no X credential, draws on your Grok plan; cookie-jar / Bird = free; Xquik / xAI / ScrapeCreators = key-based |
| TikTok | `SCRAPECREATORS_API_KEY` + `INCLUDE_SOURCES` contains `tiktok` | TikTok items | 10K free calls |
| Instagram | `SCRAPECREATORS_API_KEY` + `INCLUDE_SOURCES` contains `instagram` | Instagram Reels | 10K free calls; raise `LAST30DAYS_TRANSCRIPT_TIMEOUT` (default 30s) if SC is slow on your network |
| Threads | `SCRAPECREATORS_API_KEY` + `INCLUDE_SOURCES` contains `threads` | Threads items | 10K free calls |
@@ -167,6 +167,8 @@ python3 skills/last30days/scripts/last30days.py "MCP servers" \
**YouTube transcript tuning.** `LAST30DAYS_YT_SUB_LANGS` controls the comma-separated caption-language priority passed to yt-dlp and defaults to `en,es,pt`. When `SCRAPECREATORS_API_KEY` is available, yt-dlp uses one fast attempt before the paid fallback; set `LAST30DAYS_YT_TRANSCRIPT_FAST_TIMEOUT` to the number of seconds allowed for that attempt when a throttled host needs longer than the 12-second default. A VTT completed before the timeout is reused rather than discarded. `LAST30DAYS_YT_SEARCH_TIMEOUT` sets the per-search yt-dlp deadline (default 120s). Comparison-mode fan-out also caps concurrent yt-dlp processes process-wide and caches identical searches within a run so redundant `ytsearch` calls do not self-throttle the same IP.
**X with no X credential (Grok CLI).** Install the Grok CLI (`curl -fsSL https://x.ai/cli/install.sh | bash`) and run `grok login`, and X works with no X account, no browser cookies, and no `XAI_API_KEY`. It sits ahead of the cookie path in the backend chain; pin `LAST30DAYS_X_BACKEND=bird` to prefer cookies. It is not "free" in the way the cookie path is: calls draw on your Grok plan, and depth costs several calls per run because the underlying tool caps each search at 10 posts. Results are validated before use — every returned post's ID is decoded to confirm it falls inside the requested date range, because the retrieval is performed by a language model and can otherwise return confident, well-formed posts that were never searched for.
**X on cookie-less hosts.** Bird (the free X source) scrapes X using your logged-in browser cookies (`AUTH_TOKEN`/`CT0`), which agent hosts like OpenClaw, CI, or headless runs often can't supply — and scraping carries some account risk. On those, set `XQUIK_API_KEY` (or `XAI_API_KEY`) for full, ranked X coverage from a single API key: the same engagement-based ranking, first-party authorship, and handle (from/mentions) lanes the native X source gets. `--diagnose` reports whether the key is working (and flags an unpaid key).
**Example `.env` skeleton** (placeholders only - replace with your own values):
@@ -419,7 +421,7 @@ Every live run writes its JSON result to `~/.config/last30days/doctor-cache.json
| --- | --- |
| `LAST30DAYS_DOCTOR_TTL` | Freshness window for `doctor --cached`, in **seconds**. Defaults to `900` (15 minutes). `0` makes every `--cached` call run live. |
| `LAST30DAYS_DOCTOR_PROBE_TIMEOUT` | Per-source deadline (**seconds**) for `doctor --probe` live checks. Defaults to `10`. Caps each concurrent probe so a slow source cannot hang the command. |
| `LAST30DAYS_X_BACKEND` | Pins the X backend (`xai` / `bird` / `xurl` / `xquik`); doctor renders the pin and predicts "will use" accordingly. |
| `LAST30DAYS_X_BACKEND` | Pins the X backend (`xai` / `grok` / `bird` / `xurl` / `xquik`); doctor renders the pin and predicts "will use" accordingly. Pin `bird` to keep the cookie path when a signed-in `grok` CLI is present, since `grok` otherwise wins by default. |
| `LAST30DAYS_REDDIT_BACKEND` | `scrapecreators` makes ScrapeCreators the primary Reddit backend; doctor renders Reddit's conditional routing with the pin applied. |
| `LAST30DAYS_REDDIT_SC_MIN_ITEMS` | Integer thinness floor for ScrapeCreators Reddit **search** backfill. Default `0` = empty-only (free path keeps any non-empty result; no credit spend). Set above `0` to backfill when free yield is below that count; merged results dedupe by post id. Requires `SCRAPECREATORS_API_KEY`. Ignored when `LAST30DAYS_REDDIT_BACKEND=scrapecreators` (SC is already primary). |
+1
View File
@@ -0,0 +1 @@
X search now works with no X credential at all. Install the Grok CLI (`curl -fsSL https://x.ai/cli/install.sh | bash`, then `grok login`) and last30days reaches X with no X account, no browser cookies, and no `XAI_API_KEY` — on any host, including Claude Code, Codex, Cursor and GrokBot. It sits ahead of the browser-cookie path by default; pin `LAST30DAYS_X_BACKEND=bird` to keep cookies. Covers three lanes for a person or company topic: posts by the subject, posts @-mentioning them, and posts naming them in plain text (which is most of the discussion, and which a mention-only search misses).
@@ -0,0 +1 @@
X runs on a person or company no longer discard the subject's own posts. A post almost never contains its own author's name, so lexical relevance scored it at zero and the retrieval floor pruned it — a run for "Peter Steinberger steipete" fetched 8 posts by him and reported none of them. Fixed across the chain: planner scaffolding words no longer count as topic signal, posts by a handle the run is searching are exempt from the floor, auto-discovered handles now reach the first-party protections (previously only `--x-handle` did), quoted proper-noun phrases survive into the provider query instead of degrading into a token conjunction, and the subject of the topic gets a higher per-author cap than incidental accounts. When no real handle can be identified at all, the X floor is skipped rather than pruning against lexical name tokens. The thin-source retry path defers the X floor the same way Phase 1 does, so a subject-authored post recovered on retry is not discarded before handle resolution.
+19 -4
View File
@@ -577,6 +577,8 @@ Options:
**If the user picks Auto setup:**
**Check for a Grok path before asking for cookies.** Run `command -v grok`. If it resolves, X can be unlocked with no X credential at all, so lead with that instead of a cookie read: tell the user `grok login` is enough and only offer the cookie options if they decline. Do not call it free — it needs a Grok plan.
Get cookie consent first. Check if `BROWSER_CONSENT=true` already exists in `~/.config/last30days/.env`; if so, skip the consent prompt and run `setup --allow-browser-cookies` directly. Otherwise **call AskUserQuestion:**
Question: "Auto setup installs the free CLIs either way - yt-dlp (YouTube), Digg, arXiv, and Techmeme. The only thing that needs your OK is reading your browser's x.com cookies to authenticate X/Twitter search: I check Chrome first (a one-time macOS Keychain prompt may appear; click Always Allow), then Firefox and Safari. Cookies are read live, never saved to disk. Include X?"
Options (give each option the description shown):
@@ -584,6 +586,8 @@ Options (give each option the description shown):
- "Skip X - just the CLIs" - description: "No cookie reads. Still installs yt-dlp (YouTube), Digg, arXiv, and Techmeme." Run `FROM_BROWSER=off "${LAST30DAYS_PYTHON:-python3}" skills/last30days/scripts/last30days.py setup`.
- "xAI API key for X instead" - description: "Use an api.x.ai key for X search (no cookie read), plus install yt-dlp (YouTube), Digg, arXiv, and Techmeme." Ask them to paste it, write `XAI_API_KEY` to `.env`, then run `FROM_BROWSER=off "${LAST30DAYS_PYTHON:-python3}" skills/last30days/scripts/last30days.py setup`.
When `command -v grok` resolved, replace the "xAI API key for X instead" option with: "Sign in to Grok instead" - description: "X search with no X account, cookies, or API key. Run `grok login` once." Nothing is written to `.env`; still install the free CLIs with `FROM_BROWSER=off "${LAST30DAYS_PYTHON:-python3}" skills/last30days/scripts/last30days.py setup`.
The consented `setup --allow-browser-cookies` run extracts cookies (Chrome/Chromium family first via the Keychain with no Full Disk Access, then Firefox and Safari as fallbacks; the winning browser is pinned for future runs only when it is Firefox or Safari, so Chrome never re-triggers the Keychain prompt on later runs) and best-effort installs yt-dlp (YouTube), the free keyless Digg CLI (`digg-pp-cli` via `@mvanhorn/printing-press-library install digg --cli-only`; Digg activates only when the binary is on the **agent subprocess PATH**, typically `$HOME/.local/bin`; setup reports honestly if installed off-PATH; recommend-only if `npx` is unavailable), plus the free keyless arXiv and Techmeme CLIs. Show the user what was found and installed - including whether Digg landed on PATH (active) or off-PATH (installed but not yet active).
**macOS Full Disk Access remediation (Safari fallback only).** Chrome and Firefox need no Full Disk Access; only the Safari fallback does. After the `setup` run, inspect its stderr. If it contains `Permission denied reading Cookies.binarycookies` and the platform is macOS, the OS blocked the Safari read - surface the fix instead of swallowing it: `macOS blocked the Safari cookie read. If your x.com login is in Chrome, you don't need this. To use Safari: System Settings > Privacy & Security > Full Disk Access > enable your terminal (or the Claude app), then I can retry.` Offer ONE retry of the `setup` command. If the user skips, continue.
@@ -675,6 +679,7 @@ Shown when a Claude Code user picks "Manual setup", or for anyone who wants to c
The magic of /last30days is Reddit comments + X posts together - and both are free. Add these to `~/.config/last30days/.env`:
**X/Twitter (pick one - the most important source):**
- **Grok CLI (no X credential):** install with `curl -fsSL https://x.ai/cli/install.sh | bash`, then `grok login`. No X account, no cookies, no API key. Needs a Grok plan; calls draw on it.
- `FROM_BROWSER=auto` - free. Reads your x.com login cookies live at search time (Firefox/Safari, never saved to disk).
- `XAI_API_KEY=xxx` - no browser access needed. Get a key at api.x.ai. Best for servers.
- `XQUIK_API_KEY=xxx` - keyless-style X via Xquik.
@@ -2000,16 +2005,26 @@ Headlines should be specific and newsy ("BULLY dropped and it's dominating", "Eu
If the research output contains a `**🔍 Research Coverage:**` block, render it verbatim right before the stats block. This tells the user which core sources are missing and how to unlock them. Do NOT render this block if it is absent from the output (100% coverage = no nudge).
**Just-in-time X unlock:** If X returned 0 results because no X auth is configured (no AUTH_TOKEN/CT0, no XAI_API_KEY, no FROM_BROWSER), offer to set it up right there:
**Just-in-time X unlock:** If X returned 0 results because no X auth is configured (no AUTH_TOKEN/CT0, no XAI_API_KEY, no FROM_BROWSER), offer to set it up right there.
**Call AskUserQuestion:**
Question: "X/Twitter wasn't searched. Want to unlock it?"
Options:
**First check whether a Grok path is already present.** Run `command -v grok`. If it resolves, the user needs no X credential at all — say so and offer `grok login` as the first option, because it is one command and unlocks X with no X account, no cookies, and no API key.
**Call AskUserQuestion.** Question: "X/Twitter wasn't searched. Want to unlock it?"
Options when `grok` IS on PATH (or the user has said they have a Grok account):
- "Sign in to Grok (no X account needed)" - Have them run `grok login`; nothing is written to .env and no X credential is involved
- "Scan my browser cookies (free)" - Get consent, run cookie scan, write BROWSER_CONSENT=true + FROM_BROWSER=auto to .env
- "I have AUTH_TOKEN and CT0 from my browser" - Ask them to paste each value, then write AUTH_TOKEN=<value>\nCT0=<value> to .env
- "Skip for now"
Options when `grok` is NOT on PATH — offer today's options unchanged, and mention the Grok path only as a closing one-liner rather than a menu entry. A user with no Grok account should not have their actionable choices pushed down the list by one they cannot take:
- "Scan my browser cookies (free)" - Get consent, run cookie scan, write BROWSER_CONSENT=true + FROM_BROWSER=auto to .env
- "I have AUTH_TOKEN and CT0 from my browser" - Ask them to paste each value, then write AUTH_TOKEN=<value>\nCT0=<value> to .env
- "I have an xAI API key" - Ask them to paste it, write XAI_API_KEY to .env
- "Skip for now"
After the modal, if `grok` was absent, add one line: "If you have a Grok account, installing the Grok CLI (`curl -fsSL https://x.ai/cli/install.sh | bash`) unlocks X with no X credential at all." Do not describe the Grok path as free — it needs a Grok plan.
**THEN - Engine footer pass-through (right before invitation):**
**The research output ENDS with a deterministic footer block bracketed by `---` lines, starting with `✅ All agents reported back!` and ending with `📎 Raw results saved to {resolved LAST30DAYS_MEMORY_DIR}/<slug>-raw.md`. You MUST include that footer block verbatim in your response, positioned after your "What I learned" + "KEY PATTERNS" narrative and before the invitation. Do not recompute the stats. Do not reformat the tree. Do not paraphrase. Do not skip it. Do not add your own source lines. Copy the exact bytes.**
+58
View File
@@ -2758,6 +2758,40 @@ def _run_library_search(
return 0
def _looks_like_entity_topic(topic: str) -> bool:
"""Whether a topic names a person, company, or product rather than a theme.
Keys on brevity, not capitalization. People type lowercase: "bentgo",
"peter steinberger" and "getenergy.com" are entity searches every bit as
much as their title-cased forms, and requiring a capital meant the most
common real-world spelling never resolved a handle.
A short topic is an entity search; a longer one is a theme. "Peter
Steinberger", "bentgo" and "getenergy.com" qualify; "best AI coding tools
2026" and "how to build agents that scale" do not. Question-shaped topics
are themes regardless of length.
Used only to decide whether resolving an X handle is worth one web search,
so a false negative costs the old behavior and a false positive costs a
single search.
"""
text = (topic or "").strip()
if not text or text.endswith("?"):
return False
words = [w for w in re.findall(r"[A-Za-z0-9_.@'-]+", text) if w]
if not words or len(words) > 4:
return False
if any(w.startswith("@") for w in words):
return True
# A theme reads as a phrase built from common words; an entity does not.
common = {
"best", "top", "how", "why", "what", "when", "vs", "versus", "guide",
"tips", "review", "reviews", "news", "latest", "update", "updates",
"trends", "tools", "and", "or", "for", "the", "with", "about",
}
return not any(w.lower() in common for w in words)
def main() -> int:
parser = build_parser()
# Use parse_known_args so setup sub-flags (--device-auth, --github,
@@ -3236,6 +3270,28 @@ def _main(
# without WebSearch (OpenClaw, Codex, raw CLI).
repos_from_auto_resolve = False
trustpilot_domain_is_hint = False
# Resolve automatically for entity-shaped topics even without the flag.
# A person or company topic whose handle the user did not supply is the
# case where first-party evidence is hardest to protect: the handle is
# absent from the topic and may never appear in retrieved mentions, so
# nothing downstream can identify the subject's own posts. One web
# search closes that. If it returns nothing, pipeline.run skips the X
# relevance floor entirely — a noisier report beats losing evidence.
# Skipped when a handle was already supplied, when an external plan
# owns resolution, or in mock runs.
if (
not args.auto_resolve
and not external_plan
and not args.x_handle
and not args.mock
and _looks_like_entity_topic(topic)
):
args.auto_resolve = True
sys.stderr.write(
"[AutoResolve] entity-shaped topic with no --x-handle; "
"resolving the subject's handle so its own posts are not pruned\n"
)
if args.auto_resolve and not external_plan:
from lib import resolve
resolution = resolve.auto_resolve(topic, config)
@@ -3245,6 +3301,8 @@ def _main(
if resolution.get("x_handle") and not args.x_handle:
args.x_handle = resolution["x_handle"]
sys.stderr.write(f"[AutoResolve] X handle: @{args.x_handle}\n")
# Empty x_handle is intentional: do not invent a lexical stand-in.
# pipeline.run treats an unidentified subject as "skip the X floor".
if resolution.get("github_user") and not args.github_user:
args.github_user = resolution["github_user"]
sys.stderr.write(f"[AutoResolve] GitHub user: @{args.github_user}\n")
+68
View File
@@ -251,6 +251,72 @@ def _probe_bird(config: Dict[str, Any]) -> BackendFinding:
)
def _probe_grok(config: Dict[str, Any]) -> BackendFinding:
"""grok CLI = keyless X. LOCAL-ONLY probe, like _probe_xurl.
Deliberately does NOT call ``health.probe_dependency``: that helper runs
``subprocess.run([name, "--version"])``, and the whole-doctor-path test
patches ``subprocess.run`` to raise.
Consequence to be honest about: a grok binary that resolves on PATH but
will not execute (the stale-shim class) reports OK here and fails only when
a real run shells out. ``grok_x.is_available`` does not close that gap
either -- it is also filesystem-only. ``health.probe_dependency("grok")``
is the executing probe, and it runs in doctor's CLI-health block rather
than on this no-subprocess path.
"""
from . import grok_x
requires = "grok CLI installed + signed in (no X credential)"
if which("grok") is None:
# Installed-but-off-PATH is a distinct outcome with a distinct fix:
# telling the user to install again fixes nothing. Confirmed real --
# the official installer places the binary at ~/.grok/bin/grok, which
# an agent subprocess PATH often omits.
off_path = health._off_path_binary("grok")
if off_path is not None:
return BackendFinding(
name="grok",
status=health.MISSING,
requires=requires,
detail=f"grok is installed at {off_path} but that directory is not on this process's PATH",
prescription=f'add {off_path.parent} to PATH (e.g. export PATH="{off_path.parent}:$PATH")',
)
return BackendFinding(
name="grok",
status=health.MISSING,
requires=requires,
detail="grok CLI not found on PATH",
prescription=(
"install the Grok CLI: curl -fsSL https://x.ai/cli/install.sh | bash, "
"then run `grok login`"
),
)
store_status, store_detail = grok_x.stored_auth_status()
if store_status == grok_x.AUTH_OK:
return BackendFinding(
name="grok",
status=health.OK,
requires=requires,
detail=f"{store_detail} (not live-verified until a run)",
)
if store_status == grok_x.AUTH_ERROR:
return BackendFinding(
name="grok",
status=health.ERROR,
requires=requires,
detail=store_detail,
prescription="grok login",
)
return BackendFinding(
name="grok",
status=health.MISSING,
requires=requires,
detail="grok CLI installed but not signed in",
prescription="grok login",
)
def _probe_xurl(config: Dict[str, Any]) -> BackendFinding:
"""xurl = official X API v2 CLI (OAuth2). Free lane; LOCAL-ONLY probe.
@@ -347,6 +413,7 @@ def _probe_reddit_public(config: Dict[str, Any]) -> BackendFinding:
_X_PROBES: Dict[str, Callable[[Dict[str, Any]], BackendFinding]] = {
"xai": _key_probe("xai", "XAI_API_KEY", "XAI_API_KEY (xAI/Grok live search)"),
"grok": _probe_grok,
"bird": _probe_bird,
"xurl": _probe_xurl,
"xquik": _key_probe("xquik", "XQUIK_API_KEY", "XQUIK_API_KEY (xquik.com)"),
@@ -382,6 +449,7 @@ DESCRIPTORS: Dict[str, ChainDescriptor] = {
name=name,
requires={
"xai": "XAI_API_KEY (xAI/Grok live search)",
"grok": "grok CLI installed + signed in (no X credential)",
"bird": "X browser cookies (AUTH_TOKEN/CT0) + node",
"xurl": "xurl CLI installed + OAuth2 login",
"xquik": "XQUIK_API_KEY (xquik.com)",
+41 -3
View File
@@ -112,7 +112,12 @@ def _extract_core_subject(topic: str) -> str:
def _plain_query_tokens(text: str) -> list[str]:
"""Return lexical tokens without Bird query grouping syntax."""
"""Return lexical tokens without Bird query grouping syntax.
Strips phrase quotes as well as grouping characters. Used where a flat
token list is wanted; use ``build_topic_query`` for the provider query,
which preserves quoted phrases.
"""
separators = str.maketrans({char: " " for char in '\"“”()[]{}'})
return [
clean
@@ -121,6 +126,38 @@ def _plain_query_tokens(text: str) -> list[str]:
]
# Bird/X grouping syntax that carries no lexical meaning. Double quotes are
# deliberately absent: X advanced search treats "..." as a phrase match, which
# is exactly what the planner intended when it quoted a proper noun.
_GROUPING_CHARS = "“”()[]{}"
def build_topic_query(topic: str, from_date: str) -> str:
"""Build the X topic query, preserving quoted proper-noun phrases.
Previously the topic went through ``_plain_query_tokens``, which stripped
the quotes the planner had added, so an intended phrase match for
'"Peter Steinberger"' degraded into `peter AND steinberger` -- narrower and
noisier at once. X supports phrase queries natively, so the quotes are
passed through.
"""
separators = str.maketrans({char: " " for char in _GROUPING_CHARS})
cleaned = topic.translate(separators)
# An unbalanced quote is worse than no quote: X reads the orphan as an
# unterminated phrase and matches nothing. Upstream trimming (core-subject
# extraction, retry shortening) can cut a topic mid-phrase, so verify the
# quotes pair up and fall back to bare tokens when they do not.
if cleaned.count('"') % 2:
cleaned = cleaned.replace('"', " ")
tokens = [
clean
for token in cleaned.split()
if (clean := token.strip("'‘’"))
]
core = " ".join(tokens).strip()
return f"{core} since:{from_date}" if core else f"since:{from_date}"
def is_bird_installed() -> bool:
"""Check if vendored Bird search module is available.
@@ -371,9 +408,10 @@ def search_x(
timeout = 30 if depth == "quick" else 45 if depth == "default" else 60
# Extract core subject - X search is literal, not semantic
core_words = _plain_query_tokens(_extract_core_subject(topic))
core_subject = _extract_core_subject(topic)
core_words = _plain_query_tokens(core_subject)
core_topic = " ".join(core_words)
query = f"{core_topic} since:{from_date}"
query = build_topic_query(core_subject, from_date)
_log(f"Searching: {query}")
response = _run_bird_search(query, count, timeout)
+14
View File
@@ -417,6 +417,20 @@ def _reddit_record(config):
def _x_record(config):
record = _chained_record("x", config)
# A usable grok CLI covers X with no X credential at all, so it must not be
# reported as unconfigured. Checked before the browser-auth override below
# because it outranks bird in the chain and, unlike a cookie session, its
# availability is verifiable locally rather than "not verified until a run".
from . import grok_x
if record["status"] == "unconfigured" and grok_x.has_stored_auth():
record["status"] = health.OK
record["tier"] = TIER_BY_STATUS[health.OK]
record["note"] = (
"will use: grok (grok CLI, signed in; no X account, cookies, or "
"API key needed)"
)
record["fix"] = ""
return record
# Diagnose/doctor load config in plan_only mode, so browser cookies are not
# extracted and every X backend reads as statically missing -> unconfigured.
# But if bird is installed and FROM_BROWSER will authenticate X at run time,
+20 -4
View File
@@ -793,7 +793,7 @@ def get_reddit_source(config: dict[str, Any]) -> str | None:
# bird — X GraphQL scrape via the user's browser cookies (AUTH_TOKEN/CT0)
# xurl — official X API v2 (xurl CLI, OAuth2)
# xquik — key-based REST X search (XQUIK_API_KEY); keyless of browser cookies
_X_BACKEND_ORDER = ("xai", "bird", "xurl", "xquik")
_X_BACKEND_ORDER = ("xai", "grok", "bird", "xurl", "xquik")
# Public routing definitions for the doctor/backend-descriptor layer
# (lib/backends.py). These are aliases for knowledge this module already
@@ -813,6 +813,12 @@ def _x_backend_available(
) -> bool:
if backend == 'xai':
return bool(config.get('XAI_API_KEY'))
if backend == 'grok':
# Keyless relative to X: needs only an installed, signed-in grok CLI.
# Both surfaces are filesystem-only (PATH lookup + credential store),
# so local_only needs no separate branch.
from . import grok_x
return grok_x.has_stored_auth()
if backend == 'bird':
from . import bird_x
return has_bird_creds and bird_x.is_bird_installed()
@@ -1244,13 +1250,22 @@ def get_x_source_status(config: dict[str, Any], probe: bool = False) -> dict[str
from . import xurl_x as _xurl_x
xurl_available = _xurl_x.is_available() if probe else _xurl_x.has_stored_auth()
# Grok availability is filesystem-only on both paths (PATH lookup plus the
# credential store), so it is safe to compute here regardless of `probe`.
from . import grok_x as _grok_x
grok_available = _grok_x.has_stored_auth()
# Determine active source. bird (browser cookies) and xAI win when present;
# when neither is available, xquik is the active X source. A probe that
# clearly failed (False) means xquik is not actually usable.
if bird_status["authenticated"]:
source = 'bird'
elif xai_available:
if xai_available:
source = 'xai'
elif grok_available:
# Ahead of bird per the chain order: grok needs no X credential at all,
# while a cookie session is one expiry away from silently degrading.
source = 'grok'
elif bird_status["authenticated"]:
source = 'bird'
else:
if xurl_available:
source = 'xurl'
@@ -1265,6 +1280,7 @@ def get_x_source_status(config: dict[str, Any], probe: bool = False) -> dict[str
"bird_authenticated": bird_status["authenticated"],
"bird_username": bird_status["username"],
"xai_available": xai_available,
"grok_available": grok_available,
"xurl_available": xurl_available,
"xquik_available": xquik_available,
"xquik_working": xquik_working,
+33 -3
View File
@@ -2,6 +2,8 @@
from __future__ import annotations
from collections.abc import Iterable
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
from . import schema
@@ -50,6 +52,15 @@ _DIVERSITY_RELEVANCE_THRESHOLD = 0.25
# Per-author cap: no single author/handle should dominate the pool.
_MAX_ITEMS_PER_AUTHOR = 3
# Raised cap for the subject of the topic (a handle in the run's
# resolved_handles). On a person or company topic the subject is what the user
# asked about, so the flat cap discards exactly the evidence the run worked
# hardest to retrieve -- the measured 'Peter Steinberger steipete' baseline
# recovered 8 subject-authored posts and would have kept 3. Still bounded: a
# prolific subject must not crowd out commentary about them, which is the other
# half of the answer a user wants.
_MAX_ITEMS_PER_FIRST_PARTY_AUTHOR = 8
def _extract_author(candidate: schema.Candidate) -> str | None:
"""Return a normalized author key from a candidate's source items."""
@@ -62,12 +73,23 @@ def _extract_author(candidate: schema.Candidate) -> str | None:
def _apply_per_author_cap(
candidates: list[schema.Candidate],
max_per_author: int = _MAX_ITEMS_PER_AUTHOR,
first_party_handles: Iterable[str] | None = None,
max_per_first_party_author: int = _MAX_ITEMS_PER_FIRST_PARTY_AUTHOR,
) -> list[schema.Candidate]:
"""Keep at most *max_per_author* items from any single author.
Authors named in *first_party_handles* -- the subject of the topic -- get
the higher *max_per_first_party_author* allowance instead, because their
own posts are the point of the query rather than one voice among many.
Candidates are assumed to already be sorted by quality (rrf_score etc.),
so the first N encountered per author are the best ones.
"""
first_party = {
h.strip().lstrip("@").lower()
for h in (first_party_handles or ())
if h and h.strip()
}
author_counts: dict[str, int] = {}
result: list[schema.Candidate] = []
for c in candidates:
@@ -75,8 +97,13 @@ def _apply_per_author_cap(
if author is None:
result.append(c)
continue
limit = (
max_per_first_party_author
if author.strip().lstrip("@").lower() in first_party
else max_per_author
)
count = author_counts.get(author, 0)
if count < max_per_author:
if count < limit:
result.append(c)
author_counts[author] = count + 1
return result
@@ -125,13 +152,16 @@ def weighted_rrf(
pool_limit: int,
range_from: str | None = None,
range_to: str | None = None,
first_party_handles: Iterable[str] | None = None,
) -> list[schema.Candidate]:
"""Fuse ranked lists into a single candidate pool.
When ``range_from`` and ``range_to`` are provided, they are stored in each
candidate's metadata so ``candidate_out_of_window`` can compare the actual
date against the run window (instead of relying solely on adapter-provided
``date_confidence``).
``date_confidence``). ``first_party_handles`` raises the per-author cap
for the topic's subject so their own posts are not flattened to the
incidental-account allowance.
"""
subqueries = {subquery.label: subquery for subquery in plan.subqueries}
candidates: dict[str, schema.Candidate] = {}
@@ -227,5 +257,5 @@ def weighted_rrf(
candidate.snippet = item.snippet
fused = sorted(candidates.values(), key=_candidate_sort_key)
fused = _apply_per_author_cap(fused)
fused = _apply_per_author_cap(fused, first_party_handles=first_party_handles)
return _diversify_pool(fused, pool_limit)
+762
View File
@@ -0,0 +1,762 @@
"""X (Twitter) search via the Grok CLI — no X credential of any kind.
The `grok` CLI (https://x.ai/cli) exposes X search tools natively
(`x_keyword_search`, `x_semantic_search`, `x_thread_fetch`, `x_user_search`).
Reaching X through it needs no X account, no browser cookies, and no
`XAI_API_KEY` — only an installed and signed-in `grok`.
Install: curl -fsSL https://x.ai/cli/install.sh | bash (or npm i -g @xai-official/grok)
Auth: grok login
Two invocation constraints, both measured, both load-bearing:
* **Never pass `--json-schema`.** Constrained decoding competes with tool use:
the search silently does not run and the model fills the schema's required
fields from training data instead. Measured with an interleaved A/B
controlling for time: plain output returned verified in-window posts on 4 of
4 calls, `--json-schema` on 1 of 4.
* **Never pass `--tools`.** Two runs produced no output in 7 minutes and were
killed; the identical prompts without it completed normally.
Because retrieval is performed by a language model rather than an API client,
its output can be *confidently wrong* in a way no other backend's can. Every
returned post is therefore validated against the requested window via its
snowflake timestamp before it is allowed into the item flow — see
`_validate_items`. Author matching and schema shape are not sufficient: a
fabricated post carries a plausible handle and a numeric id by construction.
"""
import os
import re
import shutil
import subprocess
import tempfile
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from . import log
from .relevance import token_overlap_relevance as _compute_relevance
def _log(msg: str) -> None:
log.source_log("Grok", msg, tty_only=False)
# Posts requested per call. The tool caps `limit` at 10, so depth is achieved
# by fanning out across queries rather than by raising a single call's limit.
_MAX_LIMIT_PER_CALL = 10
# Upper bound on calls per topic search. Each call is an LLM subprocess of
# roughly 15-45s, so depth must not translate into unbounded wall time.
_MAX_FANOUT_CALLS = 4
# Wall-clock ceiling for ALL grok Phase 2 lanes combined. Without it, three
# lanes over three handles is up to 14 sequential LLM subprocess calls bounded
# only by per-call timeouts -- tens of minutes of foreground time for a source
# that now runs by default. Lanes stop issuing queries once this passes and
# return whatever they have.
LANE_BUDGET_SECONDS = 150.0
# Below this, a call cannot plausibly complete (measured calls run 15-45s), so
# the budget is spent rather than overrun. Skipping is strictly better than
# starting a call guaranteed to be killed mid-flight.
_MIN_USEFUL_CALL_SECONDS = 15
def _fanout_queries(topic: str, from_date: str, to_date: str, calls: int) -> List[str]:
"""Distinct query formulations for one topic, widest signal first.
Each returns at most 10 posts, and the formulations surface different
sets -- Top vs Latest ordering, and an engagement-floored variant -- so
fanning out adds coverage rather than repeating one result set.
"""
window = f"since:{from_date} until:{to_date}"
variants = [
f"{topic} {window}",
f"{topic} {window} min_faves:5",
f'"{topic}" {window}' if " " in topic else f"{topic} {window} filter:links",
f"{topic} {window} -filter:replies",
]
return variants[:calls]
DEPTH_CONFIG = {
"quick": 10,
"default": 30,
"deep": 60,
}
# Wall-clock ceiling for one `grok` invocation. A run that blocks on an
# unexpected interactive prompt would otherwise hang indefinitely, and a
# non-daemon worker can outlive a wall-clock budget.
_TIMEOUT_SECONDS = {"quick": 120, "default": 240, "deep": 360}
# Twitter/X snowflake epoch (2010-11-04T01:42:54.657Z) in milliseconds.
_SNOWFLAKE_EPOCH_MS = 1288834974657
_AUTH_STORE = Path.home() / ".grok" / "auth.json"
# Substrings that indicate stored credentials. Deliberately format-agnostic:
# the observed store is a JSON object keyed by issuer and principal, but the
# shape is the vendor's to change. Mirrors xurl_x's marker scan.
_TOKEN_STORE_MARKERS = ("refresh_token", "access_token", "auth_mode", '"key"')
AUTH_OK = "ok" # token store present with stored credentials
AUTH_MISSING = "missing" # no token store, or no credentials stored in it
AUTH_ERROR = "error" # token store exists but could not be read
_availability_cache: Optional[bool] = None
def clear_availability_cache() -> None:
"""Reset the memoized is_available() result (tests, or a re-check after login)."""
global _availability_cache
_availability_cache = None
def binary_path() -> Optional[str]:
"""Resolved `grok` path, or None when it is not on PATH.
PATH resolution is the gate, not file existence: a binary present on disk
but off the agent subprocess PATH is not installed as far as the engine is
concerned.
"""
return shutil.which("grok")
def token_store_path() -> Path:
return _AUTH_STORE
def stored_auth_status() -> Tuple[str, str]:
"""Local-only auth check: filesystem read, no subprocess, no network.
This is the doctor / --diagnose / --preflight surface. It must never spawn
a process: the whole-doctor-path test patches ``subprocess.run`` to raise,
and shelling out to `grok` here would fail it.
"""
path = token_store_path()
try:
if not path.exists():
return AUTH_MISSING, f"no Grok credential store at {path}"
raw = path.read_text(encoding="utf-8", errors="replace")
except OSError as exc:
return AUTH_ERROR, f"{type(exc).__name__}: {exc}"
if any(marker in raw for marker in _TOKEN_STORE_MARKERS):
# Deliberately reports the path only. Never echo store contents: the
# file holds an access key and refresh token, and doctor output is
# routinely pasted into issue reports.
return AUTH_OK, f"stored Grok credentials found in {path}"
return AUTH_MISSING, f"Grok credential store at {path} has no stored credentials"
def has_stored_auth() -> bool:
return binary_path() is not None and stored_auth_status()[0] == AUTH_OK
def is_available() -> bool:
"""Research-time availability. May spawn a subprocess; memoized per process."""
global _availability_cache
if _availability_cache is None:
_availability_cache = _is_available_uncached()
return _availability_cache
def _is_available_uncached() -> bool:
if binary_path() is None:
return False
return stored_auth_status()[0] == AUTH_OK
def _subprocess_env(home: str) -> Dict[str, str]:
"""Minimal environment for the `grok` child process, rooted at a throwaway HOME.
The child runs with tool permissions bypassed (non-interactivity requires
it) while its context is filled with retrieved X post text, which is
attacker-controlled. Stripping credential env vars is necessary but not
sufficient: this engine writes XAI_API_KEY / AUTH_TOKEN / CT0 /
SCRAPECREATORS_API_KEY to ``$HOME/.config/last30days/.env``, and ``~/.ssh``
and ``~/.aws`` sit alongside it. An empty cwd is not a boundary for a
filesystem-capable agent -- cwd constrains relative paths, not ``$HOME/...``
reads -- so the child gets its own HOME containing only the Grok credential
store it actually needs.
"""
keep = ("PATH", "LANG", "LC_ALL", "TMPDIR", "SystemRoot")
env = {k: os.environ[k] for k in keep if k in os.environ}
env.setdefault("PATH", os.defpath)
env["HOME"] = home
if os.name == "nt":
env["USERPROFILE"] = home
return env
def _stage_child_home(workdir: str) -> str:
"""Create the child's throwaway HOME holding only the credential file.
Copies ``auth.json`` alone, never the ``~/.grok`` tree: that directory is
~1.6 GB (marketplace cache, bundled runtime, session history), and copying
it per invocation made a single search take minutes. The child needs the
credential to authenticate and nothing else -- session history and caches
are state we specifically do not want a permission-bypassed child to read
or mutate.
Copied rather than symlinked so the child cannot follow a link back to the
real store, and copied rather than shared so it cannot rewrite the user's
credentials.
"""
home = os.path.join(workdir, "home")
store = token_store_path()
child_store_dir = os.path.join(home, store.parent.name)
os.makedirs(child_store_dir, mode=0o700, exist_ok=True)
try:
if store.is_file():
shutil.copyfile(store, os.path.join(child_store_dir, store.name))
os.chmod(os.path.join(child_store_dir, store.name), 0o600)
except OSError as exc:
_log(f"could not stage Grok credentials for the child: {exc}")
return home
def _decode_snowflake(post_id: str) -> Optional[datetime]:
"""Recover a post's creation time from its id, with no network call."""
try:
value = int(str(post_id).strip())
except (TypeError, ValueError):
return None
if value <= 0:
return None
try:
return datetime.fromtimestamp(
((value >> 22) + _SNOWFLAKE_EPOCH_MS) / 1000, tz=timezone.utc
)
except (OverflowError, OSError, ValueError):
return None
def _looks_generated(ids: List[str]) -> bool:
"""True when ids form a near-uniform arithmetic run.
Real ranked results are not evenly spaced in time. A fabricated set often
is, because the model interpolates a plausible-looking id sequence. Four
ids is the minimum used here: three gaps are needed before a near-uniform
step reads as generated rather than coincidental.
"""
numeric = []
for pid in ids:
try:
numeric.append(int(pid))
except (TypeError, ValueError):
return False
if len(numeric) < 4:
return False
numeric.sort()
gaps = [b - a for a, b in zip(numeric, numeric[1:])]
if any(g <= 0 for g in gaps):
return False
mean = sum(gaps) / len(gaps)
if mean <= 0:
return False
# Every gap within 5% of the mean is not something real timelines do.
return all(abs(g - mean) / mean < 0.05 for g in gaps)
# Why the most recent parse returned nothing. Lets _run_query distinguish a
# clean empty window (common, and not worth a second LLM call) from a suspect
# response (fabricated ids, a generated sequence, a self-reported
# non-execution), which is the only case retrying can actually fix.
_LAST_REJECTION = {"reason": ""}
_RETRYABLE_REJECTIONS = (
"failed provenance validation",
"near-uniform sequence",
"unparsable date window",
)
_PLACEHOLDER_HANDLES = {"unknown", "n/a", "none", "null", "example", "user", ""}
# X's real handle grammar. Model-reported handles are interpolated into post
# URLs and into the NEXT child prompt, so anything outside this charset is
# rejected rather than passed through: a poisoned post that steers the child
# into emitting a crafted handle line would otherwise reach a prompt slot it
# can close. entity_extract applies the same rule to @mentions.
_HANDLE_RE = re.compile(r"[A-Za-z0-9_]{1,15}")
def _clean_handle(value: str) -> str:
"""Return a grammar-valid handle, or '' when the value is not one."""
candidate = str(value or "").strip().lstrip("@")
return candidate if _HANDLE_RE.fullmatch(candidate) else ""
_NON_EXECUTION_MARKERS = (
"was not executed",
"not executed in this turn",
"unable to search",
"could not search",
"no tool call",
"tool not available",
)
def _validate_items(
items: List[Dict[str, Any]],
from_date: str,
to_date: str,
) -> Tuple[List[Dict[str, Any]], str]:
"""Drop anything that did not come from a real in-window post.
Returns (kept, reason). A non-empty reason means the response should be
treated as a non-execution to retry rather than as a thin result.
"""
if not items:
return [], "no items parsed"
try:
lo = datetime.strptime(from_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
hi = datetime.strptime(to_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
except (TypeError, ValueError):
# Fail closed. Skipping the window check would silently disable the
# module's central provenance guarantee for the whole response.
return [], "unparsable date window"
kept: List[Dict[str, Any]] = []
for item in items:
text = str(item.get("text") or "").lower()
if any(marker in text for marker in _NON_EXECUTION_MARKERS):
continue
handle = str(item.get("author_handle") or "").strip().lstrip("@").lower()
if handle in _PLACEHOLDER_HANDLES:
continue
created = _decode_snowflake(item.get("post_id"))
if created is None:
continue
if not (lo <= created <= hi.replace(hour=23, minute=59, second=59)):
continue
kept.append(item)
if not kept:
return [], "every item failed provenance validation (window/handle/id)"
if _looks_generated([str(i.get("post_id")) for i in kept]):
return [], "post ids form a near-uniform sequence (generated, not retrieved)"
return kept, ""
# --- prose parsing ---------------------------------------------------------
_FIELD_ALIASES = {
"id": "post_id",
"post id": "post_id",
"conversation id": "conversation_id",
"author": "author",
"handle": "author_handle",
"text": "text",
"content": "text",
"created_at": "created_at",
"timestamp": "created_at",
"likes": "likes",
"reposts": "reposts",
"retweets": "reposts",
"replies": "replies",
"quotes": "quotes",
"bookmarks": "bookmarks",
"views": "views",
}
_FIELD_LINE = re.compile(
r"^[\s\-*>]*\**\s*([A-Za-z][A-Za-z _]{1,20}?)\**\s*[:=]\s*(.+?)\s*$"
)
# Engagement counts arrive either literal ("1,462") or display-abbreviated
# ("39K", "1.2M"). Parsing only the leading digits turns 1.2M into 1, which
# does not merely lose precision -- it inverts ranking, placing a viral post
# below one with 500 literal likes.
_INT_RE = re.compile(r"(-?\d[\d,]*(?:\.\d+)?)\s*([KMB])?", re.I)
_SUFFIX_MULTIPLIER = {"k": 1_000, "m": 1_000_000, "b": 1_000_000_000}
def _as_int(value: str) -> Optional[int]:
match = _INT_RE.search(value or "")
if not match:
return None
number, suffix = match.group(1), match.group(2)
try:
parsed = float(number.replace(",", ""))
except ValueError:
return None
if suffix:
parsed *= _SUFFIX_MULTIPLIER[suffix.lower()]
return int(parsed)
def _parse_date(value: str) -> Optional[str]:
value = (value or "").strip()
for fmt in ("%a, %d %b %Y %H:%M:%S %Z", "%a %b %d %H:%M:%S %z %Y"):
try:
return datetime.strptime(value, fmt).strftime("%Y-%m-%d")
except (TypeError, ValueError):
continue
try:
return datetime.fromisoformat(value.replace("Z", "+00:00")).strftime("%Y-%m-%d")
except (TypeError, ValueError):
return None
def _split_blocks(text: str) -> List[str]:
"""Split the model's prose into per-post blocks.
Keyed on the post-id field starting a new record rather than on any
heading style, because the narration around the blocks varies run to run.
"""
blocks: List[str] = []
current: List[str] = []
for line in (text or "").splitlines():
match = _FIELD_LINE.match(line)
key = _FIELD_ALIASES.get(match.group(1).strip().lower()) if match else None
if key == "post_id" and current:
blocks.append("\n".join(current))
current = []
if match or current:
current.append(line)
if current:
blocks.append("\n".join(current))
return blocks
def parse_x_response(
response: Dict[str, Any],
topic: str = "",
from_date: str = "",
to_date: str = "",
) -> List[Dict[str, Any]]:
"""Parse a grok response into normalized X item dicts.
Total: returns [] on error rather than raising.
"""
if not isinstance(response, dict):
return []
if response.get("error"):
_log(f"error: {response['error']}")
return []
raw: List[Dict[str, Any]] = []
for block in _split_blocks(response.get("text") or ""):
fields: Dict[str, Any] = {}
for line in block.splitlines():
match = _FIELD_LINE.match(line)
if not match:
continue
key = _FIELD_ALIASES.get(match.group(1).strip().lower())
if key and key not in fields:
value = match.group(2).strip()
# Field values arrive with varying markdown decoration
# (`- **id:** 123`), so strip emphasis and code marks.
value = value.strip("*").strip().strip("`").strip()
fields[key] = value
if fields.get("post_id"):
raw.append(fields)
kept, reason = _validate_items(raw, from_date, to_date) if from_date else (raw, "")
if reason:
_log(f"rejected response: {reason}")
_LAST_REJECTION["reason"] = reason
return []
_LAST_REJECTION["reason"] = ""
items: List[Dict[str, Any]] = []
seen_ids = set()
for index, fields in enumerate(kept, start=1):
post_id = str(fields.get("post_id") or "").strip()
if post_id in seen_ids:
continue
seen_ids.add(post_id)
handle = _clean_handle(fields.get("author_handle"))
if not handle:
author = str(fields.get("author") or "")
match = re.search(r"@([A-Za-z0-9_]{1,15})", author)
handle = match.group(1) if match else ""
if not handle:
continue
text = str(fields.get("text") or "").strip()[:500]
engagement = {
"likes": _as_int(str(fields.get("likes", ""))),
"reposts": _as_int(str(fields.get("reposts", ""))),
"replies": _as_int(str(fields.get("replies", ""))),
"quotes": _as_int(str(fields.get("quotes", ""))),
}
items.append({
"id": f"GK{index}",
"text": text,
"url": f"https://x.com/{handle}/status/{post_id}",
"author_handle": handle,
"date": _parse_date(str(fields.get("created_at", ""))),
"engagement": engagement if any(v is not None for v in engagement.values()) else None,
"why_relevant": "",
"relevance": _compute_relevance(topic, text) if topic else 0.7,
})
return items
# --- invocation ------------------------------------------------------------
_PROMPT = """Use {tool} with query '{query}', mode Top, limit {limit}.
Report every post the tool returned, one block per post, using exactly these
field labels on their own lines:
id: <numeric post id>
handle: <author handle without @>
created_at: <post timestamp>
likes: <number>
reposts: <number>
replies: <number>
quotes: <number>
text: <full post text on one line>
Report only posts the tool actually returned. If the tool returned nothing or
could not run, say so plainly and report no post blocks. Do not supply posts
from your own knowledge."""
def _invoke(prompt: str, timeout: int) -> Dict[str, Any]:
"""Run `grok` once. Never raises; every failure returns {'error': str}."""
binary = binary_path()
if binary is None:
return {"error": "grok CLI not found on PATH"}
# Isolated working directory: the child has tool permissions bypassed and
# its context carries untrusted post text, so give it an empty directory
# rather than the user's repository to act in.
try:
with tempfile.TemporaryDirectory(prefix="last30days-grok-") as workdir:
child_home = _stage_child_home(workdir)
result = subprocess.run(
[binary, "-p", prompt, "--permission-mode", "bypassPermissions"],
capture_output=True,
text=True,
timeout=timeout,
cwd=workdir,
env=_subprocess_env(child_home),
)
except FileNotFoundError:
return {"error": "grok CLI not found on PATH"}
except subprocess.TimeoutExpired:
return {"error": f"grok CLI timed out after {timeout}s"}
except OSError as exc:
return {"error": f"{type(exc).__name__}: {exc}"}
except Exception as exc: # noqa: BLE001 - search_x must never raise
return {"error": f"{type(exc).__name__}: {exc}"}
if result.returncode != 0:
detail = (result.stderr or result.stdout or "").strip()[:300]
return {"error": f"grok CLI exited {result.returncode}: {detail}"}
return {"text": result.stdout or ""}
def _run_query(
query: str,
from_date: str,
to_date: str,
*,
tool: str = "x_keyword_search",
limit: int = _MAX_LIMIT_PER_CALL,
depth: str = "default",
attempts: int = 2,
relevance_topic: str = "",
deadline: Optional[float] = None,
) -> Tuple[List[Dict[str, Any]], str]:
"""Run one query, retrying only when the response looks fabricated.
A clean empty result is NOT retried: an empty window is a common, correct
outcome (especially for the mention lane on a low-profile handle and for
the name lane's engagement floor), and re-issuing a byte-identical prompt
doubles latency and Grok-plan spend to get the same answer.
"""
timeout = _TIMEOUT_SECONDS.get(depth, _TIMEOUT_SECONDS["default"])
prompt = _PROMPT.format(tool=tool, query=query, limit=min(limit, _MAX_LIMIT_PER_CALL))
last_error = ""
for attempt in range(1, attempts + 1):
if deadline is not None:
remaining = deadline - time.monotonic()
# Never start a call that cannot finish inside the shared budget.
# A floor here (max(15, ...)) would let the last call overrun the
# documented ceiling by up to that floor, since the lanes run
# synchronously with no outer timeout to catch it.
if remaining < _MIN_USEFUL_CALL_SECONDS:
return [], last_error or "X lane budget exhausted"
timeout = min(timeout, int(remaining))
_log(f"searching: {query}" + (f" (attempt {attempt})" if attempt > 1 else ""))
response = _invoke(prompt, timeout)
if response.get("error"):
last_error = response["error"]
continue
items = parse_x_response(
response,
topic=relevance_topic or query,
from_date=from_date,
to_date=to_date,
)
if items:
return items, ""
reason = _LAST_REJECTION.get("reason", "")
if not any(marker in reason for marker in _RETRYABLE_REJECTIONS):
# Clean empty result: the search ran and found nothing.
return [], ""
last_error = reason or "no verified in-window posts returned"
return [], last_error
def search_x(
topic: str,
from_date: str,
to_date: str,
depth: str = "default",
) -> Dict[str, Any]:
"""Search X for a topic, fanning out to reach the depth's target count.
The underlying tool caps each call at 10 posts, so depth is achieved across
calls. Without this, grok returned 10 posts at every depth while sitting
ahead of bird in the chain -- silently downgrading a `--deep` run from 60
posts to 10.
Returns {'items': [...]}; 'error' is set only for an actual invocation
failure. A completed run that found nothing returns an empty list with no
error, matching bird and xquik -- reporting "no results" as a hard failure
would make an empty window look like a broken backend.
"""
target = DEPTH_CONFIG.get(depth, DEPTH_CONFIG["default"])
calls = max(1, min(_MAX_FANOUT_CALLS, -(-target // _MAX_LIMIT_PER_CALL)))
collected: List[Dict[str, Any]] = []
seen: set = set()
last_error = ""
invocation_failed = False
for mode_query in _fanout_queries(topic, from_date, to_date, calls):
items, error = _run_query(mode_query, from_date, to_date, depth=depth,
relevance_topic=topic)
if error and not items:
last_error = error
# Distinguish "the CLI failed" from "the search found nothing".
if "not found" in error or "timed out" in error or "exited" in error:
invocation_failed = True
for item in items:
key = item["url"]
if key not in seen:
seen.add(key)
collected.append(item)
if len(collected) >= target:
break
for index, item in enumerate(collected, start=1):
item["id"] = f"GK{index}"
if collected:
return {"items": collected[:target]}
if invocation_failed:
return {"items": [], "error": last_error}
return {"items": []}
def search_handles(
handles: List[str],
topic: str,
from_date: str,
to_date: str,
*,
count_per: int = 8,
deadline: Optional[float] = None,
) -> List[Dict[str, Any]]:
"""BY lane: posts authored by each handle.
``topic`` is used for relevance ranking only and is never ANDed into the
query -- doing so was a prior defect that emptied the lane.
"""
collected: List[Dict[str, Any]] = []
for handle in handles:
if deadline is not None and time.monotonic() >= deadline:
_log("lane budget exhausted; skipping remaining handles")
break
clean = _clean_handle(handle)
if not clean:
continue
items, _ = _run_query(
f"from:{clean} since:{from_date} until:{to_date}",
from_date, to_date, limit=count_per, relevance_topic=topic,
attempts=1, deadline=deadline,
)
# Enforce the author constraint client-side: operator fidelity is not
# guaranteed. A measured `from:` query returned a different account.
collected.extend(
i for i in items
if i["author_handle"].lower() == clean.lower()
)
return collected
def search_mentions(
handles: List[str],
from_date: str,
to_date: str,
*,
topic: str = "",
count_per: int = 5,
deadline: Optional[float] = None,
) -> List[Dict[str, Any]]:
"""ABOUT lane (mention form): posts @-mentioning each handle."""
collected: List[Dict[str, Any]] = []
for handle in handles:
if deadline is not None and time.monotonic() >= deadline:
_log("lane budget exhausted; skipping remaining handles")
break
clean = _clean_handle(handle)
if not clean:
continue
items, _ = _run_query(
f"@{clean} -from:{clean} since:{from_date} until:{to_date}",
from_date, to_date, limit=count_per, relevance_topic=topic,
attempts=1, deadline=deadline,
)
# Enforce the exclusion client-side too: a measured run carrying
# `-from:X` still returned a post authored by X.
collected.extend(
i for i in items
if i["author_handle"].lower() != clean.lower()
)
return collected
def search_name(
name: str,
from_date: str,
to_date: str,
*,
exclude_handles: Optional[List[str]] = None,
count_per: int = 8,
min_faves: int = 2,
deadline: Optional[float] = None,
) -> List[Dict[str, Any]]:
"""ABOUT lane (name form): posts naming the subject in plain text.
Not redundant with the mention lane and not a fallback for it. Most talk
about a person or company never @-mentions them -- people write "Bentgo
lunch box from Costco", not "@Bentgo lunch box from Costco". A modest
engagement floor applies here only, because a bare name query is the
widest and noisiest of the three lanes.
"""
name = (name or "").strip()
if not name:
return []
# Mirror bird_x.build_topic_query's guard: an unbalanced quote reads as an
# unterminated phrase and matches nothing.
if name.count('"') % 2:
name = name.replace('"', " ").strip()
phrase = f'"{name}"' if " " in name else name
excludes = " ".join(
f"-from:{clean}"
for clean in (_clean_handle(h) for h in (exclude_handles or []))
if clean
)
query = " ".join(
part for part in
[phrase, excludes, f"min_faves:{min_faves}", f"since:{from_date}", f"until:{to_date}"]
if part
)
items, _ = _run_query(
query, from_date, to_date, limit=count_per, attempts=1, deadline=deadline,
)
blocked = {c.lower() for c in (_clean_handle(h) for h in (exclude_handles or [])) if c}
return [i for i in items if i["author_handle"].lower() not in blocked]
+14 -1
View File
@@ -132,7 +132,7 @@ _PP_CLI_SUFFIX = "-pp-cli"
_PRINTING_PRESS_NPM = "@mvanhorn/printing-press-library@0.1.16"
# Dependencies the doctor probes by default.
KNOWN_DEPENDENCIES: Tuple[str, ...] = ("yt-dlp", "digg-pp-cli", "node", "ffmpeg")
KNOWN_DEPENDENCIES: Tuple[str, ...] = ("yt-dlp", "digg-pp-cli", "node", "ffmpeg", "grok")
# Cheap side-effect-free version invocation per dependency (default --version).
_VERSION_ARGS: Dict[str, List[str]] = {
@@ -157,6 +157,15 @@ _MANAGER_PRESCRIPTIONS: Dict[str, Dict[str, Tuple[str, str]]] = {
"brew": ("brew install ffmpeg", "brew reinstall ffmpeg"),
"apt": ("sudo apt-get install -y ffmpeg", "sudo apt-get install -y --reinstall ffmpeg"),
},
# The official installer is the primary path; npm is a real alternative
# (the package is published as @xai-official/grok) and fits the existing
# manager-preference machinery.
"grok": {
"npm": (
"npm install -g @xai-official/grok",
"reinstall the Grok CLI: npm install -g @xai-official/grok@latest",
),
},
}
# Last-resort prescriptions when no known package manager is detected.
@@ -173,6 +182,10 @@ _FALLBACK_PRESCRIPTIONS: Dict[str, Tuple[str, str]] = {
"install ffmpeg (https://ffmpeg.org/download.html) and ensure it is on PATH",
"reinstall ffmpeg (https://ffmpeg.org/download.html); the current binary won't run",
),
"grok": (
"install the Grok CLI: curl -fsSL https://x.ai/cli/install.sh | bash, then run `grok login`",
"reinstall the Grok CLI: curl -fsSL https://x.ai/cli/install.sh | bash; the current binary won't run",
),
}
+309 -16
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import copy
from collections.abc import Iterable
import math
import queue
import re
@@ -32,6 +33,7 @@ from . import (
entity_extract,
env,
github,
grok_x,
grounding,
hackernews,
health,
@@ -2094,6 +2096,32 @@ def run(
print("[Planner] (no subqueries in plan)", file=sys.stderr)
bundle = schema.RetrievalBundle(artifacts={"grounding": []})
# Handles the user named explicitly. Available before any retrieval, unlike
# the entity-extracted set, so Phase 1 and quick-depth runs get first-party
# protection too. Without this the exemption reached only the Phase 2
# supplement path -- which quick runs skip entirely -- so a subject-authored
# post retrieved in Phase 1 was still pruned before fusion, which is exactly
# the evidence loss this change exists to prevent.
explicit_first_party = {
h.lstrip("@").strip().lower()
for h in ([x_handle, github_user, *(x_related or [])])
if h and h.strip()
}
# Real X handles: --x-handle, --x-related, or @mentions in the topic. These
# determine whether the deferred X floor applies. Topic words like "peter"
# are NOT real handles and should not trigger the floor — when no real
# handle is identified, the floor is skipped entirely (policy: a noisier
# report beats losing the subject's evidence).
explicit_x_handles = {
h.lstrip("@").strip().lower()
for h in ([x_handle, *(x_related or [])])
if h and h.strip()
} | _topic_handle_mentions(topic)
# Plus handle-shaped tokens from the topic. Phase 1 and quick-depth runs
# never reach automatic handle resolution, so without this a quick search
# naming a subject still discards everything that subject wrote.
explicit_first_party |= _topic_first_party_candidates(topic)
for source in (requested_sources or []):
if source not in available:
bundle.record_failure(
@@ -2359,6 +2387,10 @@ def run(
source, raw_items, from_date, to_date,
freshness_mode=plan.freshness_mode,
ranking_query=subquery.ranking_query,
first_party_handles=explicit_first_party,
# X defers its relevance floor until resolved_handles exists.
# Everything else prunes here as before.
defer_relevance_prune=(source == "x"),
)
# Jobs is exempt from per_stream_limit: a careers board is a complete
# snapshot of open roles, and truncating it to the default 12 drops
@@ -2370,6 +2402,7 @@ def run(
bundle.artifacts.setdefault("grounding", []).append(artifact)
# Phase 2: supplemental entity-based searches
supplemental_handles: list[str] = []
_run_supplemental_searches(
topic=topic,
bundle=bundle,
@@ -2383,6 +2416,7 @@ def run(
rate_limit_lock=rate_limit_lock,
x_handle=x_handle,
x_related=x_related,
resolved_handles_out=supplemental_handles,
)
# Phase 2b: retry thin sources with simplified query
@@ -2409,6 +2443,7 @@ def run(
tiktok_hashtags=tiktok_hashtags,
tiktok_creators=tiktok_creators,
ig_creators=ig_creators,
first_party_handles=explicit_first_party,
)
# Reclassify partial failures as DEGRADED instead of silently dropping them.
@@ -2436,23 +2471,53 @@ def run(
elapsed=time.monotonic() - run_started,
)
source_status = _finalize_source_status(bundle.source_status, items_by_source)
# Normalized set of handles this run resolved for the topic. A candidate
# authored by one of these is first-party and is exempted from the
# entity-miss demotion in rerank (a post never repeats its own author's
# name, so the body-text grounding check would otherwise zero out the
# subject's own highest-signal posts). Built before fusion so the
# per-author cap can give the topic's subject a higher allowance than an
# incidental third-party account.
resolved_handles = explicit_first_party | {
h.lstrip("@").strip().lower()
for h in supplemental_handles
if h and h.strip()
}
# Real X handles from explicit flags, @mentions in topic, or Phase 2 discovery.
# When no real handle is identified, skip the X floor entirely — a noisier
# report beats losing the subject's evidence. Topic tokens like "peter" are
# NOT real handles: they populate resolved_handles for downstream first-party
# protection but should NOT trigger the floor.
real_x_handles = explicit_x_handles | {
h.lstrip("@").strip().lower()
for h in supplemental_handles
if h and h.strip()
}
# Deferred X relevance floor. Phase 1 skipped it so this could run with the
# run's actual resolved handles rather than a guess made before anyone knew
# who the subject was. Applied per subquery stream so fusion sees the same
# shape it always has. Only applied when we have real X handles — topic
# tokens alone cannot identify the subject.
if real_x_handles:
for key, stream in list(bundle.items_by_source_and_query.items()):
if key[1] != "x" or not stream:
continue
bundle.items_by_source_and_query[key] = signals.prune_low_relevance(
stream, first_party_handles=resolved_handles
)
if bundle.items_by_source.get("x"):
bundle.items_by_source["x"] = signals.prune_low_relevance(
bundle.items_by_source["x"], first_party_handles=resolved_handles
)
candidates = weighted_rrf(
bundle.items_by_source_and_query,
plan,
pool_limit=settings["pool_limit"],
range_from=from_date,
range_to=to_date,
first_party_handles=resolved_handles,
)
# Normalized set of handles this run resolved for the topic. A candidate
# authored by one of these is first-party and is exempted from the
# entity-miss demotion in rerank (a post never repeats its own author's
# name, so the body-text grounding check would otherwise zero out the
# subject's own highest-signal posts).
resolved_handles = {
h.lstrip("@").strip().lower()
for h in ([x_handle, github_user, *(x_related or [])])
if h and h.strip()
}
private_candidates = [
candidate
for candidate in candidates
@@ -2538,6 +2603,10 @@ def run(
clusters = cluster_candidates(ranked_candidates, plan)
warnings = _warnings(items_by_source, ranked_candidates, bundle.errors_by_source, degraded_by_source)
# One-sided entity coverage is a reporting warning, not a source failure:
# marking the source PARTIAL would trip LAST30DAYS_STRICT_EXIT on runs that
# returned good X results.
warnings.extend(bundle.artifacts.get("x_partial_coverage", []))
library_context, library_warning = _load_library_context(
topic=topic,
config=config,
@@ -2743,6 +2812,26 @@ def merge_drill_report(
return merged
def _batch_subject_handles(raw_items: list[dict], *, top_n: int = 2) -> set[str]:
"""Most-mentioned handles in a batch of X items, as first-party candidates.
Mirrors entity_extract's ranking but runs before pruning rather than after,
and keys on *mentions only* rather than mentions plus authors. That
distinction is the safety property: a prolific commentator inflates the
author count, but being mentioned by other accounts is what identifies the
subject of a topic. Capped at the top few so a busy thread cannot exempt
the whole batch.
"""
counts: Counter = Counter()
for item in raw_items or []:
text = str((item or {}).get("text") or "")
for mention in re.findall(r"@([A-Za-z0-9_]{1,15})", text):
counts[mention.lower()] += 1
if not counts:
return set()
return {handle for handle, _ in counts.most_common(top_n)}
def _normalize_score_dedupe(
source: str,
raw_items: list[dict],
@@ -2750,8 +2839,20 @@ def _normalize_score_dedupe(
to_date: str,
freshness_mode: str,
ranking_query: str,
first_party_handles: Iterable[str] | None = None,
defer_relevance_prune: bool = False,
) -> list[schema.SourceItem]:
"""Normalize, annotate, prune, dedupe, and extract snippets for a batch of raw items."""
"""Normalize, annotate, prune, dedupe, and extract snippets for a batch of raw items.
``defer_relevance_prune`` skips the relevance floor here so the caller can
apply it once the run has resolved who the topic's subject is. Pruning X
before handle resolution is the ordering bug behind the whole first-party
evidence loss: the floor cannot exempt an author nobody has identified yet,
and no amount of guessing at prune time substitutes for knowing.
``first_party_handles`` names accounts this run is explicitly searching, so
their own posts survive the relevance floor (see signals.prune_low_relevance).
"""
normalized = normalize.normalize_source_items(
source, raw_items, from_date, to_date,
freshness_mode=freshness_mode,
@@ -2768,8 +2869,24 @@ def _normalize_score_dedupe(
reference_date=to_date,
max_days=lookback_window_days,
)
if source != "jobs":
normalized = signals.prune_low_relevance(normalized)
if source != "jobs" and not defer_relevance_prune:
floor_handles = set(first_party_handles or ())
if source == "x":
# Union, never a fallback. The caller's set is derived partly from
# topic tokens, so it is non-empty for essentially every real topic
# -- gating this on "no handles supplied" would make it dead code
# and leave the name-only case exactly as broken as before.
#
# Reuses the engine's own resolution signal on the batch already in
# hand: posts *about* a subject mention their handle, so the
# most-mentioned account in a topic's own results is the subject.
# Costs nothing extra -- no search, no network -- and closes the
# case where the handle never appears in the topic at all
# ("Peter Steinberger" -> @steipete).
floor_handles |= _batch_subject_handles(raw_items)
normalized = signals.prune_low_relevance(
normalized, first_party_handles=floor_handles
)
normalized = dedupe.dedupe_items(normalized)
for item in normalized:
item.snippet = snippet.extract_best_snippet(item, prepared_query)
@@ -3182,6 +3299,65 @@ def _is_transient_error(exc: Exception) -> bool:
return any(code in msg for code in ("500", "502", "503", "504"))
def _topic_handle_mentions(topic: str) -> set[str]:
"""@mentions in the topic, which are real X handles.
These are used to determine whether the subject was identified: an
@mention like "@steipete" is a real handle that can exempt its owner from
the relevance floor. Regular words like "Peter" are not real handles.
"""
return {
mention.lower()
for mention in re.findall(r"@([A-Za-z0-9_]{1,15})", topic or "")
}
def _topic_first_party_candidates(topic: str) -> set[str]:
"""Handle-shaped tokens in the topic itself, usable before any retrieval.
Phase 1 runs before automatic handle resolution, and a quick-depth run
skips that resolution entirely, so neither has access to the extracted
handle set. Without this a quick search for "Peter Steinberger steipete"
still drops every post steipete wrote, which is the exact failure this
branch exists to fix.
Deliberately permissive about what looks like a handle and strict about
what it does: a candidate only ever matters if a retrieved post's *author*
matches it, so an ordinary word like "lunch" costs nothing -- no author is
named "lunch". The realistic false positive is an account named after a
topic word, which the frequency-ranked path could surface anyway.
"""
tokens = set()
for mention in re.findall(r"@([A-Za-z0-9_]{1,15})", topic or ""):
tokens.add(mention.lower())
for word in re.findall(r"[A-Za-z0-9_]{3,15}", topic or ""):
lowered = word.lower()
if lowered not in relevance.STOPWORDS:
tokens.add(lowered)
return tokens
def _name_lane_subject(topic: str) -> str:
"""Resolve the entity name to search for by name, not the whole topic.
Phrase-quoting a raw topic ("Peter Steinberger steipete") matches nothing
on X: nobody writes the handle and the display name together. Prefer a
title-cased proper noun the way the planner's keyword query does, and fall
back to the first compound term, then to the topic.
"""
import re as _re
compounds = query.extract_compound_terms(topic) or []
title_cased = [
term for term in compounds
if _re.match(r"^(?:[A-Z][a-z]+\s+){1,}[A-Z][a-z]+$", term)
]
if title_cased:
return title_cased[0]
if compounds:
return compounds[0]
return topic.strip()
def _run_supplemental_searches(
*,
topic: str,
@@ -3196,6 +3372,7 @@ def _run_supplemental_searches(
rate_limit_lock: threading.Lock,
x_handle: str | None = None,
x_related: list[str] | None = None,
resolved_handles_out: list[str] | None = None,
) -> None:
"""Phase 2: extract entities from Phase 1 results, run targeted supplemental searches."""
if depth == "quick" or mock:
@@ -3248,6 +3425,40 @@ def _run_supplemental_searches(
if rh_clean and rh_clean != primary_lower and rh_clean not in [h.lower() for h in handles]:
related_handles.append(rh_clean)
# Surface every handle this run resolved back to the caller. resolved_handles
# is built later from --x-handle / --github-user / --x-related only, so
# without this an auto-discovered subject handle never reaches it and every
# downstream first-party protection (entity-miss exemption, FIRST_PARTY_FLOOR,
# interaction floor) stays inert on any run that did not pass --x-handle.
# Populated before the early return below so a run whose lanes cannot execute
# still contributes its resolved handles.
if resolved_handles_out is not None:
# Only corroborated handles get first-party status. The extracted set is
# frequency-ranked over retrieved post text, so a prolific commentator --
# or an engagement-farming account that posts on every topic -- lands in
# it without being the subject. First-party status is strong: it exempts
# an author from the relevance floor entirely and raises their per-author
# cap, so granting it on frequency alone would let a spam account buy
# immunity from filtering. Require the handle to look like the topic's
# subject, or to have been named explicitly by the user.
explicit = {
h.lstrip("@").strip().lower()
for h in ([x_handle] + list(x_related or []))
if h and h.strip()
}
topic_tokens = {t for t in re.findall(r"[a-z0-9]+", topic.lower()) if len(t) > 2}
seen = {h.lower() for h in resolved_handles_out}
for h in [*handles, *related_handles]:
clean = h.lstrip("@").strip().lower()
if not clean or clean in seen:
continue
corroborated = clean in explicit or any(
token in clean or clean in token for token in topic_tokens
)
if corroborated:
resolved_handles_out.append(clean)
seen.add(clean)
if not handles and not related_handles:
return
@@ -3264,9 +3475,44 @@ def _run_supplemental_searches(
pinned = runtime.x_search_backend
if pinned:
chain = [pinned] + [b for b in chain if b != pinned]
primary = next((b for b in chain if b in ("bird", "xquik")), None)
primary = next((b for b in chain if b in ("grok", "bird", "xquik")), None)
if primary == "bird":
# Name lane (posts naming the subject in plain text, no @-mention) is
# grok-only for now: it needs phrase-quoting and negation operators the
# other handle-capable backends do not expose uniformly. It is NOT a
# fallback for the mention lane -- most discussion of a person or company
# never @-mentions them, so the two lanes reach disjoint sets.
_name_lane = None
if primary == "grok":
# One budget shared by all three lanes, started here rather than per
# lane: the point is to bound the total, not each part.
lane_deadline = time.monotonic() + grok_x.LANE_BUDGET_SECONDS
def _from_lane(hs: list, count: int) -> list:
return grok_x.search_handles(
hs, topic, from_date, to_date, count_per=count,
deadline=lane_deadline,
)
def _about_lane(hs: list, count: int) -> list:
return grok_x.search_mentions(
hs, from_date, to_date, topic=topic, count_per=count,
deadline=lane_deadline,
)
def _name_lane(hs: list, count: int) -> list:
# Use the resolved entity name, not the raw topic. Phrase-quoting
# the whole topic ("Peter Steinberger steipete") matches nothing on
# X; the subject's name is what other people actually write.
subject = _name_lane_subject(topic)
if not subject.strip():
return []
return grok_x.search_name(
subject, from_date, to_date, exclude_handles=hs, count_per=count,
deadline=lane_deadline,
)
elif primary == "bird":
def _from_lane(hs: list, count: int) -> list:
return bird_x.search_handles(hs, topic, from_date, count_per=count)
@@ -3330,13 +3576,48 @@ def _run_supplemental_searches(
f"Phase 2 ABOUT-lane: {exc}",
attempted=attempted,
)
raw_items = from_items + about_items
name_items: list = []
if _name_lane is not None:
try:
name_items = _name_lane(handles, MENTION_LANE_COUNT_PER)
except Exception as exc:
print(f"[Pipeline] Phase 2 NAME-lane search failed: {exc}", file=sys.stderr)
state, attempted = _classify_source_failure(exc)
bundle.record_failure(
x_slug, state, f"Phase 2 NAME-lane: {exc}", attempted=attempted,
)
raw_items = from_items + about_items + name_items
# Partial coverage is a reportable outcome, not a normal result: a
# report carrying only one side of an entity topic is incomplete, and
# without this it looks indistinguishable from genuinely thin
# discussion.
if _name_lane is not None:
empty = [
label for label, items in
(("by", from_items), ("mention", about_items), ("name", name_items))
if not items
]
if empty and len(empty) < 3:
# A warning, not a source outcome. record_failure would set the
# X source to PARTIAL, which is outside _STRICT_EXIT_OK_STATES
# and would make wrappers using LAST30DAYS_STRICT_EXIT exit 3 on
# runs that returned perfectly good X coverage. An empty lane is
# common and legitimate: the name lane carries an engagement
# floor and the mention lane is empty for most non-famous
# handles.
bundle.artifacts.setdefault("x_partial_coverage", []).append(
f"X partial coverage: {', '.join(empty)} lane(s) returned "
"nothing; the report may show only one side of this entity."
)
if raw_items:
normalized = _normalize_score_dedupe(
x_slug, raw_items, from_date, to_date,
freshness_mode=plan.freshness_mode,
ranking_query=ranking_query,
first_party_handles=handles,
)
# Deduplicate against Phase 1 URLs
normalized = [item for item in normalized if item.url not in existing_urls]
@@ -3367,6 +3648,7 @@ def _run_supplemental_searches(
x_slug, raw_items, from_date, to_date,
freshness_mode=plan.freshness_mode,
ranking_query=ranking_query,
first_party_handles=related_handles,
)
# Deduplicate against all existing URLs (Phase 1 + primary handles)
normalized = [item for item in normalized if item.url not in existing_urls]
@@ -3406,6 +3688,7 @@ def _retry_thin_sources(
tiktok_hashtags: list[str] | None = None,
tiktok_creators: list[str] | None = None,
ig_creators: list[str] | None = None,
first_party_handles: Iterable[str] | None = None,
) -> None:
"""Retry sources with thin results using simplified core subject query."""
if depth == "quick":
@@ -3481,6 +3764,13 @@ def _retry_thin_sources(
to_date,
freshness_mode=plan.freshness_mode,
ranking_query=retry_subquery.ranking_query,
first_party_handles=first_party_handles,
# Match Phase 1: X defers its relevance floor until the run has
# resolved handles. Applying it here would discard a subject-
# authored post that does not repeat the subject's name, and the
# later resolved-handle floor cannot recover a post that never
# entered the bundle.
defer_relevance_prune=(source == "x"),
)
if source == "jobs":
return source, normalized, outcome_note
@@ -3530,6 +3820,9 @@ def _fetch_x_backend(backend, subquery, from_date, to_date, depth, config):
if backend == "bird":
result = bird_x.search_x(query, from_date, to_date, depth=depth)
items = bird_x.parse_bird_response(result, query=query)
elif backend == "grok":
result = grok_x.search_x(query, from_date, to_date, depth=depth)
items = result.get("items", []) if isinstance(result, dict) else []
elif backend == "xai":
model = config.get("LAST30DAYS_X_MODEL") or config.get("XAI_MODEL_PIN") or providers.XAI_DEFAULT
result = xai_x.search_x(config["XAI_API_KEY"], model, query, from_date, to_date, depth=depth)
+20 -2
View File
@@ -836,8 +836,26 @@ def _keyword_query(topic: str, core: str) -> str:
term for term in compounds
if re.match(r"^(?:[A-Z][a-z]+\s+){1,}[A-Z][a-z]+$", term)
]
quoted = " ".join(f'"{term}"' for term in title_cased[:2])
keywords = [quoted.strip(), core.strip() or topic.strip()]
selected = title_cased[:2]
quoted = " ".join(f'"{term}"' for term in selected)
remainder = core.strip() or topic.strip()
# Drop words already carried by a quoted phrase. Emitting both produced
# '"Peter Steinberger" peter steinberger steipete', which reads to a
# provider as the phrase AND each of its words again -- strictly narrower
# than the phrase alone, and on X it degraded to a bare token conjunction
# once the quotes were stripped downstream. Distinct tokens (here
# "steipete") are preserved.
if selected and remainder:
phrase_words = {
word.lower()
for term in selected
for word in term.split()
}
remainder = " ".join(
word for word in remainder.split()
if word.strip('"').lower() not in phrase_words
)
keywords = [quoted.strip(), remainder.strip()]
return " ".join(part for part in keywords if part).strip()
@@ -90,6 +90,23 @@ REGISTRY: Dict[Tuple[str, str], Prescription] = dict((
fix_cli=SETUP_BROWSER_COOKIES_CLI,
anchor="api-keys-env",
),
_entry(
"x", "grok_cli_missing",
cause="the Grok CLI is not installed, so the keyless X path is unavailable",
fix_nl=(
"install the Grok CLI (curl -fsSL https://x.ai/cli/install.sh | bash) "
"and sign in with `grok login` to search X without any X credential"
),
fix_cli="npm install -g @xai-official/grok",
anchor="api-keys-env",
),
_entry(
"x", "grok_not_authenticated",
cause="the Grok CLI is installed but not signed in",
fix_nl="sign in to Grok once; no X account or API key is needed after that",
fix_cli="grok login",
anchor="api-keys-env",
),
_entry(
"scrapecreators", "key_missing",
cause="SCRAPECREATORS_API_KEY is not set",
+1 -1
View File
@@ -316,7 +316,7 @@ def resolve_runtime(config: dict[str, Any], depth: str) -> tuple[schema.Provider
def _resolve_x_backend(config: dict[str, Any]) -> str | None:
preferred = (config.get(env.X_BACKEND_PIN_VAR) or "").lower()
if preferred in {"xai", "bird"}:
if preferred in env.X_BACKEND_ORDER:
return preferred
return env.get_x_source(config)
+17 -1
View File
@@ -301,9 +301,25 @@ def _build_nudge_text(
free_suggestions.append(f"X/Twitter errored - {x_fix.fix_nl}.")
else:
x_fix = prescriptions.get("x", "cookies_missing")
# Pick by state: telling a user who already installed grok to
# install it again is the stale-shim reading the health layer
# exists to avoid. Mirrors _probe_grok's three-way split.
from . import grok_x as _grok_x
grok_key = (
"grok_not_authenticated"
if _grok_x.binary_path() and not _grok_x.has_stored_auth()
else "grok_cli_missing"
)
grok_fix = prescriptions.get("x", grok_key)
# Deliberately not described as free: grok needs no X credential,
# but it does need an installed, signed-in grok CLI drawing on a
# Grok plan. This block is headed "Free suggestions", so the
# precondition has to be stated inline rather than inherited.
free_suggestions.append(
"X/Twitter: real-time posts with likes and reposts - the fastest "
f"signal for breaking topics. Three options: {x_fix.fix_nl}."
"signal for breaking topics. Easiest path if you have a Grok "
f"account: {grok_fix.fix_nl} (no X credential at all). "
f"Otherwise: {x_fix.fix_nl}."
)
if "youtube" in core_missing:
@@ -59,6 +59,19 @@ SYNONYMS = {
# Generic query words that should not carry relevance on their own.
# They still help when paired with stronger entity/topic matches.
#
# The second group is scaffolding emitted by planner's ranking-query templates
# ("What recent evidence from the last 30 days is most relevant to X?" and its
# siblings). Those words are not the topic, but every one of them was being
# counted as an informative query token, which capped achievable coverage at the
# topic's share of the query and demoted on-topic posts. Kept here rather than
# stripped in the planner so any caller building a similar natural-language
# ranking query gets the same treatment.
#
# Domain nouns from those same templates (production, market, workflows,
# experience, signals, ...) are deliberately absent: they can legitimately be a
# user's topic, and demoting them globally would hurt every source.
# tests/test_ranking_query_scaffolding.py pins that split.
LOW_SIGNAL_QUERY_TOKENS = frozenset({
'advice', 'animation', 'animations', 'best', 'chance', 'chances',
'code', 'compare', 'comparison', 'differences', 'explain', 'guide',
@@ -67,6 +80,10 @@ LOW_SIGNAL_QUERY_TOKENS = frozenset({
'prompting', 'prompts', 'rate', 'review', 'reviews', 'thoughts',
'tip', 'tips', 'tutorial', 'tutorials', 'update', 'updates', 'use',
'using', 'versus', 'vs', 'worth',
# planner ranking-query scaffolding
'30', 'current', 'days', 'describing', 'especially', 'evidence', 'exist',
'follow', 'hands', 'last', 'matter', 'most', 'new', 'people', 'real',
'recent', 'relevant', 'running', 'up', 'world',
})
+6 -13
View File
@@ -17,6 +17,7 @@ from . import (
library_index,
registers,
relevance,
rerank,
schema,
signals,
skill_meta,
@@ -3411,20 +3412,12 @@ def _source_label(source: str) -> str:
def _best_take_relevance_ok(candidate) -> bool:
"""Exclude off-topic-but-viral candidates from Best Takes.
The engine demotes candidates that don't match the topic entity by tagging
``entity-miss`` in the explanation and/or zeroing ``final_score`` (e.g. a
39k-like Grand Tour comment surfacing in a 'Patagonia brand' run). Those
must never reach Best Takes no matter how upvoted their comments are.
Plain ``fallback-local-score`` (without entity-miss) is NOT a demotion --
it is the default reason when LLM rerank didn't score an item -- so it is
not gated here.
Delegates to ``rerank.candidate_relevance_ok``, which owns the entity-miss
demotion test. Do not re-implement the check here: this site previously
carried its own copy, which meant the first-party carve-out applied in
rerank never reached Best Takes or cluster visibility.
"""
explanation = (candidate.explanation or "").lower()
if "entity-miss" in explanation:
return False
if (candidate.final_score or 0.0) <= 0.0:
return False
return True
return rerank.candidate_relevance_ok(candidate)
def _effective_fun_score(candidate, vote_weight: float) -> float:
+36 -1
View File
@@ -26,6 +26,10 @@ ENTITY_MISS_PENALTY = 25.0
FALLBACK_ENTITY_MISS_CONFIDENCE_ESCAPE = 0.5
FALLBACK_ENTITY_MISS_TOPIC_ESCAPE = 0.25
_FALLBACK_ENTITY_MISS_EXPLANATION = "fallback-local-score (entity-miss demotion)"
# Explanation stamped on a first-party post whose entity-miss marker was
# cleared by _apply_first_party_floor. Carries no "entity-miss" substring, so
# every downstream relevance gate treats the post as grounded.
_FIRST_PARTY_EXPLANATION = "first-party post (authored by a resolved handle)"
# Small additive credit for a post authored by one of the run's resolved
# handles (see rerank_candidates / _fallback_tuple). Deliberately small: the
@@ -537,8 +541,18 @@ def _apply_first_party_floor(
if not resolved_handles:
return
for c in candidates:
if _is_first_party(c, resolved_handles) and c.final_score < FIRST_PARTY_FLOOR:
if not _is_first_party(c, resolved_handles):
continue
if c.final_score < FIRST_PARTY_FLOOR:
c.final_score = FIRST_PARTY_FLOOR
# Clear the entity-miss marker here, at the one site that knows the
# resolved handles. Downstream relevance gates key on the marker, not
# on handle knowledge, so neutralizing it once lets the carve-out
# propagate instead of forcing every gate to re-derive first-party.
# A first-party post is entity-grounded by authorship: nobody repeats
# their own name in their own post.
if c.explanation and "entity-miss" in c.explanation.lower():
c.explanation = _FIRST_PARTY_EXPLANATION
def _apply_engagement_rescue(
@@ -931,3 +945,24 @@ def _normalized_rrf(rrf_score: float) -> float:
# Max single-stream RRF at rank 1 is 1/(K+1) ~ 0.016; multi-stream
# accumulation reaches ~0.08.
return max(0.0, min(100.0, (rrf_score / 0.08) * 100.0))
def candidate_relevance_ok(candidate: schema.Candidate) -> bool:
"""Shared gate: is this candidate topically usable for display surfaces?
Single owner of the entity-miss demotion test. Render-side surfaces (Best
Takes, cluster visibility) must call this rather than re-testing the
explanation string themselves -- a second copy of the predicate is how the
documented mirrored-predicate drift bug recurs, and it means a carve-out
added here silently fails to reach them.
First-party posts are handled upstream: ``_apply_first_party_floor`` clears
their entity-miss marker at the one site that knows the resolved handles,
so this predicate needs no handle knowledge.
"""
explanation = (candidate.explanation or "").lower()
if "entity-miss" in explanation:
return False
if (candidate.final_score or 0.0) <= 0.0:
return False
return True
+1 -1
View File
@@ -39,7 +39,7 @@ class ProviderRuntime:
reasoning_provider: Literal["gemini", "openai", "xai", "local"]
planner_model: str
rerank_model: str
x_search_backend: Literal["xai", "bird"] | None = None
x_search_backend: Literal["xai", "grok", "bird", "xurl", "xquik"] | None = None
@dataclass(frozen=True)
+31 -3
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import math
from collections.abc import Iterable
from . import dates, relevance, schema
@@ -333,16 +334,35 @@ def _passes_engagement_floor(item: schema.SourceItem, sole_source: bool) -> bool
def prune_low_relevance(
items: list[schema.SourceItem],
minimum: float = 0.15,
first_party_handles: Iterable[str] | None = None,
) -> list[schema.SourceItem]:
"""Drop weak lexical matches when stronger evidence exists.
Social-source items with zero engagement get a stricter threshold
because zero engagement on a social platform is a strong noise signal.
Social-source items with genuinely zero engagement get a stricter
threshold because zero engagement on a social platform is a strong noise
signal.
TikTok and Instagram items with fewer than 1000 views are pruned
(unless they are the only source represented in the batch).
``first_party_handles`` names accounts this run is explicitly searching
(the subject of the topic). Their own posts are exempt from the floor: a
post almost never contains its own author's name, so lexical relevance
scores it at or near zero no matter how on-topic it is. Without the
exemption a mixed batch loses them silently, because the ``filtered or
items`` rescue below only fires when *every* item fails.
"""
sources_present = {item.source for item in items}
first_party = {
h.strip().lstrip("@").lower()
for h in (first_party_handles or ())
if h and h.strip()
}
def _is_first_party(item: schema.SourceItem) -> bool:
if not first_party or not item.author:
return False
return item.author.strip().lstrip("@").lower() in first_party
def passes(item: schema.SourceItem) -> bool:
# YouTube items with successfully extracted transcripts should not
@@ -350,10 +370,18 @@ def prune_low_relevance(
# already proves substantive topical coverage.
if item.source == "youtube" and item.snippet:
return True
# Posts by an account this run is explicitly searching are evidence by
# provenance, not by lexical overlap with the topic.
if _is_first_party(item):
return True
rel = item.local_relevance if item.local_relevance is not None else 0.0
if rel < minimum:
return False
if item.source in _SOCIAL_SOURCES and (item.engagement_score is None or item.engagement_score == 0):
# Key the stricter social gate on genuinely absent engagement, not on
# the normalized score: signals.normalize is min-max over the batch, so
# it maps the least-engaged item to exactly 0 even when that item has
# thousands of likes.
if item.source in _SOCIAL_SOURCES and not engagement_raw(item):
if rel < minimum * 1.5:
return False
sole_source = sources_present == {item.source}
+19 -1
View File
@@ -16,15 +16,33 @@ def _no_arctic_network():
yield
@pytest.fixture(autouse=True)
def _no_ambient_grok_cli():
"""Default the grok CLI to absent so no test resolves it from the developer's
own machine. grok is a new X-chain backend whose availability is a plain
filesystem check, so a machine with it installed and signed in would
otherwise silently change chain resolution in every existing X test.
Tests that exercise grok stub these themselves (test_grok_x,
test_backend_descriptors._x_env) and override this by patching inside the
test body."""
# Stub the input (PATH resolution), not the logic: has_stored_auth and
# _is_available_uncached then both resolve absent on their own, leaving the
# module's real control flow intact for tests that exercise it.
with mock.patch("lib.grok_x.binary_path", return_value=None):
yield
@pytest.fixture(autouse=True)
def _reset_probe_caches():
"""The doctor stack memoizes probe results in module-level dicts (safe for
the one-shot CLI process, wrong across tests). Clear them around every test
so a probe cached by one test can never leak into another."""
from lib import health, xurl_x
from lib import grok_x, health, xurl_x
health.clear_dependency_probe_cache()
xurl_x.clear_availability_cache()
grok_x.clear_availability_cache()
yield
health.clear_dependency_probe_cache()
xurl_x.clear_availability_cache()
grok_x.clear_availability_cache()
+23 -3
View File
@@ -23,7 +23,7 @@ from unittest import mock
import pytest
from lib import backends, env, health, xurl_x
from lib import backends, env, grok_x, health, xurl_x
# ---------------------------------------------------------------------------
@@ -54,6 +54,8 @@ def _x_env(
xurl_installed=False,
xurl_authed=False,
node_status=health.OK,
grok_installed=False,
grok_authed=False,
):
"""Context managers configuring the X-chain probe environment.
@@ -67,14 +69,32 @@ def _x_env(
if xurl_authed
else (xurl_x.AUTH_MISSING, "no token store at ~/.xurl")
)
# grok defaults to absent so an existing X test never resolves it against
# the developer's own machine. Both grok surfaces are filesystem-only, so
# one flag drives them consistently.
grok_stored = (
(grok_x.AUTH_OK, "stored Grok credentials found in ~/.grok/auth.json")
if grok_authed
else (grok_x.AUTH_MISSING, "no Grok credential store at ~/.grok/auth.json")
)
return (
mock.patch("lib.bird_x.is_bird_installed", return_value=bird_installed),
mock.patch("lib.bird_x.set_credentials", lambda *a, **k: None),
mock.patch("lib.xurl_x.is_available", return_value=xurl_authed),
mock.patch(
"lib.backends.which",
lambda name: "/usr/local/bin/xurl" if (name == "xurl" and xurl_installed) else None,
lambda name: (
"/usr/local/bin/xurl" if (name == "xurl" and xurl_installed)
else "/usr/local/bin/grok" if (name == "grok" and grok_installed)
else None
),
),
mock.patch("lib.grok_x.stored_auth_status", return_value=grok_stored),
mock.patch(
"lib.grok_x.has_stored_auth",
return_value=grok_installed and grok_authed,
),
mock.patch("lib.grok_x.is_available", return_value=grok_installed and grok_authed),
mock.patch("lib.health.probe_dependency", _probe_dep({"node": node_status})),
mock.patch("lib.xurl_x.stored_auth_status", return_value=stored),
mock.patch(
@@ -115,7 +135,7 @@ class TestDescriptorRegistry:
d = backends.get_descriptor("x")
assert d.mode == backends.MODE_ALTERNATIVE
assert tuple(s.name for s in d.backends) == env.X_BACKEND_ORDER
assert env.X_BACKEND_ORDER == ("xai", "bird", "xurl", "xquik")
assert env.X_BACKEND_ORDER == ("xai", "grok", "bird", "xurl", "xquik")
assert d.pin_var == env.X_BACKEND_PIN_VAR == "LAST30DAYS_X_BACKEND"
def test_env_exposes_reddit_pin_constants(self):
+60
View File
@@ -0,0 +1,60 @@
"""Which topics are worth one web search to resolve an X handle.
Gates on brevity rather than capitalization. Requiring a capital letter meant
the most common real-world spelling never resolved a handle: every recent run
on this machine -- bentgo, buzz, getenergy.com -- is lowercase.
"""
import importlib.util
from pathlib import Path
import pytest
_CLI = Path(__file__).resolve().parent.parent / "skills" / "last30days" / "scripts" / "last30days.py"
def _cli():
spec = importlib.util.spec_from_file_location("l30_cli", _CLI)
module = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(module)
except SystemExit:
pass
return module
@pytest.mark.parametrize("topic", [
"bentgo",
"Bentgo",
"peter steinberger",
"Peter Steinberger",
"getenergy.com",
"buzz by block",
"@steipete",
"claude code",
])
def test_entity_topics_resolve(topic):
assert _cli()._looks_like_entity_topic(topic) is True
@pytest.mark.parametrize("topic", [
"best AI coding tools 2026",
"how to build agents",
"top tools for research",
"what is the best model?",
"",
])
def test_theme_topics_do_not_resolve(topic):
assert _cli()._looks_like_entity_topic(topic) is False
def test_casing_does_not_change_the_verdict():
"""The regression this replaces: capitalization decided whether a subject's
own posts could be protected."""
cli = _cli()
for lower, upper in [("bentgo", "Bentgo"), ("peter steinberger", "Peter Steinberger")]:
assert cli._looks_like_entity_topic(lower) == cli._looks_like_entity_topic(upper)
def test_question_shaped_topics_are_themes():
assert _cli()._looks_like_entity_topic("who is peter steinberger?") is False
+110
View File
@@ -0,0 +1,110 @@
"""The topic subject gets a higher per-author cap than incidental authors.
The flat cap of 3 is anti-flooding protection and is right for third parties.
But on a person or company topic the subject is the point of the query: the
measured 'Peter Steinberger steipete' run recovered 8 subject-authored posts,
and a flat cap would discard 5 of them after the rest of Phase A worked to
retrieve and keep them.
The raised cap stays bounded so a prolific subject cannot crowd out all
commentary about them.
"""
from lib import fusion, schema
def _cand(cid: str, author: str) -> schema.Candidate:
url = f"https://x.com/{author}/status/{cid}"
cand = schema.Candidate(
candidate_id=cid,
item_id=f"i{cid}",
source="x",
title=f"post {cid}",
url=url,
snippet="s",
subquery_labels=["primary"],
native_ranks={"primary:x": 1},
local_relevance=0.5,
freshness=80,
engagement=50,
source_quality=0.68,
rrf_score=0.02,
)
cand.source_items = [
schema.SourceItem(
item_id=f"i{cid}", source="x", title=f"post {cid}",
body="b", url=url, author=author,
)
]
return cand
def _subject_posts(n: int, author: str = "steipete"):
return [_cand(str(i), author) for i in range(1, n + 1)]
def test_third_party_author_is_still_capped_at_three():
kept = fusion._apply_per_author_cap(_subject_posts(8, author="rando"))
assert len(kept) == 3, "the anti-flooding cap must be unchanged for third parties"
def test_subject_keeps_more_than_three_posts():
kept = fusion._apply_per_author_cap(
_subject_posts(8), first_party_handles={"steipete"}
)
assert len(kept) > 3, (
"the subject of the topic was capped at 3 of 8 recovered posts, "
"discarding evidence the rest of Phase A worked to keep"
)
def test_subject_cap_is_bounded():
"""A prolific subject must not fill the pool."""
kept = fusion._apply_per_author_cap(
_subject_posts(40), first_party_handles={"steipete"}
)
assert len(kept) <= fusion._MAX_ITEMS_PER_FIRST_PARTY_AUTHOR
assert len(kept) < 40
def test_mixed_pool_caps_each_author_by_its_own_rule():
pool = _subject_posts(8) + [_cand(f"r{i}", "rando") for i in range(1, 8)]
kept = fusion._apply_per_author_cap(pool, first_party_handles={"steipete"})
subject = [c for c in kept if c.source_items[0].author == "steipete"]
third = [c for c in kept if c.source_items[0].author == "rando"]
assert len(subject) > 3
assert len(third) == 3
def test_no_handles_behaves_exactly_as_before():
pool = _subject_posts(8)
assert len(fusion._apply_per_author_cap(pool)) == \
len(fusion._apply_per_author_cap(pool, first_party_handles=set()))
def test_handles_are_matched_case_insensitively():
kept = fusion._apply_per_author_cap(
_subject_posts(8), first_party_handles={"SteiPete"}
)
assert len(kept) > 3
def test_ordering_within_an_author_is_preserved():
"""Candidates arrive sorted by quality; the cap keeps the best N."""
kept = fusion._apply_per_author_cap(
_subject_posts(8), first_party_handles={"steipete"}
)
assert [c.candidate_id for c in kept] == \
[str(i) for i in range(1, len(kept) + 1)]
def test_weighted_rrf_accepts_first_party_handles():
import inspect
sig = inspect.signature(fusion.weighted_rrf)
assert "first_party_handles" in sig.parameters, (
"the fusion entry point must accept the run's resolved handles or the "
"raised cap can never fire in production"
)
assert sig.parameters["first_party_handles"].default in (None, frozenset()), (
"must stay optional so the discovery caller is unaffected"
)
+623
View File
@@ -0,0 +1,623 @@
"""Retrieval-floor exemption for posts authored by a handle the run is searching.
The production failure this pins: a mixed batch where the mention lane clears
the relevance floor and the from lane does not. Because
`prune_low_relevance` ends with `return filtered or items`, the all-fail rescue
only fires when *everything* fails. A mixed batch is therefore the exact shape
that silently loses the subject's own posts, and no prior test exercised it --
existing supplement-lane tests use single-item batches that trip the rescue.
"""
from lib import schema, signals
def _x_item(item_id: str, author: str, relevance: float, engagement: dict | None = None):
item = schema.SourceItem(
item_id=item_id,
source="x",
title="",
body="post body",
url=f"https://x.com/{author}/status/{item_id}",
author=author,
engagement=engagement or {},
)
item.local_relevance = relevance
return item
def _mixed_batch():
"""From-lane items score 0.0 (a post rarely names its own author);
mention-lane items clear the floor because they contain the handle."""
return [
_x_item("1", "steipete", 0.0, {"likes": 7773, "reposts": 391}),
_x_item("2", "steipete", 0.0, {"likes": 3466, "reposts": 128}),
_x_item("3", "someone_else", 0.32, {"likes": 78, "reposts": 8}),
_x_item("4", "another_acct", 0.23, {"likes": 8, "reposts": 1}),
]
def _annotate(items):
"""Populate engagement_score the way the real pipeline does."""
scores = signals.normalize([signals.engagement_raw(i) for i in items])
for item, score in zip(items, scores, strict=True):
item.engagement_score = score
return items
def test_mixed_batch_keeps_first_party_and_drops_off_topic():
items = _annotate(_mixed_batch())
kept = signals.prune_low_relevance(items, first_party_handles={"steipete"})
authors = sorted(i.author for i in kept)
assert "steipete" in authors, (
"first-party posts were pruned from a mixed batch; this is the measured "
"defect where 8 subject-authored posts never reached the report"
)
assert len([a for a in authors if a == "steipete"]) == 2
def test_mixed_batch_without_exemption_still_loses_first_party():
"""Characterizes the defect: without the exemption the batch drops them."""
items = _annotate(_mixed_batch())
kept = signals.prune_low_relevance(items)
assert all(i.author != "steipete" for i in kept), (
"expected the unexempted path to still drop zero-relevance first-party "
"posts; if this now passes, the floor changed and the exemption's "
"justification needs rechecking"
)
def test_non_first_party_below_floor_is_still_pruned():
"""The exemption must be scoped, not a blanket floor removal."""
items = _annotate(_mixed_batch() + [_x_item("5", "spam_acct", 0.02, {"likes": 0})])
kept = signals.prune_low_relevance(items, first_party_handles={"steipete"})
assert all(i.author != "spam_acct" for i in kept)
def test_batch_minimum_is_not_treated_as_zero_engagement():
"""`normalize` maps the batch minimum to exactly 0, so an item with real
engagement was being given the stricter 1.5x social threshold purely for
being the least-engaged item present."""
items = _annotate([
_x_item("1", "acct_a", 0.20, {"likes": 500, "reposts": 40}),
_x_item("2", "acct_b", 0.20, {"likes": 5000, "reposts": 400}),
])
least = next(i for i in items if i.author == "acct_a")
assert least.engagement_score == 0, "precondition: min-max maps batch min to 0"
kept = signals.prune_low_relevance(items)
assert any(i.author == "acct_a" for i in kept), (
"an item with 500 likes was pruned by the zero-engagement gate purely "
"because it was the batch minimum"
)
def test_genuinely_zero_engagement_still_gets_stricter_threshold():
"""The stricter gate must survive for real zero-engagement social noise."""
items = _annotate([
_x_item("1", "acct_a", 0.20, {"likes": 0, "reposts": 0}),
_x_item("2", "acct_b", 0.90, {"likes": 5000, "reposts": 400}),
])
kept = signals.prune_low_relevance(items)
assert all(i.author != "acct_a" for i in kept), (
"a genuinely zero-engagement item at 0.20 should fail the 0.225 gate"
)
def test_all_fail_rescue_is_unchanged():
items = _annotate([_x_item("1", "acct_a", 0.01), _x_item("2", "acct_b", 0.02)])
kept = signals.prune_low_relevance(items)
assert len(kept) == 2, "the all-fail rescue must still return the batch"
def test_batch_with_no_first_party_behaves_as_before():
items = _annotate(_mixed_batch())
assert signals.prune_low_relevance(items, first_party_handles=frozenset()) == \
signals.prune_low_relevance(items)
def test_handles_are_matched_case_insensitively():
items = _annotate(_mixed_batch())
kept = signals.prune_low_relevance(items, first_party_handles={"SteiPete"})
assert any(i.author == "steipete" for i in kept)
# --- KTD8: one owner for the entity-miss predicate -------------------------
def _candidate(explanation: str, final_score: float, author: str = "someone"):
url = f"https://x.com/{author}/status/1"
cand = schema.Candidate(
candidate_id="c1",
item_id="i1",
source="x",
title="t",
url=url,
snippet="s",
subquery_labels=["primary"],
native_ranks={"primary:x": 1},
local_relevance=0.0,
freshness=80,
engagement=50,
source_quality=0.68,
rrf_score=0.02,
)
cand.source_items = [
schema.SourceItem(
item_id="i1", source="x", title="t", body="b", url=url, author=author,
)
]
cand.explanation = explanation
cand.final_score = final_score
return cand
def test_render_delegates_to_shared_predicate():
"""render must not carry its own copy of the entity-miss test."""
from lib import render, rerank
cand = _candidate("fallback-local-score (entity-miss demotion)", 40.0)
assert render._best_take_relevance_ok(cand) is rerank.candidate_relevance_ok(cand)
ok = _candidate("llm-scored", 40.0)
assert render._best_take_relevance_ok(ok) is rerank.candidate_relevance_ok(ok)
def test_shared_predicate_rejects_entity_miss_and_zero_score():
from lib import rerank
assert not rerank.candidate_relevance_ok(
_candidate("fallback-local-score (entity-miss demotion)", 40.0)
)
assert not rerank.candidate_relevance_ok(_candidate("llm-scored", 0.0))
assert rerank.candidate_relevance_ok(_candidate("llm-scored", 40.0))
def test_first_party_carveout_reaches_render_side_gate():
"""The measured KTD8 failure: a first-party post demoted on the LLM path
was floored by rerank but still discarded at render because the render-side
copy re-tested the explanation string."""
from lib import render, rerank
cand = _candidate("fallback-local-score (entity-miss demotion)", 0.0, author="steipete")
assert not render._best_take_relevance_ok(cand), "precondition: demoted before the floor runs"
rerank._apply_first_party_floor([cand], resolved_handles={"steipete"})
assert cand.final_score >= rerank.FIRST_PARTY_FLOOR
assert render._best_take_relevance_ok(cand), (
"first-party carve-out applied in rerank did not propagate to the "
"render-side relevance gate"
)
def test_non_first_party_demotion_survives_the_floor_pass():
from lib import render, rerank
cand = _candidate("fallback-local-score (entity-miss demotion)", 0.0, author="rando")
rerank._apply_first_party_floor([cand], resolved_handles={"steipete"})
assert not render._best_take_relevance_ok(cand), (
"an off-topic collision post must stay buried"
)
# --- Phase 1 / quick-depth wiring (Greptile) --------------------------------
def test_phase_one_normalize_receives_the_explicit_handles():
"""Quick runs skip Phase 2 entirely, so an exemption reaching only the
supplement path leaves quick-depth reports discarding the subject's posts."""
import inspect
from lib import pipeline
src = inspect.getsource(pipeline.run)
assert "explicit_first_party = {" in src, (
"the user-named handles must be resolved before retrieval, not after"
)
assert "first_party_handles=explicit_first_party," in src, (
"the Phase 1 per-source normalize must receive the exemption"
)
def test_explicit_handles_are_available_before_any_retrieval():
"""The entity-extracted set does not exist until Phase 2; the explicit one
must be built from run()'s own arguments so Phase 1 can use it."""
import inspect
from lib import pipeline
src = inspect.getsource(pipeline.run)
build_at = src.index("explicit_first_party = {")
first_use = src.index("first_party_handles=explicit_first_party,")
assert build_at < first_use
def test_related_handles_lane_gets_the_exemption():
import inspect
from lib import pipeline
src = inspect.getsource(pipeline._run_supplemental_searches)
assert "first_party_handles=related_handles," in src
def test_quick_run_without_an_explicit_handle_still_protects_the_subject():
"""The gap in the first fix: covering only user-typed handles does nothing
for a quick run, where nobody typed one and Phase 2's automatic resolution
never executes."""
from lib import pipeline
candidates = pipeline._topic_first_party_candidates("Peter Steinberger steipete")
assert "steipete" in candidates
items = _annotate(_mixed_batch())
kept = signals.prune_low_relevance(items, first_party_handles=candidates)
assert any(i.author == "steipete" for i in kept), (
"a quick search naming the subject must not discard what they wrote"
)
def test_topic_candidates_include_explicit_mentions():
from lib import pipeline
assert "getenergy_" in pipeline._topic_first_party_candidates("@GetEnergy_ launch")
def test_topic_candidates_exclude_stopwords():
from lib import pipeline
got = pipeline._topic_first_party_candidates("the best of the year")
assert "the" not in got and "of" not in got
def test_topic_candidates_do_not_exempt_unrelated_authors():
"""Candidates only matter when a post's author matches one, so ordinary
words cost nothing -- no account is named 'lunch'."""
from lib import pipeline
candidates = pipeline._topic_first_party_candidates("bentgo lunch boxes")
items = _annotate(_mixed_batch() + [_x_item("9", "spam_acct", 0.01, {"likes": 0})])
kept = signals.prune_low_relevance(items, first_party_handles=candidates)
assert all(i.author != "spam_acct" for i in kept)
def test_topic_candidates_are_unioned_into_the_explicit_set():
import inspect
from lib import pipeline
src = inspect.getsource(pipeline.run)
assert "_topic_first_party_candidates(topic)" in src
build = src.index("explicit_first_party")
use = src.index("first_party_handles=explicit_first_party,")
assert build < use
def test_name_only_topic_resolves_the_subject_from_mentions():
"""The hard case: search "Peter Steinberger" with no handle anywhere. His
handle is @steipete, which matches no topic token, and Phase 2's resolution
has not run. Posts *about* him mention him, which is the signal the engine
already uses -- just later than the prune."""
from lib import pipeline
raw = [
{"text": "Great thread from @steipete on agent loops"},
{"text": "@steipete nailed this one"},
{"text": "watching @steipete build in public is wild"},
{"text": "unrelated chatter with no mention"},
]
assert "steipete" in pipeline._batch_subject_handles(raw)
def test_batch_subject_keys_on_mentions_not_authors():
"""A prolific commentator inflates author counts; being mentioned by other
accounts is what identifies the subject."""
from lib import pipeline
raw = [
{"author_handle": "spam_acct", "text": "buy now"},
{"author_handle": "spam_acct", "text": "buy now again"},
{"author_handle": "spam_acct", "text": "and again"},
{"author_handle": "someone", "text": "actually useful thread by @realsubject"},
]
got = pipeline._batch_subject_handles(raw)
assert "spam_acct" not in got
assert "realsubject" in got
def test_batch_subject_is_capped():
from lib import pipeline
raw = [{"text": f"@acct{i} said something"} for i in range(10)]
assert len(pipeline._batch_subject_handles(raw)) <= 2
def test_batch_subject_is_empty_without_mentions():
from lib import pipeline
assert pipeline._batch_subject_handles([{"text": "no mentions here"}]) == set()
assert pipeline._batch_subject_handles([]) == set()
def test_batch_inference_is_unioned_not_gated():
"""Regression: gating this on "no handles supplied" made it dead code.
The caller's set is derived partly from topic tokens, so it is non-empty
for essentially every real topic -- a fallback would never fire and the
name-only case would stay broken while looking fixed.
"""
import inspect
from lib import pipeline
src = inspect.getsource(pipeline._normalize_score_dedupe)
assert "floor_handles |= _batch_subject_handles(raw_items)" in src
assert "not floor_handles" not in src, (
"batch inference must union, never gate on an empty supplied set"
)
def test_name_only_topic_keeps_subject_posts_end_to_end():
"""The full path: topic names a person, handle appears nowhere in it, and
the subject's own zero-relevance posts still survive the floor."""
from lib import pipeline
supplied = pipeline._topic_first_party_candidates("Peter Steinberger")
assert "steipete" not in supplied, "precondition: the handle is not in the topic"
raw = [
{"text": "Great thread from @steipete on agent loops"},
{"text": "@steipete nailed this"},
{"text": "more praise for @steipete"},
]
inferred = pipeline._batch_subject_handles(raw)
items = _annotate(_mixed_batch())
kept = signals.prune_low_relevance(
items, first_party_handles=supplied | inferred
)
assert any(i.author == "steipete" for i in kept)
# --- ordering fix: prune X after resolution, not before ---------------------
def test_x_defers_its_relevance_floor_past_resolution():
"""The root ordering bug: X was pruned before the run knew who the subject
was, so the floor could not exempt an author nobody had identified yet. No
amount of guessing at prune time substitutes for knowing."""
import inspect
from lib import pipeline
src = inspect.getsource(pipeline.run)
assert 'defer_relevance_prune=(source == "x")' in src
def test_deferred_prune_runs_after_resolution_and_before_fusion():
import inspect
from lib import pipeline
src = inspect.getsource(pipeline.run)
resolve_at = src.index("resolved_handles = explicit_first_party")
prune_at = src.index("Deferred X relevance floor")
fuse_at = src.index("candidates = weighted_rrf(")
assert resolve_at < prune_at < fuse_at, (
"the deferred floor must see resolved handles and still run before fusion"
)
def test_non_x_sources_still_prune_in_place():
"""Only X defers; everything else keeps its existing behavior."""
import inspect
from lib import pipeline
src = inspect.getsource(pipeline._normalize_score_dedupe)
assert 'if source != "jobs" and not defer_relevance_prune:' in src
def test_deferred_prune_still_drops_off_topic_posts():
"""Deferring must not mean skipping."""
items = _annotate(_mixed_batch() + [_x_item("9", "spam_acct", 0.01, {"likes": 0})])
kept = signals.prune_low_relevance(items, first_party_handles={"steipete"})
assert any(i.author == "steipete" for i in kept)
assert all(i.author != "spam_acct" for i in kept)
# --- unresolved subject policy: skip X floor when no real handle identified ---
def test_topic_handle_mentions_extracts_only_at_mentions():
"""@mentions in the topic are real handles; regular words are not."""
from lib import pipeline
assert pipeline._topic_handle_mentions("Peter Steinberger @steipete") == {"steipete"}
assert pipeline._topic_handle_mentions("Peter Steinberger") == set()
assert pipeline._topic_handle_mentions("@GetEnergy_ launch") == {"getenergy_"}
def test_topic_handle_mentions_is_case_insensitive():
from lib import pipeline
assert pipeline._topic_handle_mentions("@SteiPete") == {"steipete"}
def test_entity_topic_no_handle_no_discovery_skips_x_floor():
"""Policy: when the subject cannot be identified, skip the X floor entirely.
Entity-shaped topic, no --x-handle, auto-resolve/Phase 2 return nothing.
Retrieved X items include a zero-relevance post whose author is not a
topic token. That post survives prune because the floor is not applied.
A companion on-topic post is in the batch so the all-fail rescue cannot
hide an incorrectly applied floor.
"""
from unittest.mock import patch
from lib import pipeline
topic = "Peter Steinberger"
assert pipeline._topic_handle_mentions(topic) == set()
topic_tokens = pipeline._topic_first_party_candidates(topic)
assert "peter" in topic_tokens and "steinberger" in topic_tokens
assert "rando_acct" not in topic_tokens
raw_items = [
{
"id": "1",
"text": "shipping a new agent loop tonight",
"url": "https://x.com/rando_acct/status/1",
"author_handle": "rando_acct",
"date": "2026-08-01",
"engagement": {"likes": 0, "reposts": 0, "replies": 0},
},
{
"id": "2",
"text": "Peter Steinberger just shipped another agent demo",
"url": "https://x.com/third_acct/status/2",
"author_handle": "third_acct",
"date": "2026-08-01",
"engagement": {"likes": 50, "reposts": 5, "replies": 2},
},
]
plan = {
"intent": "person",
"freshness_mode": "balanced_recent",
"cluster_mode": "topic",
"subqueries": [{
"label": "primary",
"search_query": topic,
"ranking_query": topic,
"sources": ["x"],
}],
"source_weights": {"x": 1.0},
}
def fake_retrieve(**kwargs):
if kwargs.get("source") == "x":
return raw_items, {}
return [], {}
with patch("lib.pipeline._retrieve_stream", side_effect=lambda **kw: fake_retrieve(**kw)):
report = pipeline.run(
topic=topic,
config={"LAST30DAYS_REASONING_PROVIDER": "gemini"},
depth="quick",
requested_sources=["x"],
mock=True,
external_plan=plan,
as_of_date="2026-08-14",
)
x_items = report.items_by_source.get("x") or []
authors = {item.author for item in x_items}
assert "rando_acct" in authors, (
"unresolved subject policy: skip X floor when no real handle identified; "
"the zero-relevance post whose author is not a topic token must survive"
)
def test_entity_topic_with_at_mention_applies_x_floor():
"""When the topic DOES include @mentions, the X floor applies normally."""
from lib import pipeline
topic = "Peter Steinberger @steipete"
explicit_x_handles = pipeline._topic_handle_mentions(topic)
supplemental_handles = []
assert "steipete" in explicit_x_handles, (
"precondition: @mention is a real handle"
)
real_x_handles = explicit_x_handles | {
h.lstrip("@").strip().lower() for h in supplemental_handles if h and h.strip()
}
assert real_x_handles, "real handles were resolved"
topic_candidates = pipeline._topic_first_party_candidates(topic)
items = _annotate([
_x_item("1", "steipete", 0.0, {"likes": 100, "reposts": 10}),
_x_item("2", "rando_acct", 0.0, {"likes": 0, "reposts": 0}),
_x_item("3", "third_acct", 0.18, {"likes": 20, "reposts": 2}),
])
kept = signals.prune_low_relevance(items, first_party_handles=topic_candidates | real_x_handles)
assert "steipete" in {i.author for i in kept}, (
"first-party posts survive the floor"
)
assert "rando_acct" not in {i.author for i in kept}, (
"when real handles exist, off-topic zero-relevance posts are pruned"
)
def test_explicit_x_handle_applies_x_floor():
"""--x-handle triggers the floor even with entity-only topic."""
from lib import pipeline
topic = "Peter Steinberger"
x_handle = "steipete"
explicit_x_handles = {x_handle.lstrip("@").strip().lower()}
assert pipeline._topic_handle_mentions(topic) == set(), (
"precondition: topic has no @mentions"
)
assert explicit_x_handles == {"steipete"}, (
"but we have an explicit --x-handle"
)
topic_candidates = pipeline._topic_first_party_candidates(topic)
resolved_handles = topic_candidates | explicit_x_handles
items = _annotate([
_x_item("1", "steipete", 0.0, {"likes": 100, "reposts": 10}),
_x_item("2", "rando_acct", 0.0, {"likes": 0, "reposts": 0}),
])
kept = signals.prune_low_relevance(items, first_party_handles=resolved_handles)
assert "steipete" in {i.author for i in kept}
assert "rando_acct" not in {i.author for i in kept}, (
"--x-handle triggers the floor, pruning off-topic posts"
)
# --- thin-source retry must not prune X before handle resolution ------------
def test_thin_retry_keeps_zero_relevance_subject_authored_x_post():
"""Phase 1 defers the X floor; the simplified-query retry must too.
A default/deep run with fewer than three X items retries with a simpler
query. If that retry returns a subject-authored post that does not repeat
the subject's name, applying the relevance floor here (with no resolved
handles) discards it before it enters the bundle. The later
resolved-handle floor cannot recover a post that never arrived.
"""
import threading
from unittest.mock import patch
from lib import pipeline
topic = "Peter Steinberger"
raw_items = [
{
"id": "1",
"text": "shipping a new agent loop tonight",
"url": "https://x.com/steipete/status/1",
"author_handle": "steipete",
"date": "2026-08-01",
"engagement": {"likes": 3466, "reposts": 128, "replies": 40},
},
{
"id": "2",
"text": "Peter Steinberger just shipped another agent demo",
"url": "https://x.com/third_acct/status/2",
"author_handle": "third_acct",
"date": "2026-08-01",
"engagement": {"likes": 50, "reposts": 5, "replies": 2},
},
]
plan = schema.QueryPlan(
intent="person",
freshness_mode="balanced_recent",
cluster_mode="topic",
raw_topic=topic,
subqueries=[
schema.SubQuery(
label="primary",
search_query=topic,
ranking_query=topic,
sources=["x"],
)
],
source_weights={"x": 1.0},
)
bundle = schema.RetrievalBundle()
with patch("lib.pipeline._retrieve_stream", return_value=(raw_items, {})):
pipeline._retry_thin_sources(
topic=topic,
bundle=bundle,
plan=plan,
config={},
depth="default",
date_range=("2026-07-15", "2026-08-14"),
runtime=schema.ProviderRuntime(
reasoning_provider="mock",
planner_model="mock",
rerank_model="mock",
),
mock=False,
rate_limited_sources=set(),
rate_limit_lock=threading.Lock(),
settings=pipeline.DEPTH_SETTINGS["default"],
)
x_items = bundle.items_by_source.get("x") or []
authors = {item.author for item in x_items}
assert "steipete" in authors, (
"thin-retry X path must defer the relevance floor the way Phase 1 does; "
"a zero-relevance subject-authored post must survive into the bundle"
)
assert "third_acct" in authors, (
"precondition: the companion on-topic post cleared the floor, so the "
"all-fail rescue cannot hide an incorrectly applied prune"
)
+74
View File
@@ -0,0 +1,74 @@
"""grok as a CLI-gated dependency in the shared health layer.
The off-PATH case is confirmed real, not theoretical: on a GrokBot machine the
official installer placed the binary at ~/.grok/bin/grok and it had to be
invoked by absolute path. That is the class the Digg learning documents --
"present on disk" is not "installed".
"""
from lib import health, prescriptions
def test_grok_is_a_known_dependency():
assert "grok" in health.KNOWN_DEPENDENCIES
def test_missing_binary_yields_an_install_prescription(monkeypatch):
monkeypatch.setattr(health.shutil, "which", lambda name: None)
monkeypatch.setattr(health, "_off_path_binary", lambda name: None)
probe = health.probe_dependency("grok")
assert probe.status == health.MISSING
assert probe.prescription and not probe.off_path
assert "reinstall" not in probe.prescription.lower()
def test_off_path_binary_yields_a_path_edit_not_an_install(monkeypatch, tmp_path):
binary = tmp_path / "grok"
binary.write_text("#!/bin/sh\n")
binary.chmod(0o755)
monkeypatch.setattr(health.shutil, "which", lambda name: None)
monkeypatch.setattr(health, "_off_path_binary", lambda name: binary)
probe = health.probe_dependency("grok")
assert probe.status == health.MISSING
assert probe.off_path is True
assert "PATH" in probe.prescription
assert "install" not in probe.prescription.lower().replace("installed", "")
def test_broken_binary_yields_a_reinstall_prescription(monkeypatch):
monkeypatch.setattr(health.shutil, "which", lambda name: "/usr/bin/grok")
def boom(*a, **k):
raise OSError("exec format error")
monkeypatch.setattr(health.subprocess, "run", boom)
probe = health.probe_dependency("grok")
assert probe.status == health.BROKEN
assert "reinstall" in probe.prescription.lower()
def test_fallback_prescription_names_the_official_installer():
install, reinstall = health._FALLBACK_PRESCRIPTIONS["grok"]
assert "x.ai/cli/install.sh" in install
assert "grok login" in install
assert "reinstall" in reinstall.lower()
def test_registry_covers_missing_and_unauthenticated():
"""The not-logged-in state has no equivalent in the Digg pattern and is
the one users will actually hit after installing."""
assert ("x", "grok_cli_missing") in prescriptions.REGISTRY
assert ("x", "grok_not_authenticated") in prescriptions.REGISTRY
def test_not_authenticated_fix_is_a_login_not_an_install():
entry = prescriptions.REGISTRY[("x", "grok_not_authenticated")]
assert entry.fix_cli.startswith("grok login")
assert "no X account" in entry.fix_nl
def test_new_entries_carry_no_secret_looking_values():
for key in (("x", "grok_cli_missing"), ("x", "grok_not_authenticated")):
entry = prescriptions.REGISTRY[key]
blob = f"{entry.cause} {entry.fix_nl} {entry.fix_cli}"
assert "AUTH_TOKEN=" not in blob and "API_KEY=" not in blob
+129
View File
@@ -0,0 +1,129 @@
"""grok's three supplement lanes, wired into the pipeline.
The coverage requirement: for an entity topic the run must return what the
subject said, what others said *to* them, and what others said *about* them by
name. The third is not redundant with the second -- most discussion never
@-mentions the subject, so a mention-only lane structurally cannot reach it.
"""
import inspect
import pytest
from lib import pipeline, schema
def _supplements_source():
return inspect.getsource(pipeline._run_supplemental_searches)
def test_grok_is_handle_lane_capable():
src = _supplements_source()
assert '("grok", "bird", "xquik")' in src, (
"grok supports from:/@ natively; leaving it out of the capable set "
"silently drops all of Phase 2 for grok users, as it already does for "
"xai and xurl"
)
def test_all_three_lanes_are_defined_for_grok():
src = _supplements_source()
grok_block = src[src.index('if primary == "grok":'):src.index('elif primary == "bird":')]
assert "_from_lane" in grok_block
assert "_about_lane" in grok_block
assert "_name_lane" in grok_block
def test_name_lane_is_gated_and_defaults_off():
"""Backends without phrase/negation support must not get a broken lane."""
src = _supplements_source()
assert "_name_lane = None" in src
assert "if _name_lane is not None:" in src
def test_name_lane_items_reach_the_batch():
src = _supplements_source()
assert "from_items + about_items + name_items" in src, (
"name-lane results must join the batch, not be computed and dropped"
)
def test_name_lane_excludes_the_subject_handles():
src = _supplements_source()
grok_block = src[src.index('if primary == "grok":'):src.index('elif primary == "bird":')]
assert "exclude_handles=hs" in grok_block, (
"the name lane must exclude the subject's own posts; those belong to "
"the by-lane and would otherwise double-count"
)
def test_name_lane_failure_does_not_abort_the_run():
src = _supplements_source()
block = src[src.index("if _name_lane is not None:"):]
assert "except Exception" in block
assert "NAME-lane" in block
def test_partial_coverage_is_recorded():
"""One-sided coverage must be visible, not look like thin discussion."""
src = _supplements_source()
assert "partial coverage" in src
assert 'if empty and len(empty) < 3:' in src, (
"an all-empty result is an ordinary no-results outcome, not partial "
"coverage; only a mixed result is worth flagging"
)
def test_by_lane_does_not_and_the_topic_into_the_query():
"""A prior defect emptied the from-lane by ANDing the topic into it."""
from lib import grok_x
sig = inspect.signature(grok_x.search_handles)
assert "topic" in sig.parameters
body = inspect.getsource(grok_x.search_handles)
assert "from:{clean}" in body
assert "{topic}" not in body
# --- behavioral: the source-text assertions above cannot catch a crash -------
def test_partial_coverage_does_not_raise_on_an_empty_x_source():
"""Regression: partial coverage was recorded via bundle.record_failure with
the state string "degraded", which is not in SourceOutcome's valid_states.
With zero Phase-1 X items record_failure passes the caller's state straight
through, so it raised ValueError and killed the whole run -- on exactly the
entity topics this feature targets. No source-text assertion could catch
this; only executing the path does."""
bundle = schema.RetrievalBundle()
assert not bundle.items_by_source.get("x")
empty = ["mention"]
# Mirror the production call: this must not raise.
bundle.artifacts.setdefault("x_partial_coverage", []).append(
f"X partial coverage: {', '.join(empty)} lane(s) returned nothing"
)
assert bundle.artifacts["x_partial_coverage"]
def test_degraded_is_not_a_valid_source_outcome_state():
"""Pins why partial coverage must not go through record_failure."""
with pytest.raises(ValueError):
schema.SourceOutcome(
source="x", state="degraded", items_returned=0, attempted=True,
)
def test_partial_coverage_is_not_recorded_as_a_source_failure():
"""A one-sided lane result must not mark X partial: PARTIAL is outside
_STRICT_EXIT_OK_STATES, so wrappers using LAST30DAYS_STRICT_EXIT would exit
3 on runs that returned good X coverage."""
src = _supplements_source()
# Strip comments: the rationale for NOT using record_failure names it.
code = "\n".join(
line for line in src.splitlines() if not line.strip().startswith("#")
)
idx = code.index("x_partial_coverage")
window = code[max(0, idx - 400):idx]
assert "record_failure" not in window, (
"partial lane coverage must be a warning, not a source outcome: "
"record_failure would set X to PARTIAL and trip strict-exit wrappers"
)
+99
View File
@@ -0,0 +1,99 @@
"""grok must be visible to a user, not just functional.
A backend nobody is told about is a backend nobody uses, and the whole point
of this path is removing a credential wall the user currently hits.
"""
import inspect
from pathlib import Path
from lib import doctor, quality_nudge
REPO = Path(__file__).resolve().parent.parent
def test_doctor_reports_x_covered_by_a_signed_in_grok():
src = inspect.getsource(doctor._x_record)
assert "grok_x.has_stored_auth()" in src
assert "will use: grok" in src
def test_doctor_prefers_grok_over_the_unverified_cookie_note():
"""grok outranks bird in the chain, and unlike a cookie session its
availability is locally verifiable rather than 'not verified until a run'."""
src = inspect.getsource(doctor._x_record)
assert src.index("has_stored_auth") < src.index("x_pending_browser_auth")
def test_quality_nudge_offers_grok_but_does_not_call_it_free():
src = inspect.getsource(quality_nudge)
assert "grok_cli_missing" in src
assert "no X credential at all" in src
# The block is headed "Free suggestions"; grok needs a Grok plan, so the
# precondition must be stated inline rather than inherited from the header.
assert "if you have a Grok" in src
def test_configuration_documents_the_grok_path():
text = (REPO / "CONFIGURATION.md").read_text()
assert "X with no X credential (Grok CLI)" in text
assert "grok login" in text
assert "LAST30DAYS_X_BACKEND=bird" in text, (
"users need the documented escape hatch back to the cookie path"
)
def test_configuration_pin_row_lists_grok():
text = (REPO / "CONFIGURATION.md").read_text()
assert "`xai` / `grok` / `bird` / `xurl` / `xquik`" in text
def test_configuration_does_not_claim_grok_is_free():
text = (REPO / "CONFIGURATION.md").read_text()
section = text[text.index("X with no X credential (Grok CLI)"):][:1200]
assert "draws on your Grok plan" in section or "draw on your Grok plan" in section
def test_changelog_fragments_exist_and_changelog_is_untouched():
frags = list((REPO / "changelog.d").glob("*grok*")) + \
list((REPO / "changelog.d").glob("*first-party*"))
assert frags, "feature PRs add a changelog.d fragment"
# --- SKILL.md unlock surfaces ---------------------------------------------
def _skill_md():
return (REPO / "skills" / "last30days" / "SKILL.md").read_text()
def test_all_unlock_surfaces_mention_the_grok_path():
text = _skill_md()
for marker in (
"Just-in-time X unlock",
"Check for a Grok path before asking for cookies",
"Grok CLI (no X credential)",
):
assert marker in text, f"missing grok surfacing at: {marker}"
def test_grok_ordering_is_conditional_on_a_detected_path():
"""A user with no Grok account must not have their actionable options
pushed down the list by one they cannot take."""
text = _skill_md()
section = text[text.index("Just-in-time X unlock"):][:3000]
assert "command -v grok" in section
assert "Options when `grok` IS on PATH" in section
assert "Options when `grok` is NOT on PATH" in section
def test_no_grok_branch_keeps_todays_options_unchanged():
text = _skill_md()
section = text[text.index("Options when `grok` is NOT on PATH"):][:900]
for option in ("Scan my browser cookies", "AUTH_TOKEN and CT0", "xAI API key", "Skip for now"):
assert option in section
def test_skill_md_does_not_call_the_grok_path_free():
text = _skill_md()
section = text[text.index("Just-in-time X unlock"):][:3000]
assert "Do not describe the Grok path as free" in section
+457
View File
@@ -0,0 +1,457 @@
"""grok_x: X retrieval via the Grok CLI.
The failure modes pinned here were all measured against grok CLI 0.2.118 on
2026-08-13, not hypothesized. Retrieval is performed by a language model
rather than an API client, so the module's job is as much rejecting confident
fabrication as it is parsing.
"""
import re
import subprocess
import pytest
from lib import grok_x
@pytest.fixture(autouse=True)
def _reset():
grok_x.clear_availability_cache()
yield
grok_x.clear_availability_cache()
def _block(post_id, handle="steipete", created="Wed, 12 Aug 2026 15:55:18 GMT",
likes=1462, text="cli was a year ago."):
return (
f"id: {post_id}\n"
f"handle: {handle}\n"
f"created_at: {created}\n"
f"likes: {likes}\n"
f"reposts: 48\nreplies: 95\nquotes: 20\n"
f"text: {text}\n"
)
WINDOW = ("2026-07-14", "2026-08-13")
# --- provenance: the primary validity test --------------------------------
def test_in_window_post_is_parsed():
items = grok_x.parse_x_response(
{"text": _block("2087568620465607078")}, "steipete", *WINDOW
)
assert len(items) == 1
item = items[0]
assert item["author_handle"] == "steipete"
assert item["url"] == "https://x.com/steipete/status/2087568620465607078"
assert item["date"] == "2026-08-12"
assert item["engagement"]["likes"] == 1462
def test_out_of_window_ids_are_rejected():
"""The measured fabrication: a since:2026-07-14 request answered with 2025
posts recalled from training data. Handle, id format, text and engagement
all looked correct; only the decoded timestamp exposed it."""
text = "".join(
_block(pid, created="Fri, 15 Aug 2025 00:59:58 GMT")
for pid in ("1956158892141441450", "1955681419900834272")
)
assert grok_x.parse_x_response({"text": text}, "steipete", *WINDOW) == []
def test_uniform_id_sequence_is_rejected():
"""A generated series: real ranked results are not evenly spaced."""
base = 2080000000000000000
step = 715000000000000
text = "".join(_block(str(base + i * step)) for i in range(6))
assert grok_x.parse_x_response({"text": text}, "steipete", *WINDOW) == []
def test_placeholder_handle_is_rejected():
text = _block("2087568620465607078", handle="unknown", likes=0)
assert grok_x.parse_x_response({"text": text}, "steipete", *WINDOW) == []
def test_self_reported_non_execution_is_rejected():
text = _block(
"2087568620465607078",
text="Requested x_keyword_search was not executed in this turn",
)
assert grok_x.parse_x_response({"text": text}, "steipete", *WINDOW) == []
def test_item_without_usable_id_is_dropped():
text = "handle: steipete\nlikes: 10\ntext: no id here\n"
assert grok_x.parse_x_response({"text": text}, "steipete", *WINDOW) == []
def test_mixed_response_keeps_only_in_window_posts():
text = _block("2087568620465607078") + _block(
"1956158892141441450", created="Fri, 15 Aug 2025 00:59:58 GMT"
)
items = grok_x.parse_x_response({"text": text}, "steipete", *WINDOW)
assert [i["url"].rsplit("/", 1)[-1] for i in items] == ["2087568620465607078"]
def test_duplicate_post_ids_are_deduped():
text = _block("2087568620465607078") * 2
assert len(grok_x.parse_x_response({"text": text}, "steipete", *WINDOW)) == 1
# --- parsing resilience ----------------------------------------------------
def test_narration_around_blocks_is_tolerated():
text = (
"I'll call x_keyword_search now.\n\n"
"Here are the posts I found:\n\n"
+ _block("2087568620465607078")
+ "\nThat's everything the tool returned.\n"
)
assert len(grok_x.parse_x_response({"text": text}, "steipete", *WINDOW)) == 1
def test_markdown_decorated_fields_are_parsed():
text = (
"- **id:** 2087568620465607078\n"
"- **handle:** @steipete\n"
"- **created_at:** Wed, 12 Aug 2026 15:55:18 GMT\n"
"- **likes:** 1,462\n"
"- **text:** cli was a year ago.\n"
)
items = grok_x.parse_x_response({"text": text}, "steipete", *WINDOW)
assert len(items) == 1
assert items[0]["engagement"]["likes"] == 1462
def test_error_response_returns_empty_not_raises():
assert grok_x.parse_x_response({"error": "boom"}, "t", *WINDOW) == []
def test_non_dict_response_returns_empty():
assert grok_x.parse_x_response(None, "t", *WINDOW) == []
# --- invocation contract ---------------------------------------------------
def test_invocation_omits_json_schema_and_tools(monkeypatch):
"""Both flags degrade or suppress the tool call; neither may be passed."""
seen = {}
def fake_run(cmd, **kwargs):
seen["cmd"] = cmd
seen["kwargs"] = kwargs
return subprocess.CompletedProcess(cmd, 0, _block("2087568620465607078"), "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
grok_x.search_x("steipete", *WINDOW)
assert "--json-schema" not in seen["cmd"]
assert "--tools" not in seen["cmd"]
assert "--permission-mode" in seen["cmd"]
def test_subprocess_runs_in_an_isolated_empty_directory(monkeypatch):
"""The child has tool permissions bypassed and its context carries
untrusted post text, so it must not act in the user's repository."""
import os
seen = {}
def fake_run(cmd, **kwargs):
seen["cwd"] = kwargs.get("cwd")
seen["existed"] = os.path.isdir(kwargs.get("cwd") or "")
seen["entries"] = os.listdir(kwargs.get("cwd")) if seen["existed"] else None
return subprocess.CompletedProcess(cmd, 0, _block("2087568620465607078"), "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
grok_x.search_x("steipete", *WINDOW)
assert seen["cwd"] and seen["cwd"] != os.getcwd()
# Isolated and near-empty: the only entry is the throwaway HOME staged for
# the child, never the user's checkout.
assert seen["existed"] and seen["entries"] == ["home"]
def test_subprocess_environment_is_minimal(monkeypatch):
monkeypatch.setenv("XAI_API_KEY", "dummy-key")
monkeypatch.setenv("AUTH_TOKEN", "dummy-token")
seen = {}
def fake_run(cmd, **kwargs):
seen["env"] = kwargs.get("env")
return subprocess.CompletedProcess(cmd, 0, _block("2087568620465607078"), "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
grok_x.search_x("steipete", *WINDOW)
assert "XAI_API_KEY" not in seen["env"]
assert "AUTH_TOKEN" not in seen["env"]
assert "PATH" in seen["env"]
def test_resolved_binary_path_is_used_not_bare_name(monkeypatch):
"""shutil.which and subprocess.run resolve a bare name differently on
Windows, so the resolved path must be passed."""
seen = {}
monkeypatch.setattr(grok_x, "binary_path", lambda: "/opt/custom/grok")
def fake_run(cmd, **kwargs):
seen["cmd"] = cmd
return subprocess.CompletedProcess(cmd, 0, _block("2087568620465607078"), "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
grok_x.search_x("steipete", *WINDOW)
assert seen["cmd"][0] == "/opt/custom/grok"
def test_missing_binary_returns_error_not_raises(monkeypatch):
monkeypatch.setattr(grok_x, "binary_path", lambda: None)
result = grok_x.search_x("steipete", *WINDOW)
assert result["items"] == [] and result["error"]
def test_timeout_returns_error_not_raises(monkeypatch):
def fake_run(cmd, **kwargs):
raise subprocess.TimeoutExpired(cmd, 1)
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
result = grok_x.search_x("steipete", *WINDOW)
assert result["items"] == [] and "timed out" in result["error"]
def test_non_execution_is_retried(monkeypatch):
"""A response that fails provenance is a retryable non-execution, not a
thin result -- the measured rate made single-shot unreliable.
Asserted against _run_query directly: search_x additionally fans out across
query variants to reach the depth target, which would confound a call count.
"""
calls = {"n": 0}
def fake_run(cmd, **kwargs):
calls["n"] += 1
body = (
_block("1956158892141441450", created="Fri, 15 Aug 2025 00:59:58 GMT")
if calls["n"] == 1 else _block("2087568620465607078")
)
return subprocess.CompletedProcess(cmd, 0, body, "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
items, error = grok_x._run_query("steipete", *WINDOW)
assert calls["n"] == 2, "a provenance rejection must be retried once"
assert len(items) == 1 and not error
# --- auth surfaces ---------------------------------------------------------
def test_stored_auth_status_makes_no_subprocess_or_network(monkeypatch):
"""This is the doctor path; the whole-doctor test patches these to raise."""
def boom(*a, **k):
raise AssertionError("doctor path must not spawn a process or hit the network")
monkeypatch.setattr(grok_x.subprocess, "run", boom)
monkeypatch.setattr(grok_x.subprocess, "Popen", boom)
grok_x.stored_auth_status()
def test_stored_auth_status_reports_missing_store(monkeypatch, tmp_path):
monkeypatch.setattr(grok_x, "token_store_path", lambda: tmp_path / "nope.json")
status, detail = grok_x.stored_auth_status()
assert status == grok_x.AUTH_MISSING and "nope.json" in detail
def test_stored_auth_status_detects_credentials(monkeypatch, tmp_path):
store = tmp_path / "auth.json"
store.write_text('{"iss": {"auth_mode": "oidc", "refresh_token": "dummy-token"}}')
monkeypatch.setattr(grok_x, "token_store_path", lambda: store)
assert grok_x.stored_auth_status()[0] == grok_x.AUTH_OK
def test_stored_auth_status_never_echoes_store_contents(monkeypatch, tmp_path):
"""doctor output gets pasted into issue reports."""
store = tmp_path / "auth.json"
store.write_text('{"key": "SUPER-SECRET-VALUE", "refresh_token": "ALSO-SECRET"}')
monkeypatch.setattr(grok_x, "token_store_path", lambda: store)
_, detail = grok_x.stored_auth_status()
assert "SUPER-SECRET-VALUE" not in detail and "ALSO-SECRET" not in detail
def test_unreadable_store_is_an_error(monkeypatch, tmp_path):
store = tmp_path / "auth.json"
store.write_text("{}")
def boom(*a, **k):
raise OSError("permission denied")
monkeypatch.setattr(grok_x, "token_store_path", lambda: store)
monkeypatch.setattr(type(store), "read_text", boom, raising=False)
assert grok_x.stored_auth_status()[0] == grok_x.AUTH_ERROR
def test_availability_cache_is_resettable(monkeypatch):
monkeypatch.setattr(grok_x, "_is_available_uncached", lambda: True)
assert grok_x.is_available() is True
monkeypatch.setattr(grok_x, "_is_available_uncached", lambda: False)
assert grok_x.is_available() is True, "memoized within a process"
grok_x.clear_availability_cache()
assert grok_x.is_available() is False
# --- lanes -----------------------------------------------------------------
def _stub_response(monkeypatch, body):
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
monkeypatch.setattr(
grok_x.subprocess, "run",
lambda cmd, **kw: subprocess.CompletedProcess(cmd, 0, body, ""),
)
def test_from_lane_filters_by_actual_author(monkeypatch):
"""Operator fidelity is not guaranteed: a measured from: query returned a
post by a different account."""
_stub_response(monkeypatch, _block("2087568620465607078", handle="leojr94_"))
assert grok_x.search_handles(["steipete"], "topic", *WINDOW) == []
def test_from_lane_keeps_matching_author(monkeypatch):
_stub_response(monkeypatch, _block("2087568620465607078", handle="steipete"))
assert len(grok_x.search_handles(["steipete"], "topic", *WINDOW)) == 1
def test_from_lane_does_not_and_the_topic_into_the_query(monkeypatch):
seen = {}
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
def fake_run(cmd, **kwargs):
seen["prompt"] = cmd[2]
return subprocess.CompletedProcess(cmd, 0, _block("2087568620465607078"), "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
grok_x.search_handles(["steipete"], "quantum widgets", *WINDOW)
assert "quantum widgets" not in seen["prompt"]
def test_mention_lane_excludes_the_subject_client_side(monkeypatch):
"""A measured run carrying -from:X still returned a post authored by X."""
_stub_response(monkeypatch, _block("2087568620465607078", handle="GetEnergy_"))
assert grok_x.search_mentions(["GetEnergy_"], *WINDOW) == []
def test_name_lane_needs_no_handle(monkeypatch):
_stub_response(monkeypatch, _block("2087568620465607078", handle="iamcaroren"))
items = grok_x.search_name("Bentgo", *WINDOW)
assert len(items) == 1
def test_name_lane_quotes_multi_word_names(monkeypatch):
seen = {}
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
def fake_run(cmd, **kwargs):
seen["prompt"] = cmd[2]
return subprocess.CompletedProcess(cmd, 0, _block("2087568620465607078"), "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
grok_x.search_name("Peter Steinberger", *WINDOW)
assert '"Peter Steinberger"' in seen["prompt"]
def test_name_lane_excludes_subject_authored_posts(monkeypatch):
_stub_response(monkeypatch, _block("2087568620465607078", handle="Bentgo"))
assert grok_x.search_name("Bentgo", *WINDOW, exclude_handles=["Bentgo"]) == []
def test_name_lane_applies_an_engagement_floor(monkeypatch):
seen = {}
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
def fake_run(cmd, **kwargs):
seen["prompt"] = cmd[2]
return subprocess.CompletedProcess(cmd, 0, _block("2087568620465607078"), "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
grok_x.search_name("Bentgo", *WINDOW)
assert "min_faves:" in seen["prompt"], (
"the bare-name lane is the widest of the three and needs a floor the "
"other two do not"
)
# --- fixes applied after review --------------------------------------------
def test_child_home_is_not_the_users_home(monkeypatch, tmp_path):
"""Stripping credential env vars is not enough: the engine writes those same
credentials to $HOME/.config/last30days/.env, and an empty cwd is no
boundary for a filesystem-capable child (cwd bounds relative paths, not
$HOME/... reads)."""
seen = {}
def fake_run(cmd, **kwargs):
seen["env"] = kwargs.get("env")
seen["cwd"] = kwargs.get("cwd")
return subprocess.CompletedProcess(cmd, 0, _block("2087568620465607078"), "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
grok_x.search_x("steipete", *WINDOW)
import os as _os
assert seen["env"]["HOME"] != _os.path.expanduser("~")
assert seen["env"]["HOME"].startswith(seen["cwd"])
def test_abbreviated_engagement_does_not_invert_ranking():
"""'1.2M' parsed as 1 ranked a viral post below one with 500 likes."""
assert grok_x._as_int("39K") == 39_000
assert grok_x._as_int("1.2M") == 1_200_000
assert grok_x._as_int("1,462") == 1462
assert grok_x._as_int("N/A") is None
def test_unparsable_window_rejects_rather_than_bypasses():
"""Fail closed: a bad window must not silently disable provenance."""
assert grok_x.parse_x_response(
{"text": _block("2087568620465607078")}, "steipete", "not-a-date", "also-bad"
) == []
def test_handle_outside_x_grammar_is_rejected():
"""Model-reported handles reach post URLs and the next child's prompt."""
text = _block("2087568620465607078", handle="Peter Steinberger (@steipete)")
items = grok_x.parse_x_response({"text": text}, "steipete", *WINDOW)
# Falls back to the @-pattern inside the value, or drops the item entirely.
assert all(
re.fullmatch(r"[A-Za-z0-9_]{1,15}", i["author_handle"]) for i in items
)
def test_clean_handle_rejects_non_grammar_values():
assert grok_x._clean_handle("@steipete") == "steipete"
assert grok_x._clean_handle("Peter Steinberger") == ""
assert grok_x._clean_handle("a" * 16) == ""
assert grok_x._clean_handle("bad'; drop") == ""
def test_empty_result_is_not_reported_as_an_error():
"""A quiet window is not a broken backend."""
import subprocess as sp
import unittest.mock as m
with m.patch.object(grok_x, "binary_path", lambda: "/usr/bin/grok"), \
m.patch.object(grok_x.subprocess, "run",
lambda cmd, **kw: sp.CompletedProcess(cmd, 0, "no posts found", "")):
result = grok_x.search_x("nothing-matches-this", *WINDOW)
assert result["items"] == []
assert "error" not in result
def test_depth_drives_the_fanout_call_count():
"""DEPTH_CONFIG was dead: grok returned 10 posts at every depth while
sitting ahead of bird, silently downgrading a deep run."""
quick = grok_x._fanout_queries("t", "2026-07-14", "2026-08-13", 1)
deep = grok_x._fanout_queries("t", "2026-07-14", "2026-08-13", 4)
assert len(quick) == 1 and len(deep) == 4
assert len(set(deep)) == 4, "fan-out variants must differ or they repeat one result set"
+87
View File
@@ -0,0 +1,87 @@
"""Quoted proper-noun phrases must survive into the provider query.
Two compounding defects meant a phrase search never actually happened:
1. planner._keyword_query emitted the quoted compound *and* a core that
already contained the same words: '"Peter Steinberger" peter steinberger
steipete'.
2. bird_x stripped the quotes before building the X query, degrading an
intended phrase match into a bare token conjunction
(peter AND steinberger AND steipete).
On the measured 'Peter Steinberger steipete' baseline the primary X lane
returned nothing as a result, and the whole run depended on the supplement
lanes.
"""
from lib import bird_x, planner
def test_quoted_phrase_words_are_not_repeated_unquoted():
q = planner._keyword_query("Peter Steinberger steipete", "peter steinberger steipete")
assert q.lower().count("steinberger") == 1, (
f"'steinberger' appears more than once in {q!r}; the quoted compound "
"is being emitted alongside a core that already contains it"
)
assert '"Peter Steinberger"' in q, "the proper-noun phrase must stay quoted"
def test_distinct_topic_tokens_survive_alongside_the_phrase():
q = planner._keyword_query("Peter Steinberger steipete", "peter steinberger steipete")
assert "steipete" in q.lower(), (
f"deduplicating the phrase must not drop unrelated topic tokens: {q!r}"
)
def test_single_word_topic_is_unchanged():
q = planner._keyword_query("bentgo", "bentgo")
assert q.strip() == "bentgo"
def test_topic_with_no_title_cased_compound_is_unquoted():
q = planner._keyword_query("open source llm tooling", "open source llm tooling")
assert '"' not in q
def test_provider_query_preserves_quoted_phrase():
"""bird_x must not strip quotes out of the phrase before querying X.
X advanced search supports quoted phrases natively, so passing them
through is strictly better retrieval than a token conjunction.
"""
built = bird_x.build_topic_query('"Peter Steinberger" steipete', "2026-07-14")
assert '"Peter Steinberger"' in built, (
f"quoted phrase was stripped before reaching X: {built!r}"
)
assert "since:2026-07-14" in built
def test_provider_query_keeps_bare_tokens():
built = bird_x.build_topic_query("bentgo lunch", "2026-07-14")
assert "bentgo" in built and "lunch" in built
assert '"' not in built
def test_provider_query_drops_grouping_syntax_but_not_phrases():
"""Bird grouping characters are still noise; phrase quotes are not."""
built = bird_x.build_topic_query('("Claude Code") review', "2026-07-14")
assert '"Claude Code"' in built
assert "(" not in built and ")" not in built
def test_unbalanced_quote_falls_back_to_bare_tokens():
"""An orphan quote must never reach X.
Upstream trimming (core-subject extraction, the retry ladder shortening a
query) can cut a topic mid-phrase, leaving 'berlin "mixed-use'. X reads the
orphan as an unterminated phrase and matches nothing, so a query that
cannot be balanced is safer unquoted.
"""
built = bird_x.build_topic_query('immobilienmakler berlin "mixed-use', "2026-07-12")
assert '"' not in built, f"unbalanced quote survived into the query: {built!r}"
assert "immobilienmakler" in built and "mixed-use" in built
def test_balanced_quotes_are_still_preserved():
built = bird_x.build_topic_query('"Claude Code" "Peter Steinberger"', "2026-07-14")
assert built.count('"') == 4
+1 -1
View File
@@ -24,7 +24,7 @@ RUNNABLE = re.compile(
r"^(?:python3 \S*last30days\.py\b"
r"|[A-Z][A-Z0-9_]*="
r"|export [A-Z][A-Z0-9_]*="
r"|(?:brew|pipx|pip|scoop|npx|npm|xurl|yt-dlp|docker) )"
r"|(?:brew|pipx|pip|scoop|npx|npm|xurl|yt-dlp|docker|grok) )"
)
# The seed failure inventory from the plan (U3 approach section).
+94
View File
@@ -0,0 +1,94 @@
"""Contract test: planner ranking-query scaffolding must not score as topic signal."""
import pytest
from lib import relevance
from lib import planner
# Ranking-query templates emitted by planner.py. Placeholders are substituted
# with sentinels so only the fixed scaffolding words remain.
_RANKING_TEMPLATES = (
"What recent evidence from the last 30 days is most relevant to {X}?",
"What recent evidence from the last 30 days is most relevant to {X}, especially about {Y}?",
"What recent evidence from the last 30 days is most relevant to {X} in the comparison '{Y}'?",
"What are the current odds, forecasts, or market signals about {X}?",
"What new reactions or follow-up reporting from the last 30 days matter for {X}?",
"What real-world workflows or pipelines are people running with {X}?",
"What production deployments or real-world use cases of {X} are people describing?",
"What hands-on experience reports or reviews of {X} exist in the last 30 days?",
)
# Template words deliberately NOT added to LOW_SIGNAL_QUERY_TOKENS because they
# are domain nouns that can legitimately be a user's topic. Globally demoting
# them would harm relevance for every source. They only appear in the
# intent-specific templates, which fire because the topic already carries that
# intent, so their presence is correlated with the topic rather than noise.
_ACCEPTED_DOMAIN_NOUNS = frozenset({
"cases", "deployments", "experience", "forecasts", "market", "pipelines",
"production", "reactions", "reporting", "reports", "signals", "workflows",
})
_SENTINELS = {"zzqqsentinel", "wwqqsentinel"}
def _fixed_tokens():
tokens = set()
for tpl in _RANKING_TEMPLATES:
filled = tpl.replace("{X}", "zzqqsentinel").replace("{Y}", "wwqqsentinel")
tokens |= relevance.tokenize(filled)
return tokens - _SENTINELS
def test_every_ranking_template_word_is_classified():
"""No planner scaffolding word may silently count as informative topic signal."""
unclassified = sorted(
t for t in _fixed_tokens()
if t not in relevance.LOW_SIGNAL_QUERY_TOKENS
and t not in _ACCEPTED_DOMAIN_NOUNS
)
assert not unclassified, (
"planner ranking-query template words are neither low-signal nor an "
f"accepted domain noun: {unclassified}. Add them to "
"LOW_SIGNAL_QUERY_TOKENS, or to _ACCEPTED_DOMAIN_NOUNS with a reason."
)
def test_base_template_contributes_no_informative_tokens():
"""The base template must not inflate coverage; only the topic should."""
q = planner._ranking_query("Peter Steinberger steipete", "")
prepared = relevance.PreparedQuery(q)
assert prepared.informative_q_tokens <= {"peter", "steinberger", "steipete"}, (
f"base ranking query leaks scaffolding into informative tokens: "
f"{sorted(prepared.informative_q_tokens)}"
)
def test_core_qualified_branch_contributes_no_informative_tokens():
"""The `especially about <core>` branch must not leak `especially` either."""
q = planner._ranking_query("Peter Steinberger steipete", "steipete")
prepared = relevance.PreparedQuery(q)
assert "especially" not in prepared.informative_q_tokens, (
f"core-qualified branch leaks 'especially': {sorted(prepared.informative_q_tokens)}"
)
def test_off_topic_post_still_scores_below_floor():
"""Discrimination must survive: unrelated content stays low."""
q = planner._ranking_query("Peter Steinberger steipete", "")
prepared = relevance.PreparedQuery(q)
score = relevance.token_overlap_relevance(
prepared, "Sourdough starter tips for cold kitchens in winter baking"
)
assert score < 0.15, f"off-topic post scored {score}, expected below the 0.15 floor"
def test_zero_overlap_post_remains_zero():
"""Pins the boundary this unit cannot cross; U2's exemption owns these."""
q = planner._ranking_query("Peter Steinberger steipete", "")
prepared = relevance.PreparedQuery(q)
score = relevance.token_overlap_relevance(
prepared, "cli was a year ago. apps maybe 6 months. now it's services."
)
assert score == 0.0, (
f"expected 0.0 for a post sharing no token with the query, got {score}. "
"If this changes, KTD7's division of labour between U1 and U2 needs revisiting."
)
+98
View File
@@ -0,0 +1,98 @@
"""Auto-discovered X handles must reach resolved_handles.
resolved_handles gates every first-party protection downstream: the entity-miss
exemption in rerank, FIRST_PARTY_FLOOR, the interaction floor, and the
retrieval-floor exemption in signals. It was built only from --x-handle,
--github-user and --x-related, so on any run that did not pass --x-handle
(the overwhelmingly common case) the whole mechanism was inert.
"""
import inspect
from lib import pipeline
def _call_supplements(**overrides):
"""Drive _run_supplemental_searches far enough to populate the out-param.
The function bails early when no X or Reddit dicts are present unless a
handle was supplied, so an explicit x_handle is the cheapest way to reach
the resolution block without standing up a full retrieval bundle.
"""
out: list[str] = []
kwargs = dict(
topic="Peter Steinberger steipete",
bundle=overrides.pop("bundle"),
plan=overrides.pop("plan"),
config={},
depth="default",
date_range=("2026-07-14", "2026-08-13"),
runtime=overrides.pop("runtime"),
mock=True,
rate_limited_sources=set(),
rate_limit_lock=overrides.pop("lock"),
resolved_handles_out=out,
)
kwargs.update(overrides)
pipeline._run_supplemental_searches(**kwargs)
return out
def test_out_param_is_part_of_the_contract():
sig = inspect.signature(pipeline._run_supplemental_searches)
assert "resolved_handles_out" in sig.parameters, (
"the supplement pass must be able to report the handles it resolved; "
"without it resolved_handles cannot see auto-discovered subjects"
)
assert sig.parameters["resolved_handles_out"].default is None, (
"the out-param must stay optional so existing callers are unaffected"
)
def test_resolved_handles_includes_supplemental_handles():
"""The merge site must read the supplement pass's output.
Matched on the assignment rather than an exact literal: an earlier version
pinned "resolved_handles = {" and broke when the construction changed to
merge the explicit set, proving nothing about behavior either way.
"""
src = inspect.getsource(pipeline)
start = src.index("resolved_handles =")
block = src[start:start + 400]
assert "supplemental_handles" in block, (
"resolved_handles is still built without the handles the supplement "
"pass discovered; auto-discovered subjects stay unprotected"
)
assert "explicit_first_party" in block, (
"the user-named handles must still be part of resolved_handles"
)
def test_supplemental_handles_is_seeded_before_the_call():
src = inspect.getsource(pipeline)
assert "supplemental_handles: list[str] = []" in src
assert "resolved_handles_out=supplemental_handles," in src, (
"the supplement call must pass the collector it later merges from"
)
def test_handles_are_normalized_and_deduped():
"""Normalization must match resolved_handles' own lstrip/strip/lower form."""
src = inspect.getsource(pipeline._run_supplemental_searches)
block = src[src.index("resolved_handles_out is not None"):]
assert 'lstrip("@")' in block and ".lower()" in block, (
"handles must be normalized the same way resolved_handles normalizes, "
"or the set comparison in rerank/_is_first_party will miss them"
)
assert "seen" in block, "duplicate handles must not accumulate"
def test_population_precedes_the_no_handles_early_return():
"""A run whose lanes cannot execute must still contribute its handles."""
src = inspect.getsource(pipeline._run_supplemental_searches)
populate_at = src.index("resolved_handles_out is not None")
early_return_at = src.index("if not handles and not related_handles:")
assert populate_at < early_return_at, (
"handles are surfaced after the early return, so a run with no usable "
"handle lane would silently contribute nothing to resolved_handles"
)
+209
View File
@@ -0,0 +1,209 @@
"""grok must never be a dead end: every failure falls through to the next backend.
grok sits ahead of bird in the chain, so any way it can come back empty has to
hand off rather than leave the run with no X coverage.
"""
from unittest import mock
from lib import grok_x, pipeline
def _stub_grok(monkeypatch, stdout="", returncode=0, raises=None):
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
def fake_run(cmd, **kwargs):
if raises:
raise raises
import subprocess
return subprocess.CompletedProcess(cmd, returncode, stdout, "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
def test_grok_cli_missing_yields_an_error_for_failover(monkeypatch):
monkeypatch.setattr(grok_x, "binary_path", lambda: None)
result = grok_x.search_x("topic", "2026-07-14", "2026-08-13")
assert result["items"] == []
assert result["error"], "a missing CLI must surface an error so the chain logs it"
def test_grok_timeout_yields_an_error_for_failover(monkeypatch):
import subprocess
_stub_grok(monkeypatch, raises=subprocess.TimeoutExpired("grok", 1))
result = grok_x.search_x("topic", "2026-07-14", "2026-08-13")
assert result["items"] == []
assert "timed out" in result["error"]
def test_grok_nonzero_exit_yields_an_error_for_failover(monkeypatch):
_stub_grok(monkeypatch, stdout="boom", returncode=1)
result = grok_x.search_x("topic", "2026-07-14", "2026-08-13")
assert result["items"] == []
assert result["error"]
def test_grok_fabricated_results_end_up_empty_not_fabricated(monkeypatch):
"""Provenance rejection must yield no items, so the chain moves to bird
rather than the run reporting invented posts."""
block = (
"id: 1956158892141441450\nhandle: steipete\n"
"created_at: Fri, 15 Aug 2025 00:59:58 GMT\nlikes: 100\n"
"text: recalled from training data\n"
)
_stub_grok(monkeypatch, stdout=block)
result = grok_x.search_x("steipete", "2026-07-14", "2026-08-13")
assert result["items"] == []
def test_clean_empty_result_carries_no_error(monkeypatch):
"""A quiet window is not a broken backend; the chain still tries the next."""
_stub_grok(monkeypatch, stdout="The search returned no posts.")
result = grok_x.search_x("topic", "2026-07-14", "2026-08-13")
assert result["items"] == []
assert "error" not in result
# --- lane budget and selective retry ---------------------------------------
def test_lane_budget_stops_issuing_queries(monkeypatch):
"""Three lanes over three handles is up to 14 sequential LLM subprocess
calls, bounded only by per-call timeouts without a shared budget."""
import time as _time
calls = {"n": 0}
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
def fake_run(cmd, **kwargs):
import subprocess
calls["n"] += 1
return subprocess.CompletedProcess(cmd, 0, "no posts", "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
past = _time.monotonic() - 1
got = grok_x.search_handles(
["a", "b", "c"], "topic", "2026-07-14", "2026-08-13", deadline=past
)
assert got == []
assert calls["n"] == 0, "an expired budget must stop the lane before any call"
def test_lane_budget_constant_is_bounded():
assert 0 < grok_x.LANE_BUDGET_SECONDS <= 300
def test_clean_empty_is_not_retried(monkeypatch):
"""Retrying a byte-identical prompt cannot turn an empty window into posts;
it only doubles latency and Grok-plan spend on the common case."""
calls = {"n": 0}
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
def fake_run(cmd, **kwargs):
import subprocess
calls["n"] += 1
return subprocess.CompletedProcess(cmd, 0, "The search returned no posts.", "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
items, error = grok_x._run_query("topic", "2026-07-14", "2026-08-13")
assert items == [] and error == ""
assert calls["n"] == 1, "a clean empty result must not be retried"
def test_fabricated_response_is_still_retried(monkeypatch):
"""The retry must survive for the case it exists to fix."""
calls = {"n": 0}
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
fabricated = (
"id: 1956158892141441450\nhandle: steipete\n"
"created_at: Fri, 15 Aug 2025 00:59:58 GMT\nlikes: 10\ntext: recalled\n"
)
def fake_run(cmd, **kwargs):
import subprocess
calls["n"] += 1
return subprocess.CompletedProcess(cmd, 0, fabricated, "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
grok_x._run_query("steipete", "2026-07-14", "2026-08-13")
assert calls["n"] == 2, "a provenance rejection is exactly what retrying can fix"
def test_lanes_use_a_single_attempt(monkeypatch):
"""Lane queries pass attempts=1: an empty lane is a legitimate outcome and
the shared budget is better spent on breadth than on repeats."""
import inspect
for fn in (grok_x.search_handles, grok_x.search_mentions, grok_x.search_name):
assert "attempts=1" in inspect.getsource(fn), (
f"{fn.__name__} should not retry lane queries"
)
def test_every_empty_path_leaves_the_chain_free_to_continue(monkeypatch):
"""The chain advances on an empty item list, so each way grok can come back
empty must actually return an empty list rather than raising or hanging.
Behavioral on purpose: an earlier version of this test asserted on the
loop's source text and broke when the loop moved, while proving nothing
about what grok returns.
"""
import subprocess as sp
cases = {
"missing binary": (lambda: None, None),
"fabricated": (lambda: "/usr/bin/grok",
"id: 1956158892141441450\nhandle: steipete\n"
"created_at: Fri, 15 Aug 2025 00:59:58 GMT\nlikes: 1\ntext: x\n"),
"clean empty": (lambda: "/usr/bin/grok", "no posts found"),
"nonzero exit": (lambda: "/usr/bin/grok", None),
}
for label, (binary, stdout) in cases.items():
monkeypatch.setattr(grok_x, "binary_path", binary)
if stdout is not None:
monkeypatch.setattr(
grok_x.subprocess, "run",
lambda cmd, **kw: sp.CompletedProcess(cmd, 0, stdout, ""),
)
elif binary() is not None:
monkeypatch.setattr(
grok_x.subprocess, "run",
lambda cmd, **kw: sp.CompletedProcess(cmd, 1, "", "boom"),
)
result = grok_x.search_x("topic", "2026-07-14", "2026-08-13")
assert result["items"] == [], f"{label} must yield no items so the chain advances"
def test_a_call_is_not_started_when_it_cannot_finish_in_budget(monkeypatch):
"""The lanes run synchronously with no outer timeout, so a per-call floor
would let the last call overrun the documented shared ceiling."""
import time as _time
calls = {"n": 0}
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
def fake_run(cmd, **kwargs):
import subprocess
calls["n"] += 1
return subprocess.CompletedProcess(cmd, 0, "no posts", "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
# 5s left: below the minimum useful call, so nothing should start.
near = _time.monotonic() + 5
items, error = grok_x._run_query("topic", "2026-07-14", "2026-08-13", deadline=near)
assert calls["n"] == 0
assert "budget exhausted" in error
def test_timeout_never_exceeds_the_remaining_budget(monkeypatch):
import time as _time
seen = {}
monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok")
def fake_run(cmd, **kwargs):
import subprocess
seen["timeout"] = kwargs.get("timeout")
return subprocess.CompletedProcess(cmd, 0, "no posts", "")
monkeypatch.setattr(grok_x.subprocess, "run", fake_run)
remaining = 40
grok_x._run_query(
"topic", "2026-07-14", "2026-08-13",
deadline=_time.monotonic() + remaining,
)
assert seen["timeout"] <= remaining