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

visual_verdict.verdict must be one of: ${VISUAL_VERDICT_STAT

Error message

visual_verdict.verdict must be one of: ${VISUAL_VERDICT_STATUSES.join('|')}

What it means

parseVisualVerdictStatus requires visual_verdict.verdict to be a string that, after trim+lowercase, is one of VISUAL_VERDICT_STATUSES. It throws the same message both for non-string values and for unrecognized status strings, listing the allowed statuses in the message.

Source

Thrown at src/visual/verdict.ts:38

}

function asTrimmedStringArray(value: unknown, field: string): string[] {
  if (!Array.isArray(value)) {
    throw new Error(`visual_verdict.${field} must be an array`);
  }
  return value
    .map((item) => {
      if (typeof item !== 'string') {
        throw new Error(`visual_verdict.${field} must contain strings`);
      }
      return item.trim();
    })
    .filter((item) => item.length > 0);
}

function parseVisualVerdictStatus(value: unknown): VisualVerdictStatus {
  if (typeof value !== 'string') {
    throw new Error(`visual_verdict.verdict must be one of: ${VISUAL_VERDICT_STATUSES.join('|')}`);
  }
  const normalized = value.trim().toLowerCase();
  if (!VISUAL_VERDICT_STATUSES.includes(normalized as VisualVerdictStatus)) {
    throw new Error(`visual_verdict.verdict must be one of: ${VISUAL_VERDICT_STATUSES.join('|')}`);
  }
  return normalized as VisualVerdictStatus;
}

export function parseVisualVerdict(input: unknown): VisualVerdict {
  if (!input || typeof input !== 'object') {
    throw new Error('visual_verdict must be an object');
  }
  const raw = input as Record<string, unknown>;
  if (typeof raw.score !== 'number' || !Number.isInteger(raw.score) || raw.score < 0 || raw.score > 100) {
    throw new Error('visual_verdict.score must be an integer between 0 and 100');
  }
  if (typeof raw.category_match !== 'boolean') {
    throw new Error('visual_verdict.category_match must be a boolean');

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Use exactly one of the allowed statuses listed in the error message (the message enumerates them)
  2. Normalize model output by mapping synonyms to the canonical set before parsing
  3. If a new status is genuinely needed, update VISUAL_VERDICT_STATUSES and re-publish both sides

Example fix

// before
{ "verdict": "FAILED", "observations": [], "next_actions": [] }
// after
{ "verdict": "fail", "observations": [], "next_actions": [] } // using an allowed status from the error message
Defensive patterns

Strategy: validation

Validate before calling

const v = String(verdict.verdict ?? '').trim().toLowerCase();
if (!VISUAL_VERDICT_STATUSES.includes(v)) verdict.verdict = mapSynonym(v) ?? 'needs_review';

Type guard

const isVisualVerdictStatus = (v: unknown): v is VisualVerdictStatus =>
  typeof v === 'string' && VISUAL_VERDICT_STATUSES.includes(v.trim().toLowerCase() as VisualVerdictStatus);

Prevention

When it happens

Trigger: Calling parseVisualVerdict with verdict missing/null/object, or a value like 'Fail ' that normalizes fine but 'FAILED', 'pass_with_issues', or any status not in VISUAL_VERDICT_STATUSES.

Common situations: LLM agents inventing new verdict labels; case mismatches handled by normalization but synonym mismatches not (e.g. 'needs_work' vs the allowed value); upstream schema versions that renamed statuses.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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