{"record":{"id":"7df918ab25d8148e","repo":"can1357/oh-my-pi","slug":"write-target-target-is-a-semicolon-joined-lis","errorCode":null,"errorMessage":"write target '${target}' is a semicolon-joined list of ${count} read-tool selectors, not a filesystem path — refusing to create it. write creates a single file; issue one read() per path to read these ranges (e.g. read({ path: \"<one path>:<range>\" })).","messagePattern":"write target '(.+?)' is a semicolon-joined list of (.+?) read-tool selectors, not a filesystem path — refusing to create it\\. write creates a single file; issue one read\\(\\) per path to read these ranges \\(e\\.g\\. read\\((.+?)\\)\\)\\.","errorType":"validation","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/write.ts","lineNumber":196,"sourceCode":" * fires regardless of `content` — the non-empty-content escape hatch exists for\n * a lone selector-shaped *filename*, never a `;`-list, and honoring it here\n * silently creates a nested directory tree (`a.txt:1-2;b/`) in the workspace.\n * The caller still probes the literal target first, so an existing POSIX file\n * by that exact name stays writable (same escape as the single-selector guard).\n */\nfunction readSelectorListMisfire(target: string): number | undefined {\n\tif (!target.includes(\";\")) return undefined;\n\tconst segments = target.split(\";\");\n\tif (segments.length < 2) return undefined;\n\tfor (const segment of segments) {\n\t\tconst trimmed = segment.trim();\n\t\tif (trimmed.length === 0 || splitPathAndSel(trimmed).sel === undefined) return undefined;\n\t}\n\treturn segments.length;\n}\n\nfunction throwReadSelectorListMisfire(target: string, count: number): never {\n\tthrow new ToolError(\n\t\t`write target '${target}' is a semicolon-joined list of ${count} read-tool selectors, not a filesystem path — refusing to create it. ` +\n\t\t\t`write creates a single file; issue one read() per path to read these ranges (e.g. read({ path: \"<one path>:<range>\" })).`,\n\t);\n}\n\nasync function assertNotReadSelectorMisfire(target: string, content: string, cwd: string): Promise<void> {\n\tconst listCount = readSelectorListMisfire(target);\n\tif (listCount !== undefined && (await probeLiteralPathExists(target, cwd)) === \"missing\") {\n\t\tthrowReadSelectorListMisfire(target, listCount);\n\t}\n\tconst sel = readSelectorForEmptyWrite(target, content);\n\tif (sel === undefined) return;\n\tif ((await probeLiteralPathExists(target, cwd)) !== \"missing\") return;\n\tthrowReadSelectorMisfire(target, sel);\n}\n\nconst BULK_DIRECTIVE_RE = /^#?(\\d+)\\s*[:=]\\s*(@ours|@theirs|@base|@both)$/;\n/**","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/write.ts#L178-L214","documentation":"The write target looks like multiple read-tool selectors joined by semicolons (e.g. 'a.ts:1-5;b.ts:6-9'). write only creates a single file, so it refuses to create such a literal path and points you at per-path read() calls.","triggerScenarios":"write({ path: \"a.ts:1-5; b.ts:6-9\", content: \"\" }) — target parses into ≥2 selector segments.","commonSituations":"Batch-style attempts to read or touch several file ranges in one call, or models concatenating read paths with semicolons.","solutions":["Issue one read() per path/range instead of a single write","Split into separate write() calls, one plain file path each"],"exampleFix":"// before\nwrite({ path: \"a.ts:1-5;b.ts:6-9\", content: \"\" })\n// after\nread({ path: \"a.ts:1-5\" }); read({ path: \"b.ts:6-9\" })","handlingStrategy":"validation","validationCode":"function looksLikeSelectorList(target: string): boolean {\n  const segs = target.trim().split(\";\").map(s => s.trim()).filter(Boolean);\n  if (segs.length < 2) return false;\n  return segs.every(s => splitPathAndSel(s).sel !== undefined);\n}\nif (looksLikeSelectorList(p)) {\n  for (const s of p.split(\";\")) await read({ path: s.trim() });\n}","typeGuard":"function isSinglePath(t: string): boolean {\n  return t.trim().split(\";\").length === 1;\n}","tryCatchPattern":"try {\n  await write({ path: target, content });\n} catch (e) {\n  if (e instanceof ToolError && e.message.includes(\"semicolon-joined list\")) {\n    for (const seg of target.split(\";\")) await read({ path: seg.trim() });\n    return;\n  }\n  throw e;\n}","preventionTips":["One write() call per file — never join multiple paths with semicolons","Use one read() per path/range for batch reads","Keep write targets free of ';' characters"],"tags":["path-validation","read-selector","misfire"],"backgroundTag":"read-selector-misfire","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}