mvanhorn/last30days-skill · error · HandoffContractError
Angles file {path} must carry a top-level \"angles\" list.
Error message
Angles file {path} must carry a top-level \"angles\" list. What it means
Raised by the angles loader in discovery_handoff.py when an angles file is supplied (path is not None), passes the top-level-object and bundle-binding checks, but its 'angles' key is missing or not a JSON list. Passing path=None is the supported way to run without angles (empty mapping, topics ship without angles) — only a supplied file with the wrong shape raises. Per-row handling is lenient, mirroring the judgments loader; angle sentences are word-boundary capped at 200 chars downstream.
Source
Thrown at skills/last30days/scripts/lib/discovery_handoff.py:818
"""Read the host angles file for leg 3, keyed by nomination id.
``bundle`` is the binding target: the finalize leg passes the pending
report (the bundle_id echo validates against it, and the known ids are
its surviving ``angle_inputs`` ids), while a NominationsBundle binds
against the full pool. A missing angles file is legal: ``path=None``
returns an empty mapping and every topic ships without angles. When a
path is given the same strict-top-level / lenient-per-row rules as
judgments apply; angle sentences are word-boundary capped at 200 chars.
"""
if path is None:
return {}
payload = _load_host_file(path, "angles")
_require_bundle_binding(
payload, bundle, label="angles", save_dir=save_dir, config_dir=config_dir,
)
rows = payload.get("angles")
if not isinstance(rows, list):
raise HandoffContractError(
f"Angles file {path} must carry a top-level \"angles\" list."
)
known = (
set(bundle.angle_inputs)
if isinstance(bundle, PendingReport)
else {entry.nomination_id for entry in bundle.nominations}
)
angles: dict[str, HostAngles] = {}
for row_id, row in _known_rows(
rows, known, row_label="angles", unknown_label="angles"
):
podcast = _sanitized_angle(row.get("podcast"))
x_article = _sanitized_angle(row.get("x_article"))
if podcast is None and x_article is None:
# No usable hook at all: treat the row as absent.
continue
angles[row_id] = HostAngles(podcast=podcast, x_article=x_article)
return anglesView on GitHub (pinned to c7460f6114)
Solutions
- Use "angles": [...] as the top-level list key in the angles file.
- If you do not want angles for this leg, pass path=None / omit the angles file instead of shipping an empty wrong-shaped file.
- Verify the file loads and has the key: assert isinstance(json.load(open(p)).get("angles"), list).
Example fix
// before
{
"bundle_id": "...",
"angle": [{"nomination_id": "n1", "podcast": "..."}]
}
// after
{
"bundle_id": "...",
"angles": [{"nomination_id": "n1", "podcast": "..."}]
} Defensive patterns
Strategy: validation
Validate before calling
import json
def valid_angles_file(path: str) -> bool:
payload = json.loads(open(path, encoding="utf-8").read())
return isinstance(payload, dict) and isinstance(payload.get("angles"), list) Try / catch
try:
load_angles(path, bundle, ...)
except HandoffContractError as e:
# fix the "angles" key on the host side; or pass path=None to skip angles entirely
... Prevention
- If angles are optional for your leg, pass path=None instead of shipping a placeholder file.
- Note the plural key 'angles' (parallel to 'judgments').
- Reuse the same file-building helper for both judgments and angles with the key as a parameter.
When it happens
Trigger: Passing an angles file with key 'angle' (singular), 'suggestions', or omitting the key; "angles": {} instead of []; reusing a judgments file as the angles file (it has 'judgments', not 'angles').
Common situations: The host writes the file from memory of a similar schema; singular/plural confusion; the file was templated from the judgments example.
Related errors
- Judgments file {path} must carry a top-level \"judgments\" l
- {label.capitalize()} file {file_path} must be a top-level JS
- {repo_dir.name} emitted the agent JSON profile; the evaluato
- [CompetitorsPlan] Top-level must be a dict of {entity: {targ
- {label} {path} must be a top-level JSON object, got {type(pa
AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15).
Data as JSON: /api/errors/6106bc51314a4cb0.
Report an issue: GitHub.