/** * Cross-harness support derivation: pure functions that answer "which * harnesses express this option, and under what native spelling" from the * descriptor set. Phase 3 (D7): refusal diagnostics derive support lists at * runtime instead of hardcoding them, so a descriptor edit can never leave * a refusal message stale. The autonomy refusal in argv.ts was the * counter-pattern - a hardcoded flag array that drifts on descriptor * change. * * Interpretation-layer pure function; takes the descriptor set as an * argument rather than importing the defaults, so override sets work. */ import type { HarnessDescriptor, OptionRender } from "../knowledge/descriptor.js"; import type { DescriptorSet } from "../knowledge/overrides.js"; import type { RefusalOption } from "./refusal.js"; import type { VocabularyEntry } from "./tool-vocabulary.js"; import { canonicalTable } from "./tool-vocabulary.js"; export interface SupportEntry { readonly harness: string; /** The native spelling a caller would use directly on that harness. */ readonly spelling: string; } const spellingOf = (h: HarnessDescriptor, option: RefusalOption): string | null => { switch (option) { case "access": { const spec = h.turnOptions.access; if (spec === undefined || spec.kind !== "access") return null; const read = spec.renders.read; if (read === "tool-preset") return h.tools.includeFlag ?? h.tools.excludeFlag ?? null; if (read === null) return null; if (read.render.kind === "env") return `${read.render.name}=${read.render.value}`; // RFC-05: the in-model render carries no argv spelling. Access never // renders through it; the effort arm is the in-model case below. if (read.render.kind === "in-model") return null; return read.render.kind === "flag-list" ? (read.render.flags[0] ?? null) : read.render.flag; } case "tools": return h.tools.includeFlag; case "excludeTools": return h.tools.excludeFlag; case "skills": { if (h.skills === null) return null; if (h.skills.loadFlag !== null) return h.skills.loadFlag; switch (h.skills.overridesVia) { case "settings-skilloverrides": return "skillOverrides"; case "config-skills-array": return "-c skills.config"; default: return null; } } case "autonomy": return h.autonomy?.flag ?? null; case "resumeLast": // RFC-06: the refusal and the renderable list key on headless, so // the parse-only muse never appears in its own refusal. return h.resumeLast?.headless === true ? h.resumeLast.flag : null; case "memory": { const spec = h.turnOptions.memory; if (spec === undefined) return null; const r = (spec as { readonly render: OptionRender }).render; // The env render kind is not an argv flag - the caller-visible // spelling is the assignment itself. if (r.kind === "env") return `${r.name}=${r.value}`; const flags = r.kind === "flag-list" ? r.flags : []; // pi: vacuous support - no built-in memory, nothing to disable. return flags.length > 0 ? flags.join(" ") : "(no built-in memory - already off)"; } case "isolation": case "effort": case "sandbox": case "contextWindow": case "provider": case "agent": case "write": case "shell": case "maxSteps": case "discovery": { const spec = h.turnOptions[option]; if (spec === undefined) return null; // The in-model render carries neither flag nor flags: effort on an // effort-in-model harness is expressed through the model flag. if (spec.kind === "effort-in-model") return h.vocabulary.modelFlag; const rawRender = spec.kind === "discovery" ? ( Object.values(spec.facets)[0] as | { render?: { flag?: string; flags?: string[] } } | undefined )?.render : (spec as { render?: { flag?: string; flags?: string[] } }).render; if (rawRender === undefined) return null; // flag-value/config-kv carry `flag`; flag-list carries `flags` - the // first entry is the spelling a caller would type. return rawRender.flag ?? rawRender.flags?.[0] ?? null; } default: return null; } }; /** Every harness in the set that can express `option`, with its native * spelling. Ordered by the set's insertion order (defaults: claude, codex, * pi, muse) so output is stable across calls. */ export const supportedBy = (set: DescriptorSet, option: RefusalOption): readonly SupportEntry[] => { const out: SupportEntry[] = []; for (const h of Object.values(set)) { if (h === undefined) continue; const spelling = spellingOf(h, option); if (spelling !== null) out.push({ harness: h.name, spelling }); } return out; }; export const supportedByCanonical = ( set: DescriptorSet, canonical: string, ): readonly SupportEntry[] => { const table = canonicalTable(set); const entry = table[canonical]; if (!entry) return []; const out: SupportEntry[] = []; for (const [har, val] of Object.entries(entry)) { if (val === undefined) continue; const spelling = (val as VocabularyEntry).kind === "builtin" ? (val as { kind: "builtin"; native: string }).native : (val as { kind: "category"; key: string }).key; out.push({ harness: har, spelling }); } return out; }; /** Reverse lookup for native-spelling recognition (D7 part B): given a raw * flag token a caller typed, find the option it belongs to and which * harnesses spell it that way. Returns null for tokens no descriptor * knows - those keep the plain unknown-flag error. */ export const recognizeNativeSpelling = ( set: DescriptorSet, flag: string, ): { readonly option: RefusalOption; readonly entries: readonly SupportEntry[] } | null => { const normalized = flag.toLowerCase(); const candidates: RefusalOption[] = [ "tools", "excludeTools", "autonomy", "isolation", "effort", "sandbox", "contextWindow", "provider", "write", "shell", "memory", "maxSteps", ]; for (const option of candidates) { const entries = supportedBy(set, option).filter((e) => e.spelling.toLowerCase() === normalized); if (entries.length > 0) return { option, entries }; } // Discovery facets: single-dash spellings (pi -nt/-nc/-ne/-ns) and // claude's --setting-sources. Facet spellings map to the facet name so // the redirect can name the normalized spelling. const FACET_KEYS = ["tools", "instructionFiles", "extensions", "skills"] as const; for (const facet of FACET_KEYS) { const entries: SupportEntry[] = []; for (const h of Object.values(set)) { if (h === undefined) continue; const spec = h.turnOptions.discovery; if (spec === undefined || spec.kind !== "discovery") continue; const facetSpec = spec.facets[facet]; if (facetSpec === undefined) continue; const render = facetSpec.render as { flag?: string; flags?: string[] }; const spelling = render.flag ?? render.flags?.[0]; if (spelling !== undefined && spelling.toLowerCase() === normalized) { entries.push({ harness: h.name, spelling }); } } if (entries.length > 0) { return { option: `discovery.${facet}` as RefusalOption, entries }; } } return null; };