can1357/oh-my-pi · error

needsSeparator (Operation ${operations.length + 1} needs ${R

Error message

needsSeparator (Operation ${operations.length + 1} needs ${REWRITE_HEADER}.\nCopy-ready corrected payload (fill in the new text):\n...)

What it means

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.

Source

Thrown at packages/coding-agent/src/edit/sloppy.ts:1299

			// otherwise keep the fail-closed error.
			const closest = closestDesiredBlock(content, sourcePatternText);
			if (closest !== undefined) {
				operations.push({
					patternText: closest,
					sourcePatternText,
					rewrite: { kind: "explicit", text: sourcePatternText },
					all: false,
					recoveryNote: `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}.`,
				});
				return;
			}
		}
		const 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")}`;
		// A multiline pattern-only block may be the delete half of a move; assume
		// deletion now, justified post-parse only when another op re-emits it.
		const normalizedPattern = normalizeText(sourcePatternText).text;
		if (!sourcePatternText.includes("\n") || normalizedPattern.length < 24) {
			throw new Error(needsSeparator);
		}
		const operation = createOperation(sourcePatternText, "", allMatches, operations.length + 1, true);
		operation.assumedDeletion = true;
		pendingSeparatorErrors.set(operations.length, needsSeparator);
		operations.push(operation);
	};

	for (let index = 0; index < lines.length; index++) {
		const line = lines[index];
		const parsedOpener = parseOpener(line);
		const trimmed = line.trim();
		const registerReference = trimmed.match(/^»([1-9]\d*)$/u);
		if (isOrdinalOpener(line)) {
			throw new Error(
				`${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.`,
			);
		}
		if (trimmed === `${OPENER}${REWRITE_HEADER}`) {

View on GitHub (pinned to 9690622007)

Solutions

  1. 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.
  2. If the operation really is a deletion of a long (>=24 char) multi-line block, you may omit «»; otherwise it must be explicit.
  3. 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).

Example fix

// before
«const x = 1;

// after
«const x = 1;
»
const x = 2;
Defensive patterns

Strategy: validation

Validate before calling

function hasSeparator(body: string): boolean {
  const opCount = (body.match(/^«\*?$/gm) || []).length;
  const sepCount = (body.match(/^»$/gm) || []).length;
  return sepCount >= opCount;
}
if (!hasSeparator(sectionBody)) throw new Error("every operation needs a » rewrite line");

Type guard

const isControlLine = (l: string) => l.trim() === "«" || l.trim() === "«*" || l.trim() === "»" || /^»[1-9]\d*$/.test(l.trim());

Try / catch

try {
  const result = await computeSloppySectionDiff(section, cwd);
  if ("error" in result) {
    if (result.error.includes("needs »")) {
      // resubmit with the copy-ready payload from the message
    }
  }
} catch (err) { /* handle */ }

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/6ebf37f989c46980. Report an issue: GitHub.