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

Mission execution requires a task runner; use --dry-run for

Error message

Mission execution requires a task runner; use --dry-run for parser/plan validation.

What it means

During a real (non-dry-run) mission run, options.runTask is undefined. Actually-executing runs need a caller-supplied task runner; --dry-run paths return earlier without one.

Source

Thrown at src/cli/mission.ts:472

    if (parsed.dryRun) {
      for (const task of summary.tasks) task.status = "planned";
      summary.counts = missionCounts(summary.tasks);
      summary.completed_at = now().toISOString();
      await persistSummary(paths.summaryPath, summary);
      await appendLedger(paths.ledgerPath, { event: "mission_planned", at: summary.completed_at, slug: paths.slug, total: tasks.length, summary_path: paths.summaryPath });
      if (parsed.json) stdout(JSON.stringify({ ok: true, summary_path: paths.summaryPath, ledger_path: paths.ledgerPath, summary }, null, 2));
      else {
        stdout(`mission planned: ${paths.slug} (${tasks.length} tasks)`);
        for (const task of summary.tasks) stdout(`[planned] ${task.id} line ${task.source_line}: ${task.prompt}`);
        stdout(`summary: ${paths.summaryPath}`);
        stdout(`ledger: ${paths.ledgerPath}`);
      }
      return;
    }

    const runTask = options.runTask;
    if (!runTask) throw new MissionCommandError("Mission execution requires a task runner; use --dry-run for parser/plan validation.");

    await persistSummary(paths.summaryPath, summary);
    await appendLedger(paths.ledgerPath, { event: "mission_started", at: startedAt, slug: paths.slug, total: tasks.length, summary_path: paths.summaryPath });
    await runSelectedTasks(summary, paths, parsed, now, stdout, runTask, () => true);

    summary.status = missionStatus(summary.tasks);
    summary.completed_at = now().toISOString();
    summary.counts = missionCounts(summary.tasks);
    await persistSummary(paths.summaryPath, summary);
    await appendLedger(paths.ledgerPath, { event: "mission_completed", at: summary.completed_at, slug: paths.slug, status: summary.status, counts: summary.counts });

    if (parsed.json) stdout(JSON.stringify({ ok: summary.status === "passed", summary_path: paths.summaryPath, ledger_path: paths.ledgerPath, summary }, null, 2));
    else {
      stdout(`mission ${summary.status}: ${paths.slug}`);
      stdout(`summary: ${paths.summaryPath}`);
      stdout(`ledger: ${paths.ledgerPath}`);
    }
    if (summary.status === "failed") process.exitCode = 1;

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Provide options.runTask when invoking run/resume/rerun programmatically
  2. Add --dry-run if you only need parser/plan validation
  3. Delegate to the actual CLI entrypoint which supplies the default runner

Example fix

// before
await missionCommand(['run', 'mission.md'], { stdout });
// after
await missionCommand(['run', 'mission.md'], { stdout, runTask: runCodexTask });
Defensive patterns

Strategy: validation

Validate before calling

if (action === 'run' && !parsed.dryRun && typeof options.runTask !== 'function') failEarly('runner required');

Type guard

const hasRunner = (o: MissionOptions): o is MissionOptions & { runTask: RunTask } =>
  typeof o.runTask === 'function';

Prevention

When it happens

Trigger: Calling missionCommand(['run', file], {}) without runTask and without --dry-run — the run reaches the execution stage and has no way to execute tasks.

Common situations: Host applications embedding the CLI without wiring the runner; tests that omit the runner stub; refactors dropping the option.

Related errors


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