pbakaus/impeccable · error · Error
AI copy-edit batch did not return a valid completion…
Error message
AI copy-edit batch did not return a valid completion payload. ${tail} What it means
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.
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
Defensive patterns
Strategy: retry
Validate before calling
// Pre-check runner health before applying the batch:
import { spawnSync } from 'node:child_process';
const healthy = (cmd) => spawnSync(cmd, ['--version'], { stdio: 'ignore' }).status === 0;
if (!healthy('codex') && !healthy('claude')) {
throw new Error('No AI runner available — fix auth before Apply');
} Try / catch
const MAX_ATTEMPTS = 3;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
result = await runCopyEditBatchAgent(batch, { cwd, outDir });
break;
} catch (err) {
const transient = err?.message?.startsWith('AI copy-edit batch did not return a valid completion payload.');
if (!transient || attempt === MAX_ATTEMPTS) throw err;
// The message tail is the agent.log excerpt — log it, then retry.
console.warn(`attempt ${attempt} produced no valid payload, retrying`);
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- No live copy-edit AI runner is available.
- comp-diff: cannot read spec
- comp-spec: cannot read regions
- config.json must be an object
- {}
AI-assisted analysis of pbakaus/impeccable@f88b2837a7 (2026-08-18).
Data as JSON: /api/errors/0d850cfae0df6aae.
Report an issue: GitHub.
Appendix: source
Thrown at skill/scripts/live-copy-edit-agent.mjs:136
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' });
continue;
}
let content = '';
try { content = fs.readFileSync(file, 'utf-8'); } catch (err) {
failures.push({ file: relativeFile, reason: 'read_failed', message: err.message });
continue;
}
const markerMatch = findLeftoverImpeccableMarker(content);View on GitHub (pinned to f88b2837a7)