From 6441b05888fded3fc223db2893322974f5ec9db3 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:15:55 -0700 Subject: [PATCH] fix(skills): keep xurl SKILL self-contained; move x_search routing to gated surfaces Follow-up on the salvaged commit: the xurl skill loads even when x_search isn't registered (check_fn-gated on xAI credentials), so per the cross-toolset reference rule the skill must not name it. Replaced the skill's x_search routing block and workflow step with skill-native wording (raw engageable posts, authenticated context, write-evidence rule). The cross-surface comparison stays on surfaces where both are known to exist: x-search feature docs, toolset description, tools-config setup note, and the x_search tool schema (kept generic, no tool names). Rewrote the routing tests to pin the placement contract, including that the skill never names credential-gated surfaces. --- skills/social-media/xurl/SKILL.md | 17 ++---- tests/skills/test_xurl_x_search_routing.py | 69 ++++++++++------------ 2 files changed, 37 insertions(+), 49 deletions(-) diff --git a/skills/social-media/xurl/SKILL.md b/skills/social-media/xurl/SKILL.md index 9033a21a7dd..a784e2a21a8 100644 --- a/skills/social-media/xurl/SKILL.md +++ b/skills/social-media/xurl/SKILL.md @@ -1,7 +1,7 @@ --- name: xurl description: "X/Twitter via xurl CLI: raw post search, posting, DM, media." -version: 1.1.2 +version: 1.1.3 author: xdevplatform + openclaw + Hermes Agent license: MIT platforms: [linux, macos] @@ -28,14 +28,6 @@ Use this skill for: - raw access to any X API v2 endpoint - multi-app / multi-account workflows -If Hermes also exposes the `x_search` tool, route by intent: - -- Use `x_search` for read-only public X discovery: "what are people saying", current reactions, public claims, broad semantic search, and synthesized answers with citations. -- Use `xurl` for exact or authenticated X API work: post, reply, quote, delete, like, repost, bookmark, follow, block, mute, DM, media upload, timeline, mentions, account-specific reads, or raw v2 endpoints. -- For mixed workflows, use `x_search` to discover candidate public posts, then use `xurl read` or another exact `xurl` command only after the target post/user/action is clear. -- Never treat an `x_search` answer as evidence that an X write happened. For state-changing X actions, only the `xurl` command output or the X API response proves the action. -- Prefer `x_search` over `xurl search` when the user asks for broad public discussion and does not need X API-exact results or authenticated account context. - This skill replaces the older `xitter` skill (which wrapped a third-party Python CLI). `xurl` is maintained by the X developer platform team, supports OAuth 2.0 PKCE with auto-refresh, and covers a substantially larger API surface. --- @@ -399,13 +391,14 @@ xurl --app staging /2/users/me # one-off against staging ## Agent Workflow 1. Verify prerequisites: `xurl --help` and `xurl auth status`. -2. Before using `xurl search`, check intent. If the user needs broad public X discovery and the `x_search` tool is available, use `x_search` instead. Continue with `xurl` when the task needs an exact API read, authenticated account context, or any X write action. +2. Before using `xurl search`, check intent. Reach for it when the task needs actual post objects, authenticated account context, or leads into an X write action — it is the right surface when the user wants posts they can engage with, not just a summary of a topic. 3. **Check default app has credentials.** Parse the `auth status` output. The default app is marked with `▸`. If the default app shows `oauth2: (none)` but another app has a valid oauth2 user, tell the user to run `xurl auth default ` to fix it. This is the most common setup mistake — the user added an app with a custom name but never set it as default, so xurl keeps trying the empty `default` profile. 4. If auth is missing entirely, stop and direct the user to the "One-Time User Setup" section — do NOT attempt to register apps or pass secrets yourself. 5. Start with a cheap read (`xurl whoami`, `xurl user @handle`, `xurl search ... -n 3`) to confirm reachability. 6. Confirm the target post/user and the user's intent before any write action (post, reply, like, repost, DM, follow, block, delete). -7. Use JSON output directly — every response is already structured. -8. Never paste `~/.xurl` contents back into the conversation. +7. Only the `xurl` command output (or the raw X API response) proves that a state-changing X action happened. Never report a write as done based on any other source — search results, summaries, or prior context. +8. Use JSON output directly — every response is already structured. +9. Never paste `~/.xurl` contents back into the conversation. --- diff --git a/tests/skills/test_xurl_x_search_routing.py b/tests/skills/test_xurl_x_search_routing.py index c8e9c7c685d..e7dd3768087 100644 --- a/tests/skills/test_xurl_x_search_routing.py +++ b/tests/skills/test_xurl_x_search_routing.py @@ -1,7 +1,16 @@ """Behavioral contract for xurl / x_search routing guidance. -These tests assert structural invariants (required topics + mutual exclusivity -of responsibility), not frozen prose snapshots. +These tests assert structural invariants (required topics + placement of the +routing guidance), not frozen prose snapshots. + +Placement contract (July 2026): +- The xurl SKILL must NOT name `x_search` (or any other credential-gated + surface): the skill loads even when that tool isn't registered, so it must + describe its own search distinctively in its own terms (raw, engageable + post objects as the authenticated account). +- Cross-surface routing guidance lives where both surfaces are known to + exist together: the x_search feature docs, the toolset description, and + the tools-config setup note. """ from pathlib import Path @@ -21,45 +30,31 @@ def _contains_any(text: str, *needles: str) -> bool: return any(n.lower() in lowered for n in needles) -def test_xurl_skill_routes_by_intent_not_interchangeably(): +def test_xurl_skill_never_names_credential_gated_surfaces(): + """The skill must be self-contained: no cross-references to tools that + may not be registered (x_search is check_fn-gated on xAI credentials).""" + lowered = _read(XURL_SKILL).lower() + assert "x_search" not in lowered + assert "web_search" not in lowered + + +def test_xurl_skill_search_is_distinct_standalone(): + """Search must be described so an agent can route correctly even when + another X search surface exists — raw engageable posts, authenticated.""" text = _read(XURL_SKILL) - lowered = text.lower() - - # Both surfaces named so agents can choose by capability. - assert "x_search" in lowered - assert "xurl" in lowered - - # x_search is discovery / read-only public research. - assert _contains_any(text, "read-only public", "public x discovery", "broad public") - # xurl owns authenticated / write / exact API work. - assert _contains_any( - text, - "authenticated", - "exact or authenticated", - "exact api", - "account actions", - "write action", - ) - # Writes must not be evidenced by x_search answers. - assert _contains_any( - text, - "never treat an `x_search` answer", - "never evidence", - "proves the action", - "x api response", - ) - # Prefer x_search over xurl search for broad public discovery when available. - assert "x_search" in lowered and "xurl search" in lowered - assert _contains_any(text, "prefer `x_search`", "use `x_search` instead", "route by intent") + assert _contains_any(text, "raw post") + assert _contains_any(text, "authenticated") + assert _contains_any(text, "engage", "engageable") + # Distinguish from synthesized-answer surfaces in xurl's own terms. + assert _contains_any(text, "summarized answer", "summary of a topic") -def test_xurl_agent_workflow_prefers_x_search_for_broad_discovery(): +def test_xurl_skill_write_evidence_rule(): + """State-changing X actions are proven only by xurl output / X API + response — never by search results or summaries.""" text = _read(XURL_SKILL) - # Workflow must preflight intent before xurl search. - assert "xurl search" in text.lower() - assert _contains_any(text, "check intent", "before using `xurl search`") - assert _contains_any(text, "broad public", "public x discovery") - assert _contains_any(text, "write action", "authenticated account", "exact api") + assert _contains_any(text, "proves that a state-changing", "proves the action") + assert _contains_any(text, "never report a write", "never treat") def test_x_search_doc_separates_discovery_from_account_actions():