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_plan

View on GitHub (pinned to c7460f6114)

Solutions

  1. 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.
  2. 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.
  3. 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.
  4. 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

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


AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15). Data as JSON: /api/errors/530c4ad66a6de068. Report an issue: GitHub.