mvanhorn/last30days-skill · error · SystemExit

[Planner] Cannot read --plan file: {exc}\n

Error message

[Planner] Cannot read --plan file: {exc}\n

What it means

The --plan handling in run_topic: when the argument names an existing file, it is read as UTF-8; OSError or UnicodeDecodeError prints [Planner] Cannot read --plan file and exits 2. As with [12], the file branch only applies when os.path.isfile(plan_str) is true — otherwise the string is parsed as inline JSON.

Source

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

    try:
        x_related = [h.strip() for h in args.x_related.split(",") if h.strip()] if args.x_related else None
        subreddits = [s.strip().removeprefix("r/") for s in args.subreddits.split(",") if s.strip()] if args.subreddits else None
        dedicated_subreddits = [s.strip().removeprefix("r/") for s in args.dedicated_subreddits.split(",") if s.strip()] if args.dedicated_subreddits else None
        tiktok_hashtags = [h.strip().lstrip("#") for h in args.tiktok_hashtags.split(",") if h.strip()] if args.tiktok_hashtags else None
        tiktok_creators = [c.strip().lstrip("@") for c in args.tiktok_creators.split(",") if c.strip()] if args.tiktok_creators else None
        ig_creators = [c.strip().lstrip("@") for c in args.ig_creators.split(",") if c.strip()] if args.ig_creators else None
        # Parse external plan if provided via --plan flag
        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).

View on GitHub (pinned to c7460f6114)

Solutions

  1. Pre-validate in the caller: python3 -c "open('plan.json', encoding='utf-8').read()" before invoking the engine.
  2. Write plans atomically (temp file + rename) so the engine never reads a partial file.
  3. Pass an absolute path to avoid cwd-dependent isfile() resolution.

Example fix

# before
with open(path, 'w') as f: f.write(plan_json)  # engine may read mid-write

# after
import tempfile, os
tmp = path + '.tmp'
with open(tmp, 'w', encoding='utf-8') as f: f.write(plan_json)
os.replace(tmp, path)
Defensive patterns

Strategy: validation

Validate before calling

if os.path.isfile(plan_arg):
    Path(plan_arg).read_text(encoding='utf-8')  # fail in caller with better context

Prevention

When it happens

Trigger: --plan plan.json where the file is unreadable (permissions), is a directory-like special file, or contains invalid UTF-8 bytes; racing a writer that truncates the file.

Common situations: Plan files generated into /tmp and reaped before the engine subprocess reads them; files transferred with a bad encoding; permission-restricted plans from another user.

Related errors


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