mvanhorn/last30days-skill · error · HandoffContractError

{label} {path} must be a top-level JSON object, got {type(pa

Error message

{label} {path} must be a top-level JSON object, got {type(payload).__name__}.

What it means

`_parse_handoff_envelope` raises HandoffContractError when `json.loads` succeeds but the result is not a Python dict — e.g. the file holds a top-level JSON array or scalar. The engine-written envelope contract is a JSON object with schema_version/kind/bundle_id/generated_at, so any other top-level type is a contract violation. The message reports the actual type name (list, str, int, ...).

Source

Thrown at skills/last30days/scripts/lib/discovery_handoff.py:408

) -> 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}"
        )
    file_kind = payload.get("kind")
    if file_kind != kind:
        raise HandoffContractError(
            f"{label} {path} has kind {file_kind!r}; expected "
            f"{kind!r}. {remedy}"
        )
    bundle_id = str(payload.get("bundle_id") or "")
    if not bundle_id:
        raise HandoffContractError(

View on GitHub (pinned to c7460f6114)

Solutions

  1. Check the file: `python3 -c "import json;print(type(json.load(open('<path>'))))"` — it must be dict.
  2. If you extracted/rewrote it, restore the full envelope object (schema_version, kind, bundle_id, generated_at, context, nominations) — but the reliable path is regeneration via `--discover --nominate-only`.
  3. Never pipe jq filters that change the top level back over the handoff file.

Example fix

# before
jq '.nominations' discover-nominations.json > discover-nominations.json   # now a top-level array
# after: regenerate instead of editing
rm discover-nominations.json && python3 last30days.py "topic" --discover --nominate-only --save-dir .
Defensive patterns

Strategy: type-guard

Validate before calling

import json
from pathlib import Path

def envelope_is_object(path: Path) -> bool:
    try:
        return isinstance(json.loads(path.read_text(encoding="utf-8")), dict)
    except (OSError, json.JSONDecodeError):
        return False

Type guard

import json
from typing import Any

def is_json_object(raw: str) -> bool:
    try:
        return isinstance(json.loads(raw), dict)
    except json.JSONDecodeError:
        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:
    sys.exit(2)  # message names the actual top-level type; regenerate the file

Prevention

When it happens

Trigger: A `discover-nominations.json` replaced with a bare JSON array of nominations (dropping the envelope); a file containing just a string or number; a hand-authored attempt to reconstruct a bundle from its rows only.

Common situations: Users or agents 'simplifying' the bundle format; a tool re-serializing the file and losing the object wrapper; jq round-trips like `jq '.nominations' file > file` that extract a sub-array.

Related errors


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