diff --git a/scripts/lib/planner.py b/scripts/lib/planner.py index 49b2a20..63ab0a9 100644 --- a/scripts/lib/planner.py +++ b/scripts/lib/planner.py @@ -113,17 +113,23 @@ def plan_query( topic, available_sources, requested_sources, depth, note=f"fallback-plan (LLM error: {type(exc).__name__})", ) - # No provider configured and no --plan was passed. The deterministic - # fallback path produces weaker plans than the LLM or the --plan path - # (narrower subquery breadth, no semantic expansion). Surface that - # so Claude Code callers know to pass --plan with a JSON plan they - # generate themselves. See 2026-04-19 Hermes Agent Use Cases failure. + # No --plan was passed and no engine-internal provider is configured. + # The deterministic fallback path produces weaker plans than either the + # --plan path or the engine-internal LLM planner path. Surface this so + # the HOSTING reasoning model (Claude Code, Codex, Hermes, Gemini, etc.) + # realizes it is supposed to generate the plan itself and pass --plan. + # The word "provider" in engine messages refers to the engine's INTERNAL + # planner credentials - NOT a prerequisite the caller needs. If you are + # the hosting reasoning model, YOU are the provider. LAW 7 / 2026-04-19 + # Hermes Agent Use Cases failure mode. import sys print( - "[Planner] No --plan and no LLM provider configured. " - "Using deterministic fallback; results will be weaker than the " - "LLM-planned path. If invoked from Claude Code, generate a JSON " - "query plan and pass --plan.", + "[Planner] No --plan passed. If you are the reasoning model hosting " + "this skill (Claude Code, Codex, Hermes, Gemini, or any agent runtime), " + "YOU ARE the planner: generate a JSON query plan yourself and pass it " + "via --plan. You do not need an API key or credentials; you ARE the " + "LLM. The deterministic fallback below is the headless/cron path only. " + "See LAW 7 in SKILL.md and Step 0.75 for the plan schema.", file=sys.stderr, ) return _fallback_plan(topic, available_sources, requested_sources, depth) diff --git a/tests/test_planner_v3.py b/tests/test_planner_v3.py index 56e8f6f..fee2484 100644 --- a/tests/test_planner_v3.py +++ b/tests/test_planner_v3.py @@ -413,14 +413,24 @@ class FallbackDefaultsTests(unittest.TestCase): provider=None, model=None, ) - self.assertIn("No --plan and no LLM provider configured", buf.getvalue()) + output = buf.getvalue() + # New language: "No --plan passed" + "YOU ARE the planner" + + # runtime enumeration. Unit 4 (2026-04-19) rewrite to stop the + # "no provider = no LLM = I need a key" misread. + self.assertIn("No --plan passed", output) + self.assertIn("YOU ARE the planner", output) + self.assertIn("you ARE the LLM", output) + # Runtime-agnostic: each supported runtime name should appear. + for runtime_name in ("Claude Code", "Codex", "Hermes", "Gemini"): + self.assertIn(runtime_name, output) + # The old misleading phrasing must NOT appear. + self.assertNotIn("No --plan and no LLM provider configured", output) - def test_fallback_does_not_log_warning_when_provider_present(self): + def test_fallback_does_not_log_new_warning_when_provider_present(self): # When a provider is configured, the provider path runs; if it - # succeeds, no fallback warning should appear. - # (The existing sanitize tests cover this; we just confirm the - # warning string gating is on provider-presence, not on fallback - # activation.) + # errors, we get the "LLM planning failed" message, NOT the + # "No --plan passed" guidance (which is specifically for the + # no-provider-no-plan caller path). import io import contextlib buf = io.StringIO() @@ -438,11 +448,9 @@ class FallbackDefaultsTests(unittest.TestCase): provider=_NoopProvider(), model="some-model", ) - # Provider was present — we expect the "LLM planning failed" message, - # NOT the "No --plan and no LLM provider" message. output = buf.getvalue() self.assertIn("LLM planning failed", output) - self.assertNotIn("No --plan and no LLM provider configured", output) + self.assertNotIn("No --plan passed", output) if __name__ == "__main__":