{"record":{"id":"3d0e04775ace5e2c","repo":"paperclipai/paperclip","slug":"roster-selection-must-contain-unique-comma-separat","errorCode":null,"errorMessage":"Roster selection must contain unique comma-separated IDs","messagePattern":"Roster selection must contain unique comma-separated IDs","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs","lineNumber":81,"sourceCode":"  }\n  if (config.provider === \"acpx\") {\n    if (config.acpxAgent === \"pi\") return \"OPENROUTER_API_KEY\";\n    if (config.acpxAgent === \"claude\") return \"ANTHROPIC_API_KEY\";\n    if (config.acpxAgent === \"codex\") return \"OPENAI_API_KEY\";\n  }\n  throw new Error(\n    `No credential policy for ${config.provider ?? \"codex\"}/${config.acpxAgent ?? \"default\"}`,\n  );\n}\n\nfunction parseRosterSelection(value) {\n  if (!value?.trim() || value.trim() === \"all\") return null;\n  const selected = value\n    .split(\",\")\n    .map((entry) => entry.trim())\n    .filter(Boolean);\n  if (selected.length === 0 || new Set(selected).size !== selected.length) {\n    throw new Error(\"Roster selection must contain unique comma-separated IDs\");\n  }\n  return new Set(selected);\n}\n\nasync function maintainedRosterSelection(programRoot) {\n  const campaignPath = resolve(programRoot, \"campaigns/live-direct-full.json\");\n  const campaign = await loadObject(campaignPath);\n  if (\n    campaign.schema !== \"paperclip-runner/live-campaign/v1\" ||\n    !Array.isArray(campaign.lanes)\n  ) {\n    throw new Error(`Unsupported live campaign schema in ${campaignPath}`);\n  }\n  const selected = campaign.lanes\n    .filter((lane) => lane.executionClass !== \"disabled\")\n    .map((lane) => {\n      const rosterPath = inside(\n        programRoot,","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs#L63-L99","documentation":"parseRosterSelection parses a comma-separated list of roster IDs and requires the list to be non-empty after trimming and free of duplicates. It backs the --rosters style selection flag for the eval campaign. Empty, all-duplicate, or malformed selections are rejected.","triggerScenarios":"Passing a selection value like 'r1,r1', 'r1,,r1' (dedupes to unique so fine, but '' entries filtered — duplicates still counted on raw trimmed list), or a value that trims to nothing but isn't empty/'all', e.g. passing '   ' with extra content or 'r1,' with duplicates elsewhere.","commonSituations":"CLI flag typed as --rosters 'a,a'; scripted invocation building the list from a source with repeated IDs; pasting a list that already contains the same roster twice.","solutions":["Remove duplicate IDs from the comma-separated list.","Ensure at least one non-empty ID remains after the commas.","Use 'all' (or omit the flag) to select the maintained roster set instead of listing IDs."],"exampleFix":"// before\nnode runner-protocol-eval-campaign.mjs --rosters \"codex-core,codex-core\"\n// after\nnode runner-protocol-eval-campaign.mjs --rosters \"codex-core,claude-core\"","handlingStrategy":"validation","validationCode":"const entries = value.split(\",\").map((s) => s.trim()).filter(Boolean);\nif (entries.length === 0 || new Set(entries).size !== entries.length) throw new Error(\"Roster selection must be unique, comma-separated IDs\");","typeGuard":"const isValidSelection = (v) => {\n  if (typeof v !== \"string\") return false;\n  const entries = v.split(\",\").map((s) => s.trim()).filter(Boolean);\n  return entries.length > 0 && new Set(entries).size === entries.length;\n};","tryCatchPattern":"try {\n  const selection = parseRosterSelection(argv.rosters);\n} catch (err) {\n  if (err.message.includes(\"unique comma-separated\")) console.error(\"Pass each roster ID once, e.g. --rosters a,b,c\");\n  throw err;\n}","preventionTips":["Deduplicate and trim the roster list before building the CLI flag in scripts.","Prefer 'all' or omitting the flag when you want the maintained set.","Keep a canonical list of roster IDs in one place and derive flags from it."],"tags":["cli","validation","duplicates"],"backgroundTag":"invalid-argument-format","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}