mvanhorn/last30days-skill · error · HandoffContractError

{label.capitalize()} file {file_path} must be a top-level JS

Error message

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

What it means

Raised by _load_host_file in discovery_handoff.py when a host-authored file (judgments or angles) parses as valid JSON but its top-level value is not a JSON object (dict) — e.g. the file is a JSON array or a bare string/number. The discovery handoff protocol requires every host file to be a top-level object so fields like bundle_id and the payload list can be looked up by key. It is thrown as HandoffContractError, the dedicated exception for host-file contract violations.

Source

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


def _load_host_file(path: str | Path, label: str) -> dict[str, Any]:
    """Load a host-authored handoff file with strict top-level checks."""
    file_path = Path(path).expanduser()
    try:
        raw = file_path.read_text(encoding="utf-8")
    except OSError as exc:
        raise HandoffContractError(
            f"Could not read {label} file {file_path}: {exc}"
        ) from exc
    try:
        payload = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise HandoffContractError(
            f"{label.capitalize()} file {file_path} is not valid JSON: {exc}"
        ) from exc
    if not isinstance(payload, dict):
        raise HandoffContractError(
            f"{label.capitalize()} file {file_path} must be a top-level JSON "
            f"object, got {type(payload).__name__}."
        )
    return payload


def _require_bundle_binding(
    payload: dict[str, Any],
    bundle: NominationsBundle | PendingReport,
    *,
    label: str,
    save_dir: str | Path | None,
    config_dir: Path | None,
) -> None:
    """Enforce bundle-id binding between a host file and the current bundle
    (or, on the finalize leg, the pending report that inherited its id).
    The mismatch message names the file actually validated against - the
    pending report on the finalize leg - so a host's retry is not misdirected

View on GitHub (pinned to c7460f6114)

Solutions

  1. Wrap the array in an object with the required keys: {"bundle_id": "<id>", "judgments": [...]} (or "angles": [...] for angles files).
  2. Re-run the same discovery leg with the corrected file path; no engine-side change is needed.
  3. Validate the shape before handing the file to the engine: python -c "import json;d=json.load(open(f));assert isinstance(d,dict)".

Example fix

// before (judgments.json)
[
  {"nomination_id": "n1", "junk": false}
]

// after
{
  "bundle_id": "<bundle_id from the bundle file>",
  "judgments": [
    {"nomination_id": "n1", "junk": false}
  ]
}
Defensive patterns

Strategy: validation

Validate before calling

import json

def valid_host_file(path: str) -> bool:
    try:
        payload = json.loads(open(path, encoding="utf-8").read())
    except (OSError, json.JSONDecodeError):
        return False
    return isinstance(payload, dict)

Try / catch

try:
    load_judgments(path, bundle, ...)
except HandoffContractError as e:
    # message names the file and the offending type; fix the file, not the call
    raise SystemExit(f"host file rejected: {e}")

Prevention

When it happens

Trigger: Calling load_judgments/save flow with a judgments file whose content is a bare JSON array like '[{"nomination_id": ...}]' instead of '{"bundle_id": ..., "judgments": [...]}'; same for the angles file passed to the angles loader. Any json.loads result that is a list, str, int, float, bool, or null triggers it after the JSONDecodeError guard passes.

Common situations: The host agent writes judgments as a top-level list because the schema mentions 'a list of rows' and it skips the wrapper object; hand-editing the file and dropping the outer braces; a template or example that shows only the array portion gets copied verbatim.

Related errors


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