mvanhorn/last30days-skill · error · SystemExit
[Planner] Invalid --plan schema: {exc}.\n
Error message
[Planner] Invalid --plan schema: {exc}.\n What it means
Raised when a user-supplied `--plan` JSON value parses as JSON but fails structural validation by `lib/planner.py:validate_external_plan`. The validator requires a top-level object with `intent`, `freshness_mode`, `cluster_mode` (non-empty strings) and `subqueries` (non-empty array of objects, each with non-empty `search_query`/`ranking_query` strings and a non-empty `sources` string array; optional numeric `weight`, optional `source_weights` map of source names to numbers). The CLI prints the message to stderr and exits with code 2, deliberately failing fast rather than silently dropping to the internal planner and burning a paid run the user did not ask for.
Source
Thrown at skills/last30days/scripts/last30days.py:3266
with open(plan_str, encoding="utf-8") as f:
plan_str = f.read()
except (OSError, UnicodeDecodeError) as exc:
sys.stderr.write(f"[Planner] Cannot read --plan file: {exc}\n")
raise SystemExit(2)
try:
external_plan = _json.loads(plan_str)
except _json.JSONDecodeError as exc:
sys.stderr.write(f"[Planner] Invalid --plan JSON: {exc}\n")
# Fail fast instead of silently dropping to the internal planner
# and burning a paid run the user did not ask for. Mirrors the
# --plan file-read branch above and parse_competitors_plan.
raise SystemExit(2)
from lib import planner as _plan_validator
try:
_plan_validator.validate_external_plan(external_plan)
except ValueError as exc:
sys.stderr.write(f"[Planner] Invalid --plan schema: {exc}.\n")
raise SystemExit(2)
# Auto-resolve: use web search to discover subreddits/handles before planning.
# This is the engine-side equivalent of SKILL.md Steps 0.55/0.75 for platforms
# 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_planView on GitHub (pinned to c7460f6114)
Solutions
- Read the stderr message: it names the exact failing field (e.g. "missing required field 'intent'") from validate_external_plan in lib/planner.py:163.
- Fix the plan to satisfy the contract: required keys intent/freshness_mode/cluster_mode/subqueries; every subquery needs non-empty search_query, ranking_query, and a non-empty sources array of non-empty source names; weights must be numbers, not booleans or strings.
- Validate before running: `python3 -c "from lib import planner; planner.validate_external_plan(json.load(open('plan.json')))"` from scripts/ dir (or import lib.planner) to iterate quickly without spending an engine run.
- If you no longer need an explicit plan, drop `--plan` entirely and let the internal planner build one (note: this changes run cost).
Example fix
// before
--plan '{"intent":"comparison","subqueries":[{"search_query":"zen browser","sources":["reddit"]}]}'
// after (ranking_query added; both required per subquery)
--plan '{"intent":"comparison","freshness_mode":"standard","cluster_mode":"auto","subqueries":[{"search_query":"zen browser","ranking_query":"zen browser","sources":["reddit"],"weight":1.0}]}' Defensive patterns
Strategy: validation
Validate before calling
import json
from lib import planner # from skills/last30days/scripts
with open("plan.json", encoding="utf-8") as fh:
plan = json.load(fh)
planner.validate_external_plan(plan) # raises ValueError naming the exact field
print("plan OK") Type guard
def is_valid_external_plan(raw) -> bool:
try:
from lib import planner
planner.validate_external_plan(raw)
return True
except ValueError:
return False Try / catch
from lib import planner
try:
planner.validate_external_plan(plan)
except ValueError as exc:
raise SystemExit(f"plan rejected: {exc}") from exc Prevention
- Generate plans programmatically with json.dump from a validated template rather than hand-editing strings.
- Run planner.validate_external_plan as a pre-flight in your wrapper script before spending an engine run.
- Keep one known-good plan.json under version control and diff against it after edits.
When it happens
Trigger: Passing `--plan '{...}'` (inline JSON or a file's contents) that is valid JSON but, e.g., omits `freshness_mode`, has `subqueries: []`, has a subquery with an empty `ranking_query`, uses a boolean as a source weight (`"reddit": true`), or lists a source as an empty string.
Common situations: Hand-authoring a plan JSON for a reproducible run and forgetting a required field; adapting an old plan schema after the validator tightened; a model/host generating the plan and emitting `subqueries` as a dict or weights as strings; copy-paste truncation dropping the tail of the JSON object's fields.
Related errors
- Unknown search source in {flag_name}: {source}
- {flag_name} requires at least one source.
- Unsupported emit mode: {emit}
- [Competitors] --competitors-list is empty.\n
- [Competitors] --competitors must be >= {COMPETITORS_MIN} (go
AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15).
Data as JSON: /api/errors/530c4ad66a6de068.
Report an issue: GitHub.