mvanhorn/last30days-skill · error · SystemExit

[Planner] Invalid --plan JSON: {exc}\n

Error message

[Planner] Invalid --plan JSON: {exc}\n

What it means

The second --plan guard: json.loads on the plan string (file contents or inline JSON) raised JSONDecodeError, so the engine exits 2 rather than silently falling back to the internal planner — a deliberate fail-fast so a malformed plan never burns a paid run the user did not ask for. This mirrors parse_competitors_plan's behavior.

Source

Thrown at skills/last30days/scripts/last30days.py:3260

        external_plan = None
        if args.plan:
            import json as _json
            plan_str = args.plan
            if os.path.isfile(plan_str):
                try:
                    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

View on GitHub (pinned to c7460f6114)

Solutions

  1. Validate first: python3 -m json.tool plan.json — fix any reported syntax error.
  2. Produce plans programmatically with json.dumps, never string interpolation.
  3. Check the plan against validate_external_plan's expected schema after parsing (the next guard catches schema errors separately).

Example fix

# before (inline)
--plan "{subreddits: ['a'], handles: ['b']}"

# after
--plan '{"subreddits": ["a"], "handles": ["b"]}'
Defensive patterns

Strategy: validation

Validate before calling

import json
plan = json.loads(Path(plan_path).read_text(encoding='utf-8'))
from lib import planner as v
v.validate_external_plan(plan)  # catches syntax AND schema before any paid run

Prevention

When it happens

Trigger: --plan with inline JSON using single quotes/trailing commas; a plan file that is YAML, JSONL, or truncated; a file whose last write was interrupted mid-object.

Common situations: LLM/agent emitting non-strict JSON; hand-edited plans; newline-delimited plan files; copying from a chat client that mangled quotes.

Related errors


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