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
- 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).
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
- 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.
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
- ${trimmed} is not a valid opener. Use ${OPENER} with a patte
- Invalid control line ${JSON.stringify(trimmed)}; use only ${
- Expected ${OPENER} on input line ${index + 1}.
- ${trimmed} is valid only in REWRITE, never MATCH.
- Operation ${operations.length + 1} has a second ${REWRITE_HE
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/6ebf37f989c46980.
Report an issue: GitHub.