can1357/oh-my-pi · error

Expected ${OPENER} on input line ${index + 1}.

Error message

Expected ${OPENER} on input line ${index + 1}.

What it means

The parser starts in the "outside" state: before the first « opener, the only legal non-empty input is the opener itself. Any other non-blank line before the first operation is rejected with this error, stating the 1-based input line number where the unexpected content was found. Blank lines are tolerated so leading whitespace does not break payloads.

Source

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

		}
		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.`,
			);
		}
		if (state === "outside") {
			if (parsedOpener !== false) {
				allMatches = parsedOpener === 0;
				patternLines = [];
				rewriteLines = [];
				referenceSeparator = undefined;
				state = "pattern";
			} else if (trimmed !== "") {
				throw new Error(`Expected ${OPENER} on input line ${index + 1}.`);
			}
			continue;
		}

		if (state === "pattern") {
			const accumulated = patternLines.join("\n");
			const markersBalanced = (accumulated.match(/⟪/gu) || []).length === (accumulated.match(/⟫/gu) || []).length;
			if (trimmed === REWRITE_HEADER) {
				state = "rewrite";
			} else if (trimmed === "***" && markersBalanced && patternLines.some(entry => entry.trim() !== "")) {
				// A bare *** after MATCH content is an apply-patch-habit separator.
				state = "rewrite";
			} else if (trimmed === SELECT_CLOSE && markersBalanced && patternLines.some(entry => entry.trim() !== "")) {
				// A lone ⟫ after balanced selections is a mistyped » separator.
				state = "rewrite";
			} else if (registerReference && markersBalanced) {
				if (!patternLines.some(entry => entry.trim() !== "")) {
					throw new Error(`${trimmed} is valid only in REWRITE, never MATCH.`);

View on GitHub (pinned to 9690622007)

Solutions

  1. Start the payload with a « (or §path + «) opener line before any content.
  2. Delete any preamble text/prose before the first opener.
  3. If the content before the first opener is meaningful, move it inside the first operation's MATCH block.

Example fix

// before
Here are the changes:
«
pattern
»
replacement

// after
«
pattern
»
replacement
Defensive patterns

Strategy: validation

Validate before calling

const firstContent = body.split("\n").find(l => l.trim() !== "");
if (firstContent !== undefined && !(firstContent.trim() === "«" || firstContent.trim() === "«*" || /^§/.test(firstContent.trim()))) {
  throw new Error("payload must start with a « opener");
}

Type guard

const startsWithOpener = (body: string): boolean => {
  const l = body.split("\n").find(x => x.trim() !== "");
  return l !== undefined && (l.trim() === "«" || l.trim() === "«*");
};

Try / catch

catch (err) {
  if (err instanceof Error && err.message.includes("Expected « on input line")) {
    // drop leading prose/headers before the first « and retry
  }
}

Prevention

When it happens

Trigger: Passing a section body that starts with prose, a file path line, a diff header (--- a/...), or any other text before the first « opener line to the sloppy apply routine.

Common situations: Models prefixing payloads with explanations or «file» headers; tooling stripping the opener when slicing the body; pasted payloads that lost the first « line; bodies that include diff-style headers.

Related errors


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