{"record":{"id":"97edac53375cdc5b","repo":"pbakaus/impeccable","slug":"unsupported-live-copy-edit-ai-runner-provider","errorCode":null,"errorMessage":"Unsupported live copy-edit AI runner: ${provider}","messagePattern":"Unsupported live copy-edit AI runner: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"skill/scripts/live-copy-edit-agent.mjs","lineNumber":128,"sourceCode":"    const raw = await opts.applyBatchToSource(batch, { repair: batch?.repair || null });\n    return normalizeBatchResult(raw || {});\n  }\n  if (!provider) {\n    throw new Error(describeNoProviderError({ env }));\n  }\n\n  const prompt = buildCopyEditBatchPrompt(batch, { cwd });\n  const outDir = opts.outDir || fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-copy-batch-'));\n  fs.mkdirSync(outDir, { recursive: true });\n  const resultPath = path.join(outDir, 'result.json');\n  const logPath = path.join(outDir, 'agent.log');\n\n  if (provider === 'codex') {\n    await runCodex(prompt, { cwd, env, resultPath, logPath, timeoutMs: opts.timeoutMs });\n  } else if (provider === 'claude') {\n    await runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs: opts.timeoutMs });\n  } else {\n    throw new Error(`Unsupported live copy-edit AI runner: ${provider}`);\n  }\n\n  const output = fs.existsSync(resultPath) ? fs.readFileSync(resultPath, 'utf-8') : '';\n  const parsed = parseCopyEditBatchResult(output);\n  if (parsed) return parsed;\n\n  const tail = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf-8').slice(-1200) : output.slice(-1200);\n  throw new Error('AI copy-edit batch did not return a valid completion payload. ' + tail.trim());\n}\n\nexport function runCopyEditPostApplyChecks({ cwd = process.cwd(), files = [] } = {}) {\n  const failures = [];\n  const warnings = [];\n  const uniqueFiles = [...new Set((files || []).filter((file) => typeof file === 'string' && file.trim()))];\n  for (const relativeFile of uniqueFiles) {\n    const file = path.resolve(cwd, relativeFile);\n    if (!isPathInsideOrEqual(cwd, file) || !fs.existsSync(file)) {\n      warnings.push({ file: relativeFile, reason: 'file_missing_or_outside_cwd' });","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/pbakaus/impeccable/blob/f88b2837a7d7c3182e46307bbbb091a1ed547571/skill/scripts/live-copy-edit-agent.mjs#L110-L146","documentation":"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.","triggerScenarios":"runCopyEditBatchAgent(batch, { provider: 'cursor' }), { provider: 'Claude' } / 'CODEX' (case must match exactly), or any future/typo value not in mock|chat|codex|claude.","commonSituations":"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.","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()"],"exampleFix":"// before\nawait runCopyEditBatchAgent(batch, { provider: 'Codex' });\n\n// after\nawait runCopyEditBatchAgent(batch, { provider: 'codex' });\n// or omit provider and let IMPECCABLE_LIVE_COPY_AGENT decide","handlingStrategy":"type-guard","validationCode":"const SUPPORTED = new Set(['mock', 'chat', 'codex', 'claude']);\nconst provider = rawProvider && String(rawProvider).trim().toLowerCase();\nif (provider && !SUPPORTED.has(provider)) {\n  throw new TypeError(`provider must be one of ${[...SUPPORTED].join('|')} — got ${rawProvider}`);\n}\nawait runCopyEditBatchAgent(batch, { provider }); // or omit provider for env auto-selection","typeGuard":"const isSupportedCopyEditProvider = (p) =>\n  p === 'mock' || p === 'chat' || p === 'codex' || p === 'claude';\n\nif (!isSupportedCopyEditProvider(opts.provider)) {\n  throw new TypeError(`Unsupported live copy-edit AI runner: ${opts.provider}`);\n}","tryCatchPattern":"try {\n  await runCopyEditBatchAgent(batch, { provider });\n} catch (err) {\n  if (err?.message?.startsWith('Unsupported live copy-edit AI runner:')) {\n    // Config bug in YOUR caller: fix the provider literal; retrying cannot help.\n    fixProviderConfig(err.message);\n    return;\n  }\n  throw err;\n}","preventionTips":["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"],"tags":["provider","arguments","copy-edit","live"],"backgroundTag":"invalid-config-value","analyzedSha":"f88b2837a7d7c3182e46307bbbb091a1ed547571","analyzedAt":"2026-08-18T04:58:36.608Z","contentChangedAt":"2026-08-18T04:58:36.608Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}