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
- 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
- Better: omit opts.provider entirely and let chooseCopyEditAgent pick from IMPECCABLE_LIVE_COPY_AGENT / auto-detection
- 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
- Derive provider values from a constant union, never free-typed strings
- Normalize external input with trim().toLowerCase() before passing it as opts.provider
- Prefer omitting opts.provider and driving selection through IMPECCABLE_LIVE_COPY_AGENT
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
- No live copy-edit AI runner is available.
- --target requires a path value
- AI copy-edit batch did not return a valid completion…
- {}
- preflight returned no scaffold metadata
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)