mvanhorn/last30days-skill · error · HandoffContractError
Could not read {label.lower()} {path}: {exc}
Error message
Could not read {label.lower()} {path}: {exc} What it means
`_parse_handoff_envelope` (shared by the nominations-bundle and pending-report readers) raises HandoffContractError when `path.read_text` raises OSError — the handoff file exists but cannot be read (permissions, I/O error, race where it was deleted between the exists() check and the open). The message names the file kind ('nominations bundle' / 'pending discovery report'), the path, and the OS error; the CLI maps it to exit 2.
Source
Thrown at skills/last30days/scripts/lib/discovery_handoff.py:398
def _parse_handoff_envelope(
path: Path,
*,
label: str,
kind: str,
schema_version: str,
remedy: str,
missing_id_context: str,
stale_context: str,
) -> tuple[dict[str, Any], str, Any]:
"""Shared strict top-level validation for the two engine-written handoff
files (nominations bundle, pending report): readable, valid JSON object,
right kind and schema version, bundle_id present, within TTL. Returns
(payload, bundle_id, generated_at)."""
try:
raw = path.read_text(encoding="utf-8")
except OSError as exc:
raise HandoffContractError(
f"Could not read {label.lower()} {path}: {exc}"
) from exc
try:
payload = json.loads(raw)
except json.JSONDecodeError as exc:
raise HandoffContractError(
f"{label} {path} is not valid JSON: {exc}"
) from exc
if not isinstance(payload, dict):
raise HandoffContractError(
f"{label} {path} must be a top-level JSON object, "
f"got {type(payload).__name__}."
)
version = payload.get("schema_version")
if version != schema_version:
raise HandoffContractError(
f"{label} {path} has schema version {version!r}; this "
f"build reads {schema_version!r}. {remedy}"View on GitHub (pinned to c7460f6114)
Solutions
- Inspect the exact path in the message: `ls -l <path>` for mode/owner and `cat <path>` to reproduce the read failure.
- Fix permissions (`chmod 644` / `chown`) or remount the volume read-write.
- Re-run the leg that produces the file (nominate-only sweep, or resume leg for the pending report) to rewrite it cleanly.
- Serialize concurrent discovery runs on the same save dir — the handoff files are single-writer state.
Example fix
# before: bundle written under sudo, unreadable as user # after sudo chown "$(id -un):$(id -gn)" /path/to/save-dir/discover-nominations.json && python3 last30days.py "topic" --discover --judgments judgments.json --save-dir /path/to/save-dir
Defensive patterns
Strategy: try-catch
Validate before calling
from pathlib import Path
def readable_file(path: Path) -> bool:
try:
with path.open("r", encoding="utf-8"):
return True
except OSError:
return False Try / catch
from lib import discovery_handoff
try:
bundle = discovery_handoff.load_nominations_bundle(save_dir=save_dir, config_dir=config_dir)
except discovery_handoff.HandoffContractError as exc:
if "Could not read" in exc.message:
# permissions / IO: fix the store, then retry the leg
...
raise Prevention
- Check ownership of handoff files when runs alternate between sudo and user contexts.
- Keep the save dir on local, non-flaky storage.
- Don't run concurrent discovery protocols against the same store.
When it happens
Trigger: `discover-nominations.json` or `discover-pending.json` present but mode 000 / owned by another user; a network mount dropping mid-read; the file deleted by a concurrent process after discovery but before parse.
Common situations: Root-created files from earlier sudo runs; NFS/FUSE latency or auth expiry on the save dir; two concurrent engine invocations where one cleans state while the other reads; container read-only volume mounts.
Related errors
- Could not write nominations bundle {path}: {exc}
- No handoff location available to write the nominations bundl
- Could not read {label} file {file_path}: {exc}
- No discovery nominations bundle found. Searched:\n{_searched
- {label} {path} is not valid JSON: {exc}
AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15).
Data as JSON: /api/errors/a2c5781f905953bf.
Report an issue: GitHub.