can1357/oh-my-pi · error

Invalid control line ${JSON.stringify(trimmed)}; use only ${

Error message

Invalid control line ${JSON.stringify(trimmed)}; use only ${OPENER}, ${OPENER}*, ${REWRITE_HEADER}, or ${REWRITE_HEADER}N in REWRITE.

What it means

Lines that begin with a control marker (« or ») must be exactly one of the four legal control lines: «, «*, », or »N (a register reference). Anything else starting with those glyphs — e.g. «/path/to/file, « some comment, »! — is rejected so malformed payloads fail loudly instead of being silently parsed as literal text. The offending line is echoed with JSON.stringify for exactness.

Source

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

		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.`,
			);
		}
		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");

View on GitHub (pinned to 9690622007)

Solutions

  1. Make the line exactly « (unique match), «* (all matches), » (start rewrite), or »N (reference earlier operation) — nothing else on the line.
  2. If you meant a file path header, use the § section opener format (e.g. §path/to/file.ts) instead of gluing it to «.
  3. If the line is literal file content that happens to start with « or », rephrase or re-indent it so the payload is unambiguous.

Example fix

// before
«main.ts
pattern
»
replacement

// after
§main.ts
«
pattern
»
replacement
Defensive patterns

Strategy: validation

Validate before calling

for (const line of body.split("\n")) {
  const t = line.trim();
  if ((t.startsWith("«") || t.startsWith("»")) && !["«", "«*", "»"].includes(t) && !/^»[1-9]\d*$/.test(t)) {
    throw new Error(`invalid control line: ${t}`);
  }
}

Type guard

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

Try / catch

catch (err) {
  if (err instanceof Error && err.message.startsWith("Invalid control line")) {
    const bad = JSON.parse(err.message.match(/Invalid control line (".*?")/)?.[1] ?? '""');
    // strip or fix the offending line and retry
  }
}

Prevention

When it happens

Trigger: A line inside the section body starts with « or » but is not exactly «, «*, », or »[1-9] — e.g. «file.ts headers carried over from other formats, «foo arguments glued to the opener, or «*2 mixed variants.

Common situations: Models gluing file paths or parameters onto openers («main.ts); leftover headers from apply-patch-style payloads; typos like «* * or «*2; pasted text containing the guillemet characters mid-payload.

Related errors


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