Yeachan-Heo/oh-my-codex · error · UltragoalError

${label} must be a JSON object.

Error message

${label} must be a JSON object.

What it means

assertPlainObject guards parsed JSON inputs (notably --directive-json) so the steering engine always receives a JSON object, never an array, scalar, or null. It is used with labels like '--directive-json' so the message says which input failed.

Source

Thrown at src/cli/ultragoal.ts:235

const STEERING_KINDS = new Set<UltragoalSteeringMutationKind>(ULTRAGOAL_STEERING_MUTATION_KINDS);
const STEERING_SOURCES = new Set<UltragoalSteeringSource>(ULTRAGOAL_STEERING_SOURCES);

type CliSteerResult = Awaited<ReturnType<typeof steerUltragoal>>;

function parseSteeringKind(raw: string | undefined): UltragoalSteeringMutationKind {
  if (!raw) throw new UltragoalError('Missing --kind for structured ultragoal steer.');
  if (!STEERING_KINDS.has(raw as UltragoalSteeringMutationKind)) throw new UltragoalError(`Invalid --kind: ${raw}. Expected one of ${Array.from(STEERING_KINDS).join(', ')}.`);
  return raw as UltragoalSteeringMutationKind;
}

function parseSteeringSource(raw: string | undefined, fallback: UltragoalSteeringSource = 'cli'): UltragoalSteeringSource {
  if (!raw) return fallback;
  if (!STEERING_SOURCES.has(raw as UltragoalSteeringSource)) throw new UltragoalError(`Invalid --source: ${raw}. Expected one of ${Array.from(STEERING_SOURCES).join(', ')}.`);
  return raw as UltragoalSteeringSource;
}

function assertPlainObject(value: unknown, label: string): Record<string, unknown> {
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new UltragoalError(`${label} must be a JSON object.`);
  return value as Record<string, unknown>;
}

function normalizeTargetGoalId(raw: Record<string, unknown>): string | undefined {
  if (typeof raw.targetGoalId === 'string' && raw.targetGoalId.trim()) return raw.targetGoalId.trim();
  if (Array.isArray(raw.targetGoalIds)) return raw.targetGoalIds.find((id): id is string => typeof id === 'string' && id.trim().length > 0)?.trim();
  return undefined;
}

function normalizeSteeringProposal(raw: Record<string, unknown>, _fallbackDirectiveText?: string): UltragoalSteeringProposal {
  const kind = parseSteeringKind(typeof raw.kind === 'string' ? raw.kind : undefined);
  const source = parseSteeringSource(typeof raw.source === 'string' ? raw.source : undefined);
  const after = raw.after && typeof raw.after === 'object' && !Array.isArray(raw.after)
    ? raw.after as UltragoalSteeringAfterPayload
    : undefined;
  return {
    kind,
    source,

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Wrap the payload in an object: `{"proposals":[...]}` or send one proposal object with kind/evidence/rationale fields
  2. Validate with `jq 'type'` — it must print "object" before passing the file

Example fix

# before
omx ultragoal steer --directive-json '[{"kind":"add_goal"}]'
# Error: --directive-json must be a JSON object.

# after
omx ultragoal steer --directive-json '{"kind":"add_goal","evidence":"e","rationale":"r"}'
Defensive patterns

Strategy: type-guard

Validate before calling

const parsed = JSON.parse(directiveJson);
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
  console.error('--directive-json must be an object, not an array/scalar'); process.exit(2);
}

Type guard

function isPlainObject(v: unknown): v is Record<string, unknown> {
  return !!v && typeof v === 'object' && !Array.isArray(v);
}

Prevention

When it happens

Trigger: Passing `--directive-json '[1,2]'`, `--directive-json '"text"'`, or `--directive-json 'null'` to `omx ultragoal steer`; a JSON file containing an array also triggers it.

Common situations: Reusing an exported array of proposals instead of a single object; tools emitting a top-level array; JSON files that wrap payloads in arrays by convention.

Related errors


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/31d1e7ee64b1fb79. Report an issue: GitHub.