{"record":{"id":"020b954dad619af8","repo":"can1357/oh-my-pi","slug":"trimmed-is-not-a-valid-opener-use-opener-wi","errorCode":null,"errorMessage":"${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.","messagePattern":"(.+?) is not a valid opener\\. Use (.+?) with a pattern that matches once — add context only the intended match has — or (.+?)\\* to change every match\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/edit/sloppy.ts","lineNumber":1313,"sourceCode":"\t\t// A multiline pattern-only block may be the delete half of a move; assume\n\t\t// deletion now, justified post-parse only when another op re-emits it.\n\t\tconst normalizedPattern = normalizeText(sourcePatternText).text;\n\t\tif (!sourcePatternText.includes(\"\\n\") || normalizedPattern.length < 24) {\n\t\t\tthrow new Error(needsSeparator);\n\t\t}\n\t\tconst operation = createOperation(sourcePatternText, \"\", allMatches, operations.length + 1, true);\n\t\toperation.assumedDeletion = true;\n\t\tpendingSeparatorErrors.set(operations.length, needsSeparator);\n\t\toperations.push(operation);\n\t};\n\n\tfor (let index = 0; index < lines.length; index++) {\n\t\tconst line = lines[index];\n\t\tconst parsedOpener = parseOpener(line);\n\t\tconst trimmed = line.trim();\n\t\tconst registerReference = trimmed.match(/^»([1-9]\\d*)$/u);\n\t\tif (isOrdinalOpener(line)) {\n\t\t\tthrow new Error(\n\t\t\t\t`${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.`,\n\t\t\t);\n\t\t}\n\t\tif (trimmed === `${OPENER}${REWRITE_HEADER}`) {\n\t\t\t// A glued «» line: after MATCH content it is a mistyped » separator;\n\t\t\t// anywhere else it is a stray operation terminator to drop.\n\t\t\tif (state === \"pattern\" && patternLines.some(entry => entry.trim() !== \"\")) state = \"rewrite\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (\n\t\t\tparsedOpener === false &&\n\t\t\t(trimmed.startsWith(OPENER) ||\n\t\t\t\t(trimmed.startsWith(REWRITE_HEADER) && trimmed !== REWRITE_HEADER && !registerReference))\n\t\t) {\n\t\t\tthrow new Error(\n\t\t\t\t`Invalid control line ${JSON.stringify(trimmed)}; use only ${OPENER}, ${OPENER}*, ${REWRITE_HEADER}, or ${REWRITE_HEADER}N in REWRITE.`,\n\t\t\t);\n\t\t}","sourceCodeStart":1295,"sourceCodeEnd":1331,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/edit/sloppy.ts#L1295-L1331","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n«2\nold line\n»\nnew line\n\n// after\n«\nfunction context() {\n  old line\n}\n»\nfunction context() {\n  new line\n}","handlingStrategy":"validation","validationCode":"const badOrdinal = /^«[1-9]\\d*$/m;\nif (badOrdinal.test(body)) throw new Error(\"numbered openers («N) are invalid; use « or «*\");","typeGuard":"const isValidOpener = (line: string) => line.trim() === \"«\" || line.trim() === \"«*\";","tryCatchPattern":"catch (err) {\n  if (err instanceof Error && err.message.includes(\"is not a valid opener\")) {\n    body = body.replace(/^«\\d+\\s*$/gm, \"«\");\n  }\n}","preventionTips":["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."],"tags":["parser","edit-format","invalid-opener"],"backgroundTag":"invalid-opener-syntax","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}