/** YAML roster entries, skill metadata, model references, and inherited tools. */ import { parse } from 'yaml' export const CONFIG = '/etc/sausage/config.yaml' export const thinkingLevels = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const export type Thinking = typeof thinkingLevels[number] export interface Definition { name: string description: string path: string instructions: string model: string thinking?: Thinking tools?: string[] addTools?: string[] denyTools?: string[] isolate?: 'none' | 'worktree' resident?: boolean fast?: boolean summaries?: boolean icon?: string tint?: string } export function parseFrontmatter(text: string) { const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/.exec(text) if (!match) throw new Error('Expected frontmatter fields between --- lines') return {frontmatter:parse(match[1]) as Record, body:match[2].trim()} } export function parseRoster(text: string, path: string) { const value = parse(text) if (!value || !Array.isArray(value.roster)) throw new Error(`${path}: roster must be a list`) const names = new Set() const definitions = value.roster.map((entry: any): Definition => { if (!entry || typeof entry.name !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(entry.name)) throw new Error(`${path}: invalid roster name`) if (names.has(entry.name)) throw new Error(`${path}: duplicate agent ${entry.name}`) names.add(entry.name) for (const key of ['description', 'prompt', 'model']) if (typeof entry[key] !== 'string' || !entry[key].trim()) throw new Error(`${path}: ${entry.name}.${key} is required`) if (entry.thinking !== undefined && !thinkingLevels.includes(entry.thinking)) throw new Error(`${path}: invalid thinking for ${entry.name}`) for (const key of ['tools', 'addTools', 'denyTools']) if (entry[key] !== undefined && (!Array.isArray(entry[key]) || !entry[key].every((tool: any)=>typeof tool === 'string'))) throw new Error(`${path}: ${entry.name}.${key} must be a list of tool names`) const {prompt, ...settings} = entry const definition = {...settings, path, instructions:prompt} definitionRuns(definition) return definition }) return {...value, definitions} } export function parseConfig(text: string) { const value = parseRoster(text, CONFIG) if (typeof value.prompt !== 'string') throw new Error(`${CONFIG}: prompt must be a string`) if (!value.definitions.some((entry: Definition)=>entry.name === value.standing)) throw new Error(`${CONFIG}: standing must name a roster member`) return value } export function definitionRuns(definition: Definition) { if (!/^[^/\s]+\/\S+$/.test(definition.model)) throw new Error(`${definition.path}: invalid model for ${definition.name}`) const slash = definition.model.indexOf('/') return {model:{provider:definition.model.slice(0,slash),modelId:definition.model.slice(slash+1)},thinking:definition.thinking} } export function toolsFor(definition: Definition, parent: readonly string[]): string[] { return [...new Set([...(definition.tools ?? parent), ...(definition.addTools ?? [])])].filter(tool => !definition.denyTools?.includes(tool)) }