{"record":{"id":"9132aa2a2a49ec9d","repo":"mvanhorn/last30days-skill","slug":"competitorsplan-cannot-read-plan-file-exc-n","errorCode":null,"errorMessage":"[CompetitorsPlan] Cannot read plan file: {exc}\\n","messagePattern":"\\[CompetitorsPlan\\] Cannot read plan file: (.+?)\\\\n","errorType":"console","errorClass":"SystemExit","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/last30days.py","lineNumber":803,"sourceCode":"\ndef parse_competitors_plan(raw: str | None) -> dict[str, dict]:\n    \"\"\"Parse a --competitors-plan argument into a {entity_name_lower: plan_entry} dict.\n\n    Accepts inline JSON or a file path (matches --plan). Returns {} on None/empty.\n    Validation: top-level must be a dict; each value must be a dict. Unknown fields\n    in entry values log a warning but do not abort. Invalid JSON or non-dict shape\n    raises SystemExit(2) with a clear stderr message.\n    \"\"\"\n    if not raw:\n        return {}\n    plan_str = raw\n    if os.path.isfile(plan_str):\n        try:\n            with open(plan_str, encoding=\"utf-8\") as f:\n                plan_str = f.read()\n        except (OSError, UnicodeDecodeError) as exc:\n            sys.stderr.write(f\"[CompetitorsPlan] Cannot read plan file: {exc}\\n\")\n            raise SystemExit(2)\n    try:\n        parsed = json.loads(plan_str)\n    except json.JSONDecodeError as exc:\n        sys.stderr.write(f\"[CompetitorsPlan] Invalid JSON: {exc}\\n\")\n        raise SystemExit(2)\n    if not isinstance(parsed, dict):\n        sys.stderr.write(\n            f\"[CompetitorsPlan] Top-level must be a dict of \"\n            f\"{{entity: {{targeting}}}}, got {type(parsed).__name__}\\n\"\n        )\n        raise SystemExit(2)\n    known_fields = {\n        \"x_handle\", \"x_related\", \"subreddits\",\n        \"github_user\", \"github_repos\", \"trustpilot_domain\", \"context\",\n    }\n    normalized: dict[str, dict] = {}\n    for entity, entry in parsed.items():\n        if not isinstance(entry, dict):","sourceCodeStart":785,"sourceCodeEnd":821,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/last30days.py#L785-L821","documentation":"parse_competitors_plan() first treats the raw --competitors-plan value as a file path when os.path.isfile() matches; failures opening or decoding that file (OSError, UnicodeDecodeError) print a [CompetitorsPlan] message and exit 2. Note the value may also be inline JSON — the file branch only triggers when the string happens to name an existing file.","triggerScenarios":"--competitors-plan plan.json where plan.json lacks read permission, is a broken symlink (isfile False → falls to JSON parse instead), or contains non-UTF-8 bytes.","commonSituations":"Plan files written by another tool/user with 0600 perms; accidentally passing a binary or JSONL file; relative path resolving against a different cwd.","solutions":["Check permissions and encoding: chmod +r plan.json; file plan.json to confirm UTF-8 text.","Pass an absolute path so isfile() resolves predictably.","If the value is inline JSON, ensure no file with that exact string as its name exists in cwd (which would hijack it into the file branch)."],"exampleFix":"# before\n--competitors-plan ../plans/rivals.json   # relative, wrong cwd\n\n# after\n--competitors-plan /abs/path/plans/rivals.json","handlingStrategy":"validation","validationCode":"p = Path(raw).expanduser()\nif os.path.isfile(raw):\n    data = p.read_bytes()\n    data.decode('utf-8')  # raises before the engine does, with your own context","typeGuard":null,"tryCatchPattern":null,"preventionTips":["chmod +r plan files created by other users/tools.","Avoid passing inline JSON strings that collide with an existing filename in cwd."],"tags":["cli","filesystem","competitors","plan"],"backgroundTag":null,"analyzedSha":"c7460f6114449ddfe6ea3fc2f23c3d910c0e740c","analyzedAt":"2026-08-15T03:34:49.540Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}