{"record":{"id":"8c51589dee2588a9","repo":"mvanhorn/last30days-skill","slug":"could-not-write-nominations-bundle-path-exc","errorCode":null,"errorMessage":"Could not write nominations bundle {path}: {exc}","messagePattern":"Could not write nominations bundle (.+?): (.+?)","errorType":"exception","errorClass":"HandoffContractError","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/lib/discovery_handoff.py","lineNumber":332,"sourceCode":"                list(enrichment_source_boundary)\n                if enrichment_source_boundary is not None\n                else None\n            ),\n            \"requested_sources\": (\n                list(requested_sources) if requested_sources is not None else None\n            ),\n            \"lookback_days\": int(lookback_days),\n        },\n        \"nominations\": rows,\n    }\n    path = nominations_bundle_path(state_dir)\n    try:\n        state_dir.mkdir(parents=True, exist_ok=True)\n        path.write_text(json.dumps(payload, indent=2), encoding=\"utf-8\")\n    except OSError as exc:\n        # A locked/read-only/full disk is the protocol's clean exit-2 path,\n        # never a traceback.\n        raise HandoffContractError(\n            f\"Could not write nominations bundle {path}: {exc}\"\n        ) from exc\n    return NominationsBundle(\n        schema_version=schema.DISCOVERY_NOMINATIONS_SCHEMA_VERSION,\n        bundle_id=bundle_id,\n        generated_at=generated_at,\n        from_date=from_date,\n        to_date=to_date,\n        domain=domain,\n        tier=tier,\n        enrichment_source_boundary=(\n            list(enrichment_source_boundary)\n            if enrichment_source_boundary is not None\n            else None\n        ),\n        requested_sources=(\n            list(requested_sources) if requested_sources is not None else None\n        ),","sourceCodeStart":314,"sourceCodeEnd":350,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/lib/discovery_handoff.py#L314-L350","documentation":"`write_nominations_bundle` raises HandoffContractError when creating the state dir or writing `discover-nominations.json` raises OSError. The comment states the design intent: a locked, read-only, or full disk is the protocol's clean exit-2 path, never a traceback. The message includes the target path and the underlying OS error.","triggerScenarios":"`--save-dir` on a read-only mount; `~/.config/last30days/` owned by another user; disk full at write time; an immutable/locked file at the bundle path; SELinux/AppArmor denying writes.","commonSituations":"Shared servers where the config dir was created by root; macOS Full Disk Access blocking writes under certain protected paths; CI runners with small tmpfs; a leftover root-owned `discover-nominations.json` from a sudo run.","solutions":["Check the path named in the message: `ls -ld <dir>` and `df -h <dir>` for permission and space.","Fix ownership/permissions (`chown -R \"$USER\" <dir>` or chmod u+w) or free disk space.","Point `--save-dir` at a known-writable location for this run and subsequent legs.","Remove a stale locked file at the bundle path if that is the blocker, then re-run the nominate-only sweep."],"exampleFix":"# before: config dir owned by root\nsudo mkdir -p ~/.config/last30days   # created as root\npython3 last30days.py \"topic\" --discover --nominate-only\n# after\nsudo chown -R \"$(id -un)\" ~/.config/last30days && python3 last30days.py \"topic\" --discover --nominate-only","handlingStrategy":"try-catch","validationCode":"from pathlib import Path\n\ndef store_is_writable(state_dir: Path) -> bool:\n    try:\n        state_dir.mkdir(parents=True, exist_ok=True)\n        probe = state_dir / \".write-probe\"\n        probe.write_text(\"x\", encoding=\"utf-8\")\n        probe.unlink()\n        return True\n    except OSError:\n        return False","typeGuard":null,"tryCatchPattern":"from lib import discovery_handoff\ntry:\n    bundle = discovery_handoff.write_nominations_bundle(...)\nexcept discovery_handoff.HandoffContractError as exc:\n    raise SystemExit(2) from exc  # clean exit; message names path + OS error","preventionTips":["Pre-flight a write probe in the chosen save dir before launching a paid sweep.","Keep handoff state off network/locked mounts; use local disk.","Never create the config dir with sudo — it ends up root-owned."],"tags":["discovery","handoff","filesystem","permissions","exit-code-2"],"backgroundTag":null,"analyzedSha":"c7460f6114449ddfe6ea3fc2f23c3d910c0e740c","analyzedAt":"2026-08-15T03:34:49.540Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}