import { readSettingsFile } from './settings-io.js'; /** The `henyo` block of ~/.pi/agent/settings.json (always fully merged). */ export interface HenyoSettings { /** Standalone tool repair (event hooks only) + coaching + prompt guideline. */ toolRepair: boolean; footer: boolean; agentsMd: boolean; /** Live TTFT/TPS working line during assistant streaming. */ ttftTokps: boolean; /** ttft-tokps JSONL debug trace, off by default. */ trace: boolean; /** * Fuzzy/nearest-match edit fallback (tool-repair layer): rewrites * whitespace-drifted `edits[].oldText` to file-exact bytes before execution * and enriches content-mismatch errors with nearest-match candidates / * occurrence line numbers. Event hooks only — built-in `edit` untouched. */ editFallback: boolean; /** * Compaction summary retry guard: takes over summary generation on * `session_before_compact` with a strict plain-text prompt + up to 3 * targeted retries; off by default. */ compactionRetry: boolean; skills: Record; commands: Record; } export const DEFAULTS: HenyoSettings = { toolRepair: true, footer: true, agentsMd: true, ttftTokps: true, trace: false, editFallback: true, compactionRetry: false, skills: { plans: true, notes: true }, commands: { cwd: true, newp: true }, }; export function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === 'object' && !Array.isArray(value); } /** * Merge the on-disk `henyo` block over DEFAULTS (nested `skills`/`commands` * merged key-by-key so a partial user block never hides default siblings). * Returns a fresh copy of the merged settings (module-level DEFAULTS is * never exposed or mutated) and whether the input was missing any of the * 13 known keys (`toolRepair`, `footer`, `agentsMd`, * `ttftTokps`, `trace`, `editFallback`, `compactionRetry`, skills×2, * commands×2) — values are never a write trigger. */ export function mergeHenyo(user: unknown): { henyo: HenyoSettings; changed: boolean } { const henyo: HenyoSettings = { ...DEFAULTS }; if (!isPlainObject(user)) { henyo.skills = { ...DEFAULTS.skills }; henyo.commands = { ...DEFAULTS.commands }; return { henyo, changed: true }; } const h = user as Partial; henyo.toolRepair = h.toolRepair ?? DEFAULTS.toolRepair; henyo.footer = h.footer ?? DEFAULTS.footer; henyo.agentsMd = h.agentsMd ?? DEFAULTS.agentsMd; henyo.ttftTokps = h.ttftTokps ?? DEFAULTS.ttftTokps; henyo.trace = h.trace ?? DEFAULTS.trace; henyo.editFallback = h.editFallback ?? DEFAULTS.editFallback; henyo.compactionRetry = h.compactionRetry ?? DEFAULTS.compactionRetry; const userSkills = isPlainObject(h.skills) ? h.skills : {}; const userCommands = isPlainObject(h.commands) ? h.commands : {}; henyo.skills = { ...DEFAULTS.skills, ...userSkills }; henyo.commands = { ...DEFAULTS.commands, ...userCommands }; const knownKeys: (keyof HenyoSettings)[] = [ 'toolRepair', 'footer', 'agentsMd', 'ttftTokps', 'trace', 'editFallback', 'compactionRetry', 'skills', 'commands', ]; const changed = knownKeys.some((key) => !(key in h)) || Object.keys(DEFAULTS.skills).some((key) => !(key in userSkills)) || Object.keys(DEFAULTS.commands).some((key) => !(key in userCommands)); return { henyo, changed }; } /** * Effective henyo settings (merged with defaults, user values preferred). * Read-only: never writes the settings file (use loadHenyoSettings from * index.ts for the fill-on-first-load behavior). */ export function getEffectiveHenyoSettings(): HenyoSettings { return mergeHenyo(readSettingsFile().henyo).henyo; }