pbakaus/impeccable · error · Error

Unsupported live copy-edit AI runner

Error message

Unsupported live copy-edit AI runner: ${provider}

What it means

runCopyEditBatchAgent() dispatches on the resolved provider and only recognizes 'mock', 'chat', 'codex', and 'claude'. Anything else that is truthy reaches the final else and throws 'Unsupported live copy-edit AI runner: <provider>'. Only opts.provider can trigger this: env-based selection via chooseCopyEditAgent lowercases IMPECCABLE_LIVE_COPY_AGENT and maps unknown values to null (which produces the no-runner error instead), so a bogus value here comes from a caller passing provider explicitly — including wrong casing like 'Codex', since opts.provider is used verbatim.

Solutions

  1. Pass exactly one of 'codex', 'claude', 'mock', or 'chat' — for 'chat' also provide the applyBatchToSource(batch, { repair }) callback or you get the separate chat-provider error
  2. Better: omit opts.provider entirely and let chooseCopyEditAgent pick from IMPECCABLE_LIVE_COPY_AGENT / auto-detection
  3. Lowercase external input before passing it: provider = String(raw).trim().toLowerCase()

Example fix

// before
await runCopyEditBatchAgent(batch, { provider: 'Codex' });

// after
await runCopyEditBatchAgent(batch, { provider: 'codex' });
// or omit provider and let IMPECCABLE_LIVE_COPY_AGENT decide
Defensive patterns

Strategy: type-guard

Validate before calling

const SUPPORTED = new Set(['mock', 'chat', 'codex', 'claude']);
const provider = rawProvider && String(rawProvider).trim().toLowerCase();
if (provider && !SUPPORTED.has(provider)) {
  throw new TypeError(`provider must be one of ${[...SUPPORTED].join('|')} — got ${rawProvider}`);
}
await runCopyEditBatchAgent(batch, { provider }); // or omit provider for env auto-selection

Type guard

const isSupportedCopyEditProvider = (p) =>
  p === 'mock' || p === 'chat' || p === 'codex' || p === 'claude';

if (!isSupportedCopyEditProvider(opts.provider)) {
  throw new TypeError(`Unsupported live copy-edit AI runner: ${opts.provider}`);
}

Try / catch

try {
  await runCopyEditBatchAgent(batch, { provider });
} catch (err) {
  if (err?.message?.startsWith('Unsupported live copy-edit AI runner:')) {
    // Config bug in YOUR caller: fix the provider literal; retrying cannot help.
    fixProviderConfig(err.message);
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: runCopyEditBatchAgent(batch, { provider: 'cursor' }), { provider: 'Claude' } / 'CODEX' (case must match exactly), or any future/typo value not in mock|chat|codex|claude.

Common situations: Embedders wiring runCopyEditBatchAgent into their own tooling with a hardcoded runner name; refactors that rename provider constants; copy-paste from env-var examples where values are case-normalized but the opts field is not.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of pbakaus/impeccable@f88b2837a7 (2026-08-18). Data as JSON: /api/errors/97edac53375cdc5b. Report an issue: GitHub.

Appendix: source

Thrown at skill/scripts/live-copy-edit-agent.mjs:128

    const raw = await opts.applyBatchToSource(batch, { repair: batch?.repair || null });
    return normalizeBatchResult(raw || {});
  }
  if (!provider) {
    throw new Error(describeNoProviderError({ env }));
  }

  const prompt = buildCopyEditBatchPrompt(batch, { cwd });
  const outDir = opts.outDir || fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-copy-batch-'));
  fs.mkdirSync(outDir, { recursive: true });
  const resultPath = path.join(outDir, 'result.json');
  const logPath = path.join(outDir, 'agent.log');

  if (provider === 'codex') {
    await runCodex(prompt, { cwd, env, resultPath, logPath, timeoutMs: opts.timeoutMs });
  } else if (provider === 'claude') {
    await runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs: opts.timeoutMs });
  } else {
    throw new Error(`Unsupported live copy-edit AI runner: ${provider}`);
  }

  const output = fs.existsSync(resultPath) ? fs.readFileSync(resultPath, 'utf-8') : '';
  const parsed = parseCopyEditBatchResult(output);
  if (parsed) return parsed;

  const tail = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf-8').slice(-1200) : output.slice(-1200);
  throw new Error('AI copy-edit batch did not return a valid completion payload. ' + tail.trim());
}

export function runCopyEditPostApplyChecks({ cwd = process.cwd(), files = [] } = {}) {
  const failures = [];
  const warnings = [];
  const uniqueFiles = [...new Set((files || []).filter((file) => typeof file === 'string' && file.trim()))];
  for (const relativeFile of uniqueFiles) {
    const file = path.resolve(cwd, relativeFile);
    if (!isPathInsideOrEqual(cwd, file) || !fs.existsSync(file)) {
      warnings.push({ file: relativeFile, reason: 'file_missing_or_outside_cwd' });

View on GitHub (pinned to f88b2837a7)