pbakaus/impeccable · error
Unsupported live copy-edit AI runner: ${provider}
Error message
Unsupported live copy-edit AI runner: ${provider} What it means
Defensive throw inside runCopyEditBatchAgent(): provider is truthy (so it got past the !provider and mock/chat branches) but is not 'codex' or 'claude'. The internal chooseCopyEditAgent() only ever returns 'mock'|'chat'|'codex'|'claude'|null, so in normal operation this branch is unreachable. It fires when a caller passes opts.provider explicitly with an unsupported string.
Source
Thrown at plugin/skills/impeccable/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 d14711ae3d)
Solutions
- Use one of the supported provider values: 'mock', 'chat', 'codex', or 'claude' (case-sensitive, lowercase).
- If passing opts.provider programmatically, validate it against ['mock','chat','codex','claude'] before calling.
- Leave opts.provider unset and let chooseCopyEditAgent({ env }) pick, which can only return valid values or null (null yields the descriptive error 43 instead).
- For a new runner, extend both chooseCopyEditAgent() and this if/else; this throw means the dispatch table is out of sync.
Example fix
// before
await runCopyEditBatchAgent(batch, { provider: agentName }); // agentName='gemini'
// after
const ALLOWED = new Set(['mock', 'chat', 'codex', 'claude']);
if (agentName && !ALLOWED.has(agentName)) {
throw new Error(`Unknown copy-edit provider: ${agentName}`);
}
await runCopyEditBatchAgent(batch, { provider: ALLOWED.has(agentName) ? agentName : undefined }); Defensive patterns
Strategy: validation
Validate before calling
const ALLOWED_PROVIDERS = new Set(['mock', 'chat', 'codex', 'claude']);
function isValidProvider(value) {
return value == null || ALLOWED_PROVIDERS.has(value);
}
if (!isValidProvider(opts.provider)) {
throw new Error(`Unsupported provider: ${opts.provider}. Allowed: ${[...ALLOWED_PROVIDERS].join(', ')}`);
}
await runCopyEditBatchAgent(batch, opts); Type guard
/** Narrow an unknown value to a supported copy-edit provider name. */
function isCopyEditProvider(value) {
return typeof value === 'string'
&& ['mock', 'chat', 'codex', 'claude'].includes(value);
} Try / catch
try {
await runCopyEditBatchAgent(batch, { provider });
} catch (err) {
if (/Unsupported live copy-edit AI runner/.test(err.message)) {
// provider dispatch table is out of sync — leave opts.provider unset
// and let chooseCopyEditAgent pick a valid one.
await runCopyEditBatchAgent(batch, {});
return;
}
throw err;
} Prevention
- Leave opts.provider undefined in production so chooseCopyEditAgent() is the single source of valid values.
- If you forward a user-configured provider string, validate against the allowlist before calling.
- When adding a provider, update chooseCopyEditAgent(), this if/else, and the allowlist together.
When it happens
Trigger: Calling runCopyEditBatchAgent(batch, { provider: 'gemini' }) or any value outside the recognised set; a typo like 'Claude' (capitalised) or 'codex ' (trailing space); a future provider name added to env but not yet implemented in this if/else. The mock and chat paths are handled earlier, so only an unknown non-empty string reaches here.
Common situations: Hardcoding a provider for testing with a misspelled name; IMPECCABLE_LIVE_COPY_AGENT set to an experimental name the build doesn't recognise but caller forwards verbatim; downstream code that forwards user input as the provider without an allowlist.
Related errors
- No live copy-edit AI runner is available.
- AI copy-edit batch did not return a valid completion payload
- No live copy-edit AI runner is available.
- Unsupported live copy-edit AI runner: ${provider}
- AI copy-edit batch did not return a valid completion payload
AI-assisted analysis of pbakaus/impeccable@d14711ae3d (2026-08-13).
Data as JSON: /api/errors/8979feb024d21b63.
Report an issue: GitHub.