feat(robomp): added repo-scoped issue search to issue triage flow

- Added `search_issues` support to the GitHub backend and client, including `state_reason` and `is_pull_request` in issue summaries.
- Added the `gh_search_issues` host tool with repo-prefixed query handling, non-empty/restricted `repo:` validation, default and bounded `limit` values, and inbound issue filtering.
- Added proxy integration for issue search with a new `/gh/v1/search_issues` endpoint and matching proxy-client method/response parsing.
- Updated triage prompts to perform pre-`classify_issue` duplicate and already-fixed checks via search, and added tests for search query formatting, validation, and state-aware match rendering.
This commit is contained in:
can1357
2026-07-15 00:30:15 +02:00
parent 539c132094
commit 3e6ae36c7c
12 changed files with 257 additions and 19 deletions
+70
View File
@@ -1256,6 +1256,75 @@ def _build_fetch_thread(bindings: ToolBindings) -> HostTool[Any, Any]:
)
# ---------- gh_search_issues ----------
_REPO_QUALIFIER_RE = re.compile(r"(?i)\brepo:")
def _build_search_issues(bindings: ToolBindings) -> HostTool[Any, Any]:
"""Read-only issue/PR search scoped to the current repo.
Exists so triage can find duplicates and already-merged fixes instead of
classifying blind; the inbound issue itself is filtered out of results.
"""
def execute(args: dict[str, Any], _ctx: HostToolContext[Any]) -> str:
query = args.get("query")
if not isinstance(query, str) or not query.strip():
msg = "gh_search_issues requires a non-empty 'query'."
_audit(bindings, "gh_search_issues", args, error=msg)
_raise_command(msg)
query = query.strip()
if _REPO_QUALIFIER_RE.search(query):
msg = "gh_search_issues scopes to the current repo automatically; drop the 'repo:' qualifier."
_audit(bindings, "gh_search_issues", args, error=msg)
_raise_command(msg)
limit_raw = args.get("limit")
limit = max(1, min(int(limit_raw), 20)) if isinstance(limit_raw, int) else 10
try:
results = _run_coro(
bindings.loop,
bindings.github.search_issues(bindings.repo.full_name, query, limit=limit),
)
except GitHubError as exc:
_audit(bindings, "gh_search_issues", args, error=str(exc))
_raise_command(f"GitHub search failed: {exc.status} {exc.message}")
results = [s for s in results if s.is_pull_request or s.number != bindings.issue.number]
if not results:
_audit(bindings, "gh_search_issues", args, result={"matches": 0})
return f"No issues or PRs in {bindings.repo.full_name} match {query!r}."
lines = [f"# {len(results)} match(es) for {query!r} in {bindings.repo.full_name}"]
for s in results:
kind = "PR" if s.is_pull_request else "issue"
state = f"{s.state} ({s.state_reason})" if s.state_reason else s.state
labels = f" [{', '.join(s.labels)}]" if s.labels else ""
lines.append(
f"- #{s.number} ({kind}, {state}) {s.title} — @{s.author}, updated {s.updated_at[:10]}{labels}"
)
_audit(bindings, "gh_search_issues", args, result={"matches": len(results)})
return "\n".join(lines)
return host_tool(
name="gh_search_issues",
description=persona.host_tool_description("gh_search_issues"),
parameters={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": persona.host_tool_parameter_description("gh_search_issues", "query"),
},
"limit": {
"type": "integer",
"description": persona.host_tool_parameter_description("gh_search_issues", "limit"),
},
},
"required": ["query"],
"additionalProperties": False,
},
execute=execute,
)
_PRIMARY_TYPES = ("bug", "enhancement", "question", "proposal", "documentation", "wontfix", "invalid", "duplicate")
_AUTO_PR_CLASSIFICATIONS = frozenset({"bug", "documentation"})
_PRIORITIES = ("prio:p0", "prio:p1", "prio:p2", "prio:p3")
@@ -1835,6 +1904,7 @@ def build(bindings: ToolBindings) -> tuple[HostTool[Any, Any], ...]:
_build_mark_unable(bindings),
_build_abort_task(bindings),
_build_fetch_thread(bindings),
_build_search_issues(bindings),
)