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 misdirectedView on GitHub (pinned to c7460f6114)
Solutions
- Wrap the array in an object with the required keys: {"bundle_id": "<id>", "judgments": [...]} (or "angles": [...] for angles files).
- Re-run the same discovery leg with the corrected file path; no engine-side change is needed.
- 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
- Always emit host files with json.dump on a dict, never on a list.
- Keep a canonical example judgments/angles file and copy it as the starting point.
- Run a one-line shape check (isinstance(json.load(...), dict)) before invoking the engine.
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
- The {label} file is bound to bundle_id {file_bundle_id!r} bu
- Judgments file {path} must carry a top-level \"judgments\" l
- Angles file {path} must carry a top-level \"angles\" list.
- publish endpoint returned unexpected JSON response
- top-level plan must be an object
AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15).
Data as JSON: /api/errors/936b354b417d136f.
Report an issue: GitHub.