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

  1. Replace «N with plain « and expand the pattern with surrounding context so it matches only the intended occurrence.
  2. Use «* if the intent was to apply the change to every match.
  3. 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

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


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