{"record":{"id":"0d850cfae0df6aae","repo":"pbakaus/impeccable","slug":"ai-copy-edit-batch-did-not-return-a-valid-completi","errorCode":null,"errorMessage":"AI copy-edit batch did not return a valid completion payload. ${tail}","messagePattern":"AI copy-edit batch did not return a valid completion payload\\. (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"skill/scripts/live-copy-edit-agent.mjs","lineNumber":136,"sourceCode":"  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' });\n      continue;\n    }\n    let content = '';\n    try { content = fs.readFileSync(file, 'utf-8'); } catch (err) {\n      failures.push({ file: relativeFile, reason: 'read_failed', message: err.message });\n      continue;\n    }\n    const markerMatch = findLeftoverImpeccableMarker(content);","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/pbakaus/impeccable/blob/f88b2837a7d7c3182e46307bbbb091a1ed547571/skill/scripts/live-copy-edit-agent.mjs#L118-L154","documentation":"After the codex or claude subprocess exits 0, runCopyEditBatchAgent reads result.json (written by codex --output-last-message, or mirrored from claude --print --output-format json stdout) and hands it to parseCopyEditBatchResult(), which needs a JSON object with status 'done' | 'partial' | 'error'. If the file is missing/empty or no such payload can be extracted, it throws this error with the last ~1200 characters of agent.log (or of the raw output) appended, so the message itself shows what the runner actually said last.","triggerScenarios":"The model replies with prose containing no JSON object at all (violating the 'Return ONLY JSON' contract in buildCopyEditBatchPrompt); the CLI prints an auth/quota/limit notice instead of a result; the runner exits 0 without writing the output file; a pinned IMPECCABLE_LIVE_COPY_AGENT_MODEL produces answers that never include the required status field.","commonSituations":"Expired or rate-limited CLI credentials surfacing as a final non-JSON line; low reasoning effort (default model_reasoning_effort 'low' for codex) yielding chatty output; models wrapping the answer in shapes without a recognizable {...} block containing status.","solutions":["Read the tail embedded in the error — agent.log carries the runner's actual last output, and auth/quota/model failures show up there","Retry the Apply: transient non-JSON answers are the most common cause and usually clear on a second attempt","Pin a model that follows the contract via IMPECCABLE_LIVE_COPY_AGENT_MODEL","Raise IMPECCABLE_LIVE_COPY_AGENT_TIMEOUT_MS (default 120s in the commit path) if the log shows the answer cut off","For deterministic tests, switch to IMPECCABLE_LIVE_COPY_AGENT=mock with IMPECCABLE_LIVE_COPY_AGENT_MOCK_RESULT supplying the JSON"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Pre-check runner health before applying the batch:\nimport { spawnSync } from 'node:child_process';\nconst healthy = (cmd) => spawnSync(cmd, ['--version'], { stdio: 'ignore' }).status === 0;\nif (!healthy('codex') && !healthy('claude')) {\n  throw new Error('No AI runner available — fix auth before Apply');\n}","typeGuard":null,"tryCatchPattern":"const MAX_ATTEMPTS = 3;\nfor (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n  try {\n    result = await runCopyEditBatchAgent(batch, { cwd, outDir });\n    break;\n  } catch (err) {\n    const transient = err?.message?.startsWith('AI copy-edit batch did not return a valid completion payload.');\n    if (!transient || attempt === MAX_ATTEMPTS) throw err;\n    // The message tail is the agent.log excerpt — log it, then retry.\n    console.warn(`attempt ${attempt} produced no valid payload, retrying`);\n  }\n}","preventionTips":["Keep codex/claude CLIs authenticated — their auth/quota notices are what usually replace the JSON payload","Pin a reliable model with IMPECCABLE_LIVE_COPY_AGENT_MODEL and allow more time via IMPECCABLE_LIVE_COPY_AGENT_TIMEOUT_MS","Always log the appended tail (last 1200 chars of agent.log); it distinguishes auth failures from contract violations","In tests use IMPECCABLE_LIVE_COPY_AGENT=mock with IMPECCABLE_LIVE_COPY_AGENT_MOCK_RESULT to remove the nondeterminism"],"tags":["ai-runner","json","parsing","copy-edit"],"backgroundTag":"llm-invalid-json-response","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"}