nexu-io/open-design · error · SystemExit

invalid imagegen-jobs.json: jobs must be a list

Error message

invalid imagegen-jobs.json: jobs must be a list

What it means

Raised by job_list() in queue_pet_repairs.py when manifest.get('jobs') is not a list. The repair flow reads imagegen-jobs.json and expects jobs to be an array of job objects; any other shape (object, null, scalar) is treated as corruption rather than an empty job set.

Source

Thrown at skills/hatch-pet/scripts/queue_pet_repairs.py:66

    note = f"""

Repair attempt {attempt}:
- The previous `{state}` strip failed QA: {reason}
- Regenerate the entire row, not just one pose.
- Fill every requested frame slot with one complete centered full-body pet pose.
- Keep large gaps of pure chroma key only between slots; do not leave a requested slot empty.
- Avoid pose overlap, clipping, edge slivers, extra partial sprites, and detached fragments from neighboring poses.
- Use the canonical base image and any original references listed in `imagegen-jobs.json` as grounding inputs.
- Do not redesign the pet. Keep the exact same head shape, face design, markings, body proportions, palette, outline weight, materials, and props as the approved base pet.
- If the contact sheet shows identity drift, repair only this row while preserving the canonical base identity.
"""
    prompt_path.write_text(existing.rstrip() + note.rstrip() + "\n", encoding="utf-8")


def job_list(manifest: dict[str, object]) -> list[dict[str, object]]:
    jobs = manifest.get("jobs")
    if not isinstance(jobs, list):
        raise SystemExit("invalid imagegen-jobs.json: jobs must be a list")
    return [job for job in jobs if isinstance(job, dict)]


def next_archive_path(archive_dir: Path, state: str, attempt: int, suffix: str) -> Path:
    candidate = archive_dir / f"{state}-attempt-{attempt}-previous{suffix}"
    if not candidate.exists():
        return candidate
    counter = 2
    while True:
        candidate = archive_dir / f"{state}-attempt-{attempt}-previous-{counter}{suffix}"
        if not candidate.exists():
            return candidate
        counter += 1


def archive_decoded_output(run_dir: Path, job: dict[str, object], state: str, attempt: int) -> str | None:
    output_raw = job.get("output_path")
    output = (

View on GitHub (pinned to 5be4028344)

Solutions

  1. Re-run prepare_pet_run.py --force to regenerate a well-formed imagegen-jobs.json.
  2. Validate the shape: jq '.jobs | type' imagegen-jobs.json must print 'array'.
  3. If hand-editing, wrap the entries back into "jobs": [ ... ].
  4. Restore imagegen-jobs.json from version control or the prepare step's output.

Example fix

// before: {"jobs": {"idle": {...}}}
// after:  {"jobs": [{"id": "idle", ...}]}
Defensive patterns

Strategy: type-guard

Validate before calling

jobs = manifest.get("jobs")
if not isinstance(jobs, list):
    raise SystemExit("imagegen-jobs.json 'jobs' must be an array; re-run prepare")

Type guard

def has_array_jobs(manifest: object) -> bool:
    return isinstance(manifest, dict) and isinstance(manifest.get("jobs"), list)

Prevention

When it happens

Trigger: imagegen-jobs.json was hand-edited into an object keyed by id; a partial write left malformed JSON that still parsed but with the wrong top-level shape; a different tool wrote the manifest with a 'tasks' key instead of 'jobs'.

Common situations: Manual schema 'cleanup' that wrapped jobs in an object; downstream tool wrote the manifest with a 'tasks' key instead of 'jobs'; merge conflict resolved incorrectly.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/914e76649673a144. Report an issue: GitHub.