{"version":3,"file":"operator-roster.d.ts","sourceRoot":"","sources":["../../src/core/operator-roster.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,eAAO,MAAM,uBAAuB,8BAAwC,CAAC;AAC7E,eAAO,MAAM,oBAAoB,eAAyB,CAAC;AAC3D,eAAO,MAAM,8BAA8B,0CAAgE,CAAC;AAE5G,0EAA0E;AAC1E,eAAO,MAAM,qBAAqB,oDAAqD,CAAC;AAWxF,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,aAAa,CAAC;AAE/D,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,yBAAyB,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,wBAAwB;IACxC,IAAI,EACD,8BAA8B,GAC9B,qCAAqC,GACrC,8BAA8B,GAC9B,gCAAgC,GAChC,iCAAiC,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,WAAW,EAAE,wBAAwB,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,yBAAyB;IACzC,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAID,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE3D;AAuBD,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,cAAc,CAoF1F","sourcesContent":["/**\n * Canonical operator roster loader.\n *\n * The operator roster is a separate, local-model authority surface from the\n * built-in subagent registry. It loads operator agent definitions from\n * `getAgentDir()/agents/*.md`, parses their frontmatter (name, description,\n * tools, model), and guarantees the canonical operator roles\n * (investigator, tester, synthesizer) always exist.\n *\n * Loading is per-file fault-tolerant: an unreadable file, malformed\n * frontmatter, a missing or duplicate name, or a non-local model produces a\n * diagnostic for that file only, and the rest of the roster loads unchanged.\n *\n * Every loaded operator agent must resolve to the local Qwen model\n * (`llamacpp-qwen38-bucephalus/qwen3.8-27b`). In strict mode (the default) a\n * file declaring a non-local model is rejected with a diagnostic and excluded\n * from the roster; the rest of the roster loads unchanged.\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { getAgentDir } from \"../config.js\";\nimport { parseFrontmatter } from \"../utils/frontmatter.js\";\n\nexport const OPERATOR_LOCAL_PROVIDER = \"llamacpp-qwen38-bucephalus\" as const;\nexport const OPERATOR_LOCAL_MODEL = \"qwen3.8-27b\" as const;\nexport const OPERATOR_LOCAL_MODEL_REFERENCE = `${OPERATOR_LOCAL_PROVIDER}/${OPERATOR_LOCAL_MODEL}` as const;\n\n/** Operator roles that must always be present in the canonical roster. */\nexport const OPERATOR_ROSTER_NAMES = [\"investigator\", \"tester\", \"synthesizer\"] as const;\n\nconst SYNTHESIZED_DESCRIPTIONS: Record<(typeof OPERATOR_ROSTER_NAMES)[number], string> = {\n\tinvestigator:\n\t\t\"Investigate one concrete question or failure against repository and runtime evidence without mutation.\",\n\ttester: \"Design and run bounded verification for completed work without mutation.\",\n\tsynthesizer: \"Synthesize child results into one coherent, decision-ready summary without mutation.\",\n};\n\nconst SYNTHESIZED_TOOLS = [\"read\", \"grep\", \"find\", \"ls\"];\n\nexport type OperatorRosterAgentSource = \"file\" | \"synthesized\";\n\nexport interface OperatorRosterAgent {\n\tname: string;\n\tdescription: string;\n\ttools: string[];\n\t/** Normalized local model reference (always the canonical local Qwen). */\n\tmodel: string;\n\tsource: OperatorRosterAgentSource;\n\tfilePath?: string;\n}\n\nexport interface OperatorRosterDiagnostic {\n\tcode:\n\t\t| \"OPERATOR_ROSTER_FILE_INVALID\"\n\t\t| \"OPERATOR_ROSTER_FRONTMATTER_INVALID\"\n\t\t| \"OPERATOR_ROSTER_NAME_MISSING\"\n\t\t| \"OPERATOR_ROSTER_NAME_DUPLICATE\"\n\t\t| \"OPERATOR_ROSTER_MODEL_NON_LOCAL\";\n\tmessage: string;\n\tpath?: string;\n}\n\nexport interface OperatorRoster {\n\tagents: OperatorRosterAgent[];\n\tdiagnostics: OperatorRosterDiagnostic[];\n}\n\nexport interface LoadOperatorRosterOptions {\n\t/** Agent config directory. Default: getAgentDir() */\n\tagentDir?: string;\n\t/**\n\t * Strict mode (default true): files declaring a non-local model are rejected\n\t * and excluded from the roster. When false the file still loads, with a\n\t * diagnostic, and keeps its declared model reference verbatim.\n\t */\n\tstrict?: boolean;\n}\n\ntype OperatorFrontmatter = Record<string, unknown>;\n\n/** Accept the local model as a bare id or the full `provider/model` reference. */\nexport function isLocalOperatorModel(value: string): boolean {\n\treturn value === OPERATOR_LOCAL_MODEL || value === OPERATOR_LOCAL_MODEL_REFERENCE;\n}\n\nfunction normalizeTools(value: unknown): string[] {\n\tif (typeof value === \"string\")\n\t\treturn value\n\t\t\t.split(\",\")\n\t\t\t.map((entry) => entry.trim())\n\t\t\t.filter((entry) => entry.length > 0);\n\tif (Array.isArray(value))\n\t\treturn value.filter((entry): entry is string => typeof entry === \"string\" && entry.trim().length > 0);\n\treturn [];\n}\n\nfunction synthesizeAgent(name: (typeof OPERATOR_ROSTER_NAMES)[number]): OperatorRosterAgent {\n\treturn {\n\t\tname,\n\t\tdescription: SYNTHESIZED_DESCRIPTIONS[name],\n\t\ttools: [...SYNTHESIZED_TOOLS],\n\t\tmodel: OPERATOR_LOCAL_MODEL_REFERENCE,\n\t\tsource: \"synthesized\",\n\t};\n}\n\nexport function loadOperatorRoster(options: LoadOperatorRosterOptions = {}): OperatorRoster {\n\tconst strict = options.strict ?? true;\n\tconst agentDir = options.agentDir ?? getAgentDir();\n\tconst agentsDir = join(agentDir, \"agents\");\n\tconst diagnostics: OperatorRosterDiagnostic[] = [];\n\tconst agents: OperatorRosterAgent[] = [];\n\tconst seen = new Map<string, OperatorRosterAgent>();\n\n\tlet files: string[] = [];\n\tif (existsSync(agentsDir) && statSync(agentsDir).isDirectory()) {\n\t\tfiles = readdirSync(agentsDir)\n\t\t\t.filter((entry) => entry.endsWith(\".md\"))\n\t\t\t.sort((left, right) => left.localeCompare(right));\n\t}\n\n\tfor (const file of files) {\n\t\tconst filePath = join(agentsDir, file);\n\t\tlet content: string;\n\t\ttry {\n\t\t\tcontent = readFileSync(filePath, \"utf-8\");\n\t\t} catch {\n\t\t\tdiagnostics.push({ code: \"OPERATOR_ROSTER_FILE_INVALID\", message: `Cannot read ${file}`, path: file });\n\t\t\tcontinue;\n\t\t}\n\t\tlet frontmatter: OperatorFrontmatter;\n\t\ttry {\n\t\t\tfrontmatter = parseFrontmatter<OperatorFrontmatter>(content).frontmatter;\n\t\t} catch (error) {\n\t\t\tdiagnostics.push({\n\t\t\t\tcode: \"OPERATOR_ROSTER_FRONTMATTER_INVALID\",\n\t\t\t\tmessage: `Malformed frontmatter in ${file}: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\tpath: file,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tconst name = typeof frontmatter.name === \"string\" ? frontmatter.name.trim() : \"\";\n\t\tif (!name) {\n\t\t\tdiagnostics.push({\n\t\t\t\tcode: \"OPERATOR_ROSTER_NAME_MISSING\",\n\t\t\t\tmessage: `Missing frontmatter name in ${file}`,\n\t\t\t\tpath: file,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (seen.has(name)) {\n\t\t\tdiagnostics.push({\n\t\t\t\tcode: \"OPERATOR_ROSTER_NAME_DUPLICATE\",\n\t\t\t\tmessage: `Duplicate operator name ${name}`,\n\t\t\t\tpath: file,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tconst description = typeof frontmatter.description === \"string\" ? frontmatter.description.trim() : \"\";\n\t\tconst declaredModel = typeof frontmatter.model === \"string\" ? frontmatter.model.trim() : \"\";\n\t\tif (declaredModel && !isLocalOperatorModel(declaredModel)) {\n\t\t\tdiagnostics.push({\n\t\t\t\tcode: \"OPERATOR_ROSTER_MODEL_NON_LOCAL\",\n\t\t\t\tmessage: `Operator ${name} declares non-local model ${declaredModel}`,\n\t\t\t\tpath: file,\n\t\t\t});\n\t\t\tif (strict) continue;\n\t\t}\n\t\tconst agent: OperatorRosterAgent = {\n\t\t\tname,\n\t\t\tdescription,\n\t\t\ttools: normalizeTools(frontmatter.tools),\n\t\t\tmodel: !declaredModel || isLocalOperatorModel(declaredModel) ? OPERATOR_LOCAL_MODEL_REFERENCE : declaredModel,\n\t\t\tsource: \"file\",\n\t\t\tfilePath: basename(file),\n\t\t};\n\t\tseen.set(name, agent);\n\t\tagents.push(agent);\n\t}\n\n\tfor (const requiredName of OPERATOR_ROSTER_NAMES) {\n\t\tif (!seen.has(requiredName)) {\n\t\t\tconst agent = synthesizeAgent(requiredName);\n\t\t\tseen.set(requiredName, agent);\n\t\t\tagents.push(agent);\n\t\t}\n\t}\n\n\tagents.sort((left, right) => left.name.localeCompare(right.name));\n\treturn { agents, diagnostics };\n}\n"]}