feat(WARDEN-WP-0020): worker drafts real route answers in dry-run (T3 groundwork)

build_plans now computes the concrete routing answer for each route_answer action
in-process (reuses the catalog; read-only, no subprocess/network) and render_plans
shows it as a `draft:` line. The dry-run demonstrates the actual answer the executor
(T3) will send, not just an intent. RuleBrain stays the default; the llm-connect brain
(T2) is gated on llm-connect being operational + its /execute contract.

230 tests, lint clean. Live dry-run verified against the real inbox.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 22:42:54 +02:00
parent 706674d784
commit 4287eccc80
2 changed files with 45 additions and 2 deletions

View File

@@ -149,9 +149,43 @@ class HubClient:
return data if isinstance(data, list) else []
def draft_route_answer(query: str) -> str:
"""Compute the routing answer the worker would send for a query. Read-only.
Reuses the routing catalog in-process (no subprocess, no network) so the dry-run
shows the concrete answer the executor (T3) will send, not just an intent.
"""
try:
from warden.routing.catalog import load_catalog
matches = load_catalog().find(query, limit=1)
except Exception: # noqa: BLE001 — never let a lookup failure break planning
return ""
if not matches:
return f"No routing match for {query!r}; try `warden route list --all`."
e = matches[0]
role = "issue" if e.warden_executes else ("assist" if e.exec_capable else "route")
parts = [f"{e.id} — owner {e.owner_repo} ({e.subsystem}), warden role: {role}."]
if e.warden_executes and e.cert_command:
parts.append(f"Run: {e.cert_command}.")
elif e.has_native_exec:
parts.append(f"Primary: {e.exec_command}.")
elif e.exec_capable:
parts.append(f"Proxy: warden access {e.id} --fetch (as the caller).")
parts.append(f"See {e.wiki_ref}.")
return " ".join(parts)
def build_plans(messages: List[dict], brain: Brain) -> List[WorkerPlan]:
"""Plan every message and apply the guardrail pass. Pure — no execution."""
return [_guardrail(brain.plan(m), m) for m in messages]
"""Plan every message, attach computed route answers, and apply the guardrail pass."""
plans: List[WorkerPlan] = []
for m in messages:
plan = brain.plan(m)
for a in plan.actions:
if a.kind == "route_answer" and "answer" not in a.payload:
a.payload["answer"] = draft_route_answer(a.payload.get("query", m.get("subject", "")))
plans.append(_guardrail(plan, m))
return plans
def render_plans(plans: List[WorkerPlan]) -> str:
@@ -167,6 +201,8 @@ def render_plans(plans: List[WorkerPlan]) -> str:
for a in p.actions:
mark = "" if a.risk == "safe" else ""
lines.append(f" {mark} {a.kind}: {a.summary}")
if a.payload.get("answer"):
lines.append(f" draft: {a.payload['answer']}")
if a.risk == "escalate":
lines.append(f" escalated: {a.reason}")
return "\n".join(lines)