using System.Text.Json.Serialization; namespace AcDream.Headless.Configuration; internal sealed class HeadlessConfiguration { [JsonRequired] public int Version { get; init; } public HeadlessProcessSettings Process { get; init; } = new(); [JsonRequired] public List Sessions { get; init; } = []; } internal sealed class HeadlessProcessSettings { public HeadlessPathOverrides Paths { get; init; } = new(); public HeadlessContentDescriptor? Content { get; init; } } internal sealed class HeadlessContentDescriptor { [JsonRequired] public string DatDirectory { get; init; } = string.Empty; [JsonRequired] public string PreparedAssetPath { get; init; } = string.Empty; public string? PreparedAssetOverlayPath { get; init; } public uint? PreparedAssetBaseRecipeVersion { get; init; } public uint? PreparedAssetEffectiveRecipeVersion { get; init; } } // MF-1 (Campaign OP OP7 review fix, 2026-08-11): record, not class — the // direct-CLI launch path (HeadlessProcessHost.WithAccount) needs a `with` // expression so adding a future property can't silently drop it from a // hand-copied clone the way `CharacterOptions` was dropped here (the K3 // direct-credential launch mode reached HeadlessSessionHost with // CharacterOptions == null, silently no-op'ing the whole OP7 feature). internal sealed record HeadlessSessionDescriptor { [JsonRequired] public string Id { get; init; } = string.Empty; [JsonRequired] public HeadlessEndpointDescriptor Endpoint { get; init; } = new(); [JsonRequired] public string Account { get; init; } = string.Empty; /// /// Campaign LA slice LA2: the JSON field is ABSENT for normal play /// sessions (explicit JSON null is invalid); /// for the LA2 probe /// (connect → characterList → graceful disconnect, never EnterWorld) — /// the pinned launch-contract schema's mode field /// (docs/plans/2026-08-14-launcher-campaign.md LA1/LA2). /// / requiredness depends on /// this value, which is why their requiredness lives in /// 's semantic validation rather /// than a [JsonRequired] attribute — that attribute fires during /// deserialization, before can be inspected at all. /// public HeadlessSessionMode? Mode { get; init; } /// /// Required for play sessions ( absent); MUST be /// omitted for probe sessions () — /// the pinned contract keeps the shape unambiguous by forbidding a probe /// session from also declaring a selector. Enforced by /// , not /// [JsonRequired] (see this record's own doc on ). /// public HeadlessCharacterSelector? Character { get; init; } /// Same mode-dependent requiredness as : /// required for play sessions, forbidden for probe sessions. public HeadlessBotPolicyDescriptor? Policy { get; init; } [JsonRequired] public HeadlessCredentialReference Credential { get; init; } = new(); /// /// Campaign OP slice OP7 (2026-08-11), D8: optional declared character- /// option overrides. Keys MUST be exact CharacterOptionId enum- /// member spellings drawn from the lane-B tier-1+tier-2 bot-relevant /// subset (docs/research/2026-08-10-character-options-map.md §5.2/§7.3) /// — rejects everything else /// at load, before any name can reach the wire (ACE throws /// KeyNotFoundException server-side on an unmodelled id — set- /// character-options-wire.md §5.4.2). Dictionary VALUES bypass the /// loader's camelCase property-naming policy entirely (only C# property /// names go through that policy; JSON object keys inside a /// Dictionary<string, TValue> are read verbatim), so the /// exact PascalCase enum spelling is what the config file must contain. /// null (the field entirely absent) and an empty object are both /// legal no-ops. /// public Dictionary? CharacterOptions { get; init; } /// /// Campaign LA slice LA1: plugin ids to load from the standard plugins /// directory (docs/plans/2026-08-14-launcher-campaign.md LA1). /// Absent means load every discovered plugin (the developer flow); /// explicit empty means load none. LA5 host composition consumes this /// as the actual allow-list filter. /// public List? Plugins { get; init; } /// /// Campaign LA slice LA1/LA6: ordered chat-typed strings run through the /// shared Runtime parser/router once the session enters world. /// public List? LoginCommands { get; init; } /// /// Campaign LA slice LA1: inter-command delay for /// , in milliseconds. Matches the pinned /// launch-contract default (500 ms) when the field is absent from the /// document. /// public int LoginCommandDelayMs { get; init; } = 500; /// /// Campaign LA slice LA1: absolute path for this session's status-event /// JSONL stream (docs/superpowers/specs/2026-08-14-launcher-campaign-design.md /// §6). Absent selects the writer's permanent no-op mode. /// public string? StatusFile { get; init; } } internal sealed class HeadlessEndpointDescriptor { [JsonRequired] public string Host { get; init; } = string.Empty; [JsonRequired] public int Port { get; init; } } internal sealed class HeadlessCharacterSelector { public int? Index { get; init; } public uint? Id { get; init; } public string? Name { get; init; } } internal sealed class HeadlessBotPolicyDescriptor { [JsonRequired] public string Id { get; init; } = string.Empty; /// /// Campaign FA slice FA6: optional role discriminator for a policy that /// coordinates two bots run from the SAME process config (e.g. the /// fellowship/allegiance gate's Leader — fellowship leader AND /// allegiance patron — vs Recruit — fellowship recruit AND allegiance /// vassal, docs/research/2026-08-11-fa-acdream-seams.md §6.2). Ignored /// by every policy id that doesn't need it (all five pre-FA6 policies); /// /// rejects a missing role for a policy id that requires one. /// public HeadlessBotPolicyRole? Role { get; init; } } /// /// Campaign LA slice LA2: see . /// The pinned launch-contract schema defines exactly two states for a /// session — ABSENT (mapped to , meaning "play") or /// the literal string "probe" — so is the only /// member; there is no explicit "play" spelling. This deliberately uses /// 's global camel-case, /// string-only enum converter; a per-enum converter with its default options /// would accidentally accept numeric 0 as a second probe spelling. /// internal enum HeadlessSessionMode { Probe, } /// See . [JsonConverter(typeof(JsonStringEnumConverter))] internal enum HeadlessBotPolicyRole { /// Fellowship leader / allegiance patron — creates the /// fellowship, recruits the Recruit bot, and receives its oath. Leader, /// Fellowship recruit / allegiance vassal — gets recruited and /// swears to the Leader bot. Recruit, } [JsonConverter(typeof(JsonStringEnumConverter))] internal enum HeadlessCredentialProviderKind { Environment, StandardInput, File, } internal sealed class HeadlessCredentialReference { [JsonRequired] public HeadlessCredentialProviderKind Provider { get; init; } [JsonRequired] public string Reference { get; init; } = string.Empty; }