{"record":{"id":"6ebf37f989c46980","repo":"can1357/oh-my-pi","slug":"needsseparator-operation-operations-length-1","errorCode":null,"errorMessage":"needsSeparator (Operation ${operations.length + 1} needs ${REWRITE_HEADER}.\\nCopy-ready corrected payload (fill in the new text):\\n...)","messagePattern":"needsSeparator \\(Operation (.+?) needs (.+?)\\.\\\\nCopy-ready corrected payload \\(fill in the new text\\):\\\\n\\.\\.\\.\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/edit/sloppy.ts","lineNumber":1299,"sourceCode":"\t\t\t// otherwise keep the fail-closed error.\n\t\t\tconst closest = closestDesiredBlock(content, sourcePatternText);\n\t\t\tif (closest !== undefined) {\n\t\t\t\toperations.push({\n\t\t\t\t\tpatternText: closest,\n\t\t\t\t\tsourcePatternText,\n\t\t\t\t\trewrite: { kind: \"explicit\", text: sourcePatternText },\n\t\t\t\t\tall: false,\n\t\t\t\t\trecoveryNote: `Note: operation ${operations.length + 1} stated desired text without markers; the closest matching block was replaced with it. Mark changes explicitly with ${SELECT_OPEN}old${SELECT_DIVIDER}new${SELECT_CLOSE}.`,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tconst needsSeparator = `Operation ${operations.length + 1} needs ${REWRITE_HEADER}.\\nCopy-ready corrected payload (fill in the new text):\\n${[...lines.slice(0, endIndex), REWRITE_HEADER, \"<new text>\", ...lines.slice(endIndex)].join(\"\\n\")}`;\n\t\t// A multiline pattern-only block may be the delete half of a move; assume\n\t\t// deletion now, justified post-parse only when another op re-emits it.\n\t\tconst normalizedPattern = normalizeText(sourcePatternText).text;\n\t\tif (!sourcePatternText.includes(\"\\n\") || normalizedPattern.length < 24) {\n\t\t\tthrow new Error(needsSeparator);\n\t\t}\n\t\tconst operation = createOperation(sourcePatternText, \"\", allMatches, operations.length + 1, true);\n\t\toperation.assumedDeletion = true;\n\t\tpendingSeparatorErrors.set(operations.length, needsSeparator);\n\t\toperations.push(operation);\n\t};\n\n\tfor (let index = 0; index < lines.length; index++) {\n\t\tconst line = lines[index];\n\t\tconst parsedOpener = parseOpener(line);\n\t\tconst trimmed = line.trim();\n\t\tconst registerReference = trimmed.match(/^»([1-9]\\d*)$/u);\n\t\tif (isOrdinalOpener(line)) {\n\t\t\tthrow new Error(\n\t\t\t\t`${trimmed} is not a valid opener. Use ${OPENER} with a pattern that matches once — add context only the intended match has — or ${OPENER}* to change every match.`,\n\t\t\t);\n\t\t}\n\t\tif (trimmed === `${OPENER}${REWRITE_HEADER}`) {","sourceCodeStart":1281,"sourceCodeEnd":1317,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/edit/sloppy.ts#L1281-L1317","documentation":"This parser handles the «sloppy» edit format where each operation is «pattern» followed by » (rewrite header) plus replacement text. When an operation has only a MATCH block and no «» separator, the parser must guess: if the pattern is a single line or shorter than 24 normalized characters it refuses to guess it is a deletion and throws, telling the author to add the «» line explicitly. The error message includes a copy-ready corrected payload with the «» header inserted at the right place, so the model/author can resubmit verbatim after filling in the new text.","triggerScenarios":"Calling sloppyVariant.apply()/computeSloppySectionDiff() with a section body where an operation block contains a «...» pattern but no «» line before the next operation, and the pattern text is one line OR under 24 visible characters (too small to safely assume deletion).","commonSituations":"An LLM authoring edits forgets the «» separator line for a one-line change (e.g. deleting or renaming a short line); hand-written payloads treating the whole block as pattern text only; copy-pasting fragments that dropped the separator.","solutions":["Insert a «» line after the MATCH text and put the replacement (or nothing to delete) after it — the error message itself contains the corrected payload with a <new text> placeholder.","If the operation really is a deletion of a long (>=24 char) multi-line block, you may omit «»; otherwise it must be explicit.","Resubmit the whole payload; the parser tracks this as a pending separator error and only forgives it if another operation re-emits the text (a move)."],"exampleFix":"// before\n«const x = 1;\n\n// after\n«const x = 1;\n»\nconst x = 2;","handlingStrategy":"validation","validationCode":"function hasSeparator(body: string): boolean {\n  const opCount = (body.match(/^«\\*?$/gm) || []).length;\n  const sepCount = (body.match(/^»$/gm) || []).length;\n  return sepCount >= opCount;\n}\nif (!hasSeparator(sectionBody)) throw new Error(\"every operation needs a » rewrite line\");","typeGuard":"const isControlLine = (l: string) => l.trim() === \"«\" || l.trim() === \"«*\" || l.trim() === \"»\" || /^»[1-9]\\d*$/.test(l.trim());","tryCatchPattern":"try {\n  const result = await computeSloppySectionDiff(section, cwd);\n  if (\"error\" in result) {\n    if (result.error.includes(\"needs »\")) {\n      // resubmit with the copy-ready payload from the message\n    }\n  }\n} catch (err) { /* handle */ }","preventionTips":["Always emit one «» pair per operation, even for deletions (empty rewrite).","When generating payloads programmatically, template the «/pattern/»/replacement structure.","Rely on the error's embedded copy-ready payload rather than hand-reconstructing the fix."],"tags":["parser","edit-format","missing-separator"],"backgroundTag":"missing-section-separator","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}