{"record":{"id":"fdc7b5177f42d60c","repo":"can1357/oh-my-pi","slug":"path-entry-entry-has-an-invalid-selector","errorCode":null,"errorMessage":"path entry \"${entry}\" has an invalid selector \":${internalSplit.sel}\" — use \":N-M\" line ranges, \":raw\"/\":conflicts\", a range plus \":raw\", or percent-encode a literal \":\" as %3A","messagePattern":"path entry \"(.+?)\" has an invalid selector \":(.+?)\" — use \":N-M\" line ranges, \":raw\"/\":conflicts\", a range plus \":raw\", or percent-encode a literal \":\" as %3A","errorType":"validation","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/grep.ts","lineNumber":172,"sourceCode":"\tconst lower = sel.toLowerCase();\n\treturn lower === \"raw\" || lower === \"conflicts\" || parseLineRanges(sel) !== null;\n}\n\nasync function parsePathSpecs(rawEntries: readonly string[], cwd: string): Promise<GrepPathSpec[]> {\n\tconst specs: GrepPathSpec[] = [];\n\tfor (const entry of rawEntries) {\n\t\t// Internal URLs (`artifact://`, `skill://`, …) use the URL-aware splitter,\n\t\t// which peels selector-shaped tails only for selector-capable schemes and\n\t\t// leaves opaque ones (`mcp://`) intact. Unlike filesystem paths, their\n\t\t// verbatim/index display modes (`raw`, `conflicts`) carry no meaning for\n\t\t// content search, so we accept them — searching the whole resource — and\n\t\t// still honor any embedded line range as a match filter.\n\t\tconst internalSplit = splitInternalUrlSel(entry);\n\t\tif (internalSplit.sel !== undefined) {\n\t\t\t// Reject selectors read's parseSel would reject (`:-10`, `:1-1:1-2`,\n\t\t\t// `:conflicts:1-1`) instead of silently widening the search or dropping a chunk.\n\t\t\tif (!isReadSelectorGrammar(internalSplit.sel)) {\n\t\t\t\tthrow new ToolError(\n\t\t\t\t\t`path entry \"${entry}\" has an invalid selector \":${internalSplit.sel}\" — use \":N-M\" line ranges, \":raw\"/\":conflicts\", a range plus \":raw\", or percent-encode a literal \":\" as %3A`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tspecs.push({ original: entry, clean: internalSplit.path, ranges: selectorLineRanges(internalSplit.sel) });\n\t\t\tcontinue;\n\t\t}\n\t\t// Prefer a literal filesystem match when one exists — a real file named\n\t\t// `test:1-2` outranks the `:1-2` selector interpretation (issue #4618).\n\t\tconst strictSplit = splitPathAndSel(entry);\n\t\tconst split = await splitPathAndSelPreferringLiteral(entry, cwd);\n\t\tconst literalFilesystemMatch = strictSplit.sel !== undefined && split.sel === undefined;\n\t\tlet clean = literalFilesystemMatch ? resolveReadPath(entry, cwd) : entry;\n\t\tlet ranges: [LineRange, ...LineRange[]] | undefined;\n\t\tif (!literalFilesystemMatch && split.sel) {\n\t\t\tconst parsed = parseLineRanges(split.sel);\n\t\t\tif (!parsed) {\n\t\t\t\tthrow new ToolError(\n\t\t\t\t\t`path entry \"${entry}\" — only line-range selectors like \":50-100\" are supported (no \":raw\"/\":conflicts\")`,","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/grep.ts#L154-L190","documentation":"grep's parsePathSpecs accepts internal-URL-style selectors on path entries (via splitInternalUrlSel). If the selector does not match the read-selector grammar (:N-M ranges, :raw, :conflicts, range+:raw), the entry is rejected with guidance rather than silently widened or dropped.","triggerScenarios":"A path entry like 'file.ts:conflicts:1-1', ': -10' style selectors, or any selector read's parseSel would reject; also a literal ':' in a filename that was not percent-encoded.","commonSituations":"Copy-pasting read-tool selectors into grep paths; filenames containing colons (e.g. 'my:file.txt') misinterpreted as selectors; hand-built path lists from other tools.","solutions":["Use only supported selectors: ':N-M' line ranges, ':raw', ':conflicts', or range plus ':raw'","Percent-encode a literal colon in a filename as %3A","Simplify to a plain path with at most a ':N-M' range"],"exampleFix":"// before\nawait grepTool.execute({ pattern: \"foo\", path: \"src/my:file.txt\" })\n// after\nawait grepTool.execute({ pattern: \"foo\", path: \"src/my%3Afile.txt\" })","handlingStrategy":"validation","validationCode":"function validateGrepPathEntry(entry: string): string {\n  const m = /^(.*?):([^:].*)$/.exec(entry);\n  if (m && !/^\\d+-\\d+$/.test(m[2]) && m[2] !== \"raw\" && m[2] !== \"conflicts\") {\n    // not a plain path, line range, raw, or conflicts -> encode literal colons\n    return entry.replace(/:/g, \"%3A\");\n  }\n  return entry;\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await grepTool.execute({ pattern, path: entries });\n} catch (err) {\n  if (err instanceof ToolError && err.message.includes(\"invalid selector\")) {\n    // retry with colons in literal filenames percent-encoded\n    return await grepTool.execute({ pattern, path: entries.map(e => e.replace(/:/g, \"%3A\")) });\n  }\n  throw err;\n}","preventionTips":["Only use read-grammar selectors: :N-M, :raw, :conflicts, or range+:raw","Percent-encode literal ':' in filenames as %3A","Sanitize path lists built from other tools' output before passing to grep"],"tags":["parsing","selector-syntax","grep-tool"],"backgroundTag":"invalid-path-selector","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}