/** Markdown roster files, YAML settings and 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 const ROSTER = '/etc/sausage/roster' export function parseDefinition(text: string, path: string, inherited?: Definition): Definition { const {frontmatter, body} = parseFrontmatter(text) const name = path.split('/').pop()!.replace(/\.md$/, '') const settings = {...(inherited ?? {}), ...frontmatter} const instructions = inherited && frontmatter.addTools !== undefined ? [inherited.instructions, body].filter(Boolean).join('\n\n## Project additions\n\n') : body const definition = {...settings, name, path, instructions} as Definition if (!/^[a-zA-Z0-9_-]+$/.test(name)) throw new Error(`${path}: invalid roster name`) for (const key of ['description','model','instructions']) if (typeof definition[key] !== 'string' || !definition[key].trim()) throw new Error(`${path}: ${key} is required`) if (definition.thinking !== undefined && !thinkingLevels.includes(definition.thinking)) throw new Error(`${path}: invalid thinking`) for (const key of ['tools','addTools','denyTools']) if (definition[key] !== undefined && (!Array.isArray(definition[key]) || !definition[key].every((tool: any)=>typeof tool === 'string'))) throw new Error(`${path}: ${key} must be a list of tool names`) if (inherited && frontmatter.addTools !== undefined) definition.tools = toolsFor(definition, inherited.tools ?? []) definitionRuns(definition) return definition } export function parseConfig(text: string) { const value = parse(text) if (typeof value?.prompt !== 'string') throw new Error(`${CONFIG}: prompt must be a string`) if (typeof value.main_agent_name !== 'string' || !value.main_agent_name) throw new Error(`${CONFIG}: main_agent_name must name a roster file`) 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)) }