NousResearch/hermes-agent · critical

${isPosix ? 'install.sh --manifest' : 'install.ps1 -Manifest

Error message

${isPosix ? 'install.sh --manifest' : 'install.ps1 -Manifest'} failed: exit ${result.code}\n${result.stderr || result.stdout}

What it means

RuntimeError from load_jobs() (cron/jobs.py:1120): jobs.json parsed successfully as JSON, but the top-level value is neither an object (expected {'jobs': [...]}) nor a list (auto-repairable) — it is a string, number, bool, or null. Without this guard, data.get('jobs') would raise AttributeError and take down the cron subsystem; instead it fails with a precise type name.

Source

Thrown at apps/desktop/electron/bootstrap-runner.ts:707

  return args
}

async function fetchManifest({ scriptPath, installerKind, emit, hermesHome, activeRoot, installStamp, pinCommit }) {
  const isPosix = installerKind === 'posix'

  const args = isPosix
    ? ['--manifest', ...buildPosixPinArgs({ installStamp, activeRoot, hermesHome, pinCommit })]
    : ['-Manifest', ...buildPinArgs(installStamp, { pinCommit })]

  const result = await (isPosix ? spawnBash : spawnPowerShell)(scriptPath, args, {
    emit,
    stageName: '__manifest__',
    hermesHome
  })

  if (result.code !== 0) {
    throw new Error(
      `${isPosix ? 'install.sh --manifest' : 'install.ps1 -Manifest'} failed: exit ${result.code}\n${result.stderr || result.stdout}`
    )
  }

  // The manifest is the LAST JSON line on stdout (install.ps1 may print
  // banner / info lines first depending on Console.OutputEncoding effects).
  // Find the last line that parses as JSON with a `stages` field.
  const lines = result.stdout.split(/\r?\n/).filter(Boolean)

  for (let i = lines.length - 1; i >= 0; i--) {
    try {
      const parsed = JSON.parse(lines[i])

      if (parsed && Array.isArray(parsed.stages)) {
        return parsed
      }
    } catch {
      void 0

View on GitHub (pinned to c896c09c42)

Solutions

  1. Look at the reported type name in the message to confirm what's actually in the file.
  2. Back up the file, then replace its contents with {"jobs": []}.
  3. Recreate needed jobs with `hermes cron add` (or restore from a backup of the old JSON).
  4. Ensure only Hermes writes jobs.json — no shell redirects or editors target it.

Example fix

# before: ~/.hermes/cron/jobs.json contains  null
echo '{"jobs": []}' > ~/.hermes/cron/jobs.json  # after
hermes cron list
Defensive patterns

Strategy: validation

Validate before calling

import json
from pathlib import Path
from hermes_constants import get_hermes_home

def jobs_payload_ok() -> bool:
    f = get_hermes_home() / "cron" / "jobs.json"
    if not f.exists():
        return True
    data = json.loads(f.read_text())
    return isinstance(data, (dict, list)) and not isinstance(data, (str, bytes))

# a bare string/number/null top level will fail here before Hermes raises

Type guard

def is_jobs_payload(data: object) -> bool:
    """True when data is a shape load_jobs() accepts or auto-repairs."""
    return isinstance(data, dict) or isinstance(data, list)

Try / catch

try:
    jobs = list_jobs()
except RuntimeError as e:
    if "expected {'jobs": " in str(e) or "got " in str(e) and "corrupted" in str(e):
        f.write_text('{"jobs": []}')  # after backing up; then reload
        jobs = list_jobs()

Prevention

When it happens

Trigger: jobs.json contains e.g. "ok", 42, true, or null at top level — usually a script or human overwriting the file with something that isn't a job store at all.

Common situations: A user echoed a status string into jobs.json by mistake; a monitoring script truncated the file to empty content that parsed as null; an editor auto-saved a scratch note into the wrong file.

Related errors


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/55bdce89b631bbea. Report an issue: GitHub.