can1357/oh-my-pi · error
${trimmed} is not a valid opener. Use ${OPENER} with a patte
Error message
${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. What it means
Operation openers in this format are exactly « (match once) or «* (match all). A numbered opener like «3 is invalid — the parser detects it via isOrdinalOpener (/^«[1-9]\d*$/) and throws immediately. The author probably meant to target a specific occurrence by number, which the format deliberately does not support: instead you must include enough surrounding context in the pattern so it matches exactly once, or use «* to change every match.
Source
Thrown at packages/coding-agent/src/edit/sloppy.ts:1313
// 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}`) {
// A glued «» line: after MATCH content it is a mistyped » separator;
// anywhere else it is a stray operation terminator to drop.
if (state === "pattern" && patternLines.some(entry => entry.trim() !== "")) state = "rewrite";
continue;
}
if (
parsedOpener === false &&
(trimmed.startsWith(OPENER) ||
(trimmed.startsWith(REWRITE_HEADER) && trimmed !== REWRITE_HEADER && !registerReference))
) {
throw new Error(
`Invalid control line ${JSON.stringify(trimmed)}; use only ${OPENER}, ${OPENER}*, ${REWRITE_HEADER}, or ${REWRITE_HEADER}N in REWRITE.`,
);
}View on GitHub (pinned to 9690622007)
Solutions
- Replace «N with plain « and expand the pattern with surrounding context so it matches only the intended occurrence.
- Use «* if the intent was to apply the change to every match.
- Remove the numbering entirely — operations are implicitly ordered by their appearance in the payload.
Example fix
// before
«2
old line
»
new line
// after
«
function context() {
old line
}
»
function context() {
new line
} Defensive patterns
Strategy: validation
Validate before calling
const badOrdinal = /^«[1-9]\d*$/m;
if (badOrdinal.test(body)) throw new Error("numbered openers («N) are invalid; use « or «*"); Type guard
const isValidOpener = (line: string) => line.trim() === "«" || line.trim() === "«*";
Try / catch
catch (err) {
if (err instanceof Error && err.message.includes("is not a valid opener")) {
body = body.replace(/^«\d+\s*$/gm, "«");
}
} Prevention
- Never number operations — order in the payload is the numbering.
- For occurrence targeting, widen the pattern with unique surrounding context instead of an index.
- Sanitize model output with a regex strip of ^«\d+$ lines before applying.
When it happens
Trigger: A payload whose operation opener line is «N (e.g. «1, «2) passed to computeSloppySectionDiff/sloppyVariant.apply — typically a model numbering its operations or confusing this format with another numbered edit protocol.
Common situations: LLMs trained on ordinal-numbered patch formats emit «1 «2 headers; humans numbering steps in a multi-edit payload; mixing up this format with apply-patch or search/replace count syntax.
Related errors
- needsSeparator (Operation ${operations.length + 1} needs ${R
- 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/020b954dad619af8.
Report an issue: GitHub.