@agimon-ai/po-config 0.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/dist/agentPluginMcp.cjs +322 -0
  4. package/dist/agentPluginMcp.cjs.map +1 -0
  5. package/dist/agentPluginMcp.d.mts +38 -0
  6. package/dist/agentPluginMcp.d.mts.map +1 -0
  7. package/dist/agentPluginMcp.mjs +318 -0
  8. package/dist/agentPluginMcp.mjs.map +1 -0
  9. package/dist/apiContracts.cjs +203 -0
  10. package/dist/apiContracts.cjs.map +1 -0
  11. package/dist/apiContracts.d.mts +53 -0
  12. package/dist/apiContracts.d.mts.map +1 -0
  13. package/dist/apiContracts.mjs +198 -0
  14. package/dist/apiContracts.mjs.map +1 -0
  15. package/dist/atomicJson-1W0WhuFF.mjs +44 -0
  16. package/dist/atomicJson-1W0WhuFF.mjs.map +1 -0
  17. package/dist/atomicJson-Dt2z_DXl.cjs +52 -0
  18. package/dist/atomicJson-Dt2z_DXl.cjs.map +1 -0
  19. package/dist/atomicJson.cjs +3 -0
  20. package/dist/atomicJson.d.mts +2 -0
  21. package/dist/atomicJson.mjs +2 -0
  22. package/dist/config-BUw5kHsx.cjs +219 -0
  23. package/dist/config-BUw5kHsx.cjs.map +1 -0
  24. package/dist/config-CKA07FxG.mjs +162 -0
  25. package/dist/config-CKA07FxG.mjs.map +1 -0
  26. package/dist/config-Cm3EV1jp.d.mts +320 -0
  27. package/dist/config-Cm3EV1jp.d.mts.map +1 -0
  28. package/dist/config-NSHRd6na.mjs +2 -0
  29. package/dist/config-zZz80Mh5.cjs +1 -0
  30. package/dist/config.cjs +24 -0
  31. package/dist/config.d.mts +3 -0
  32. package/dist/config.mjs +3 -0
  33. package/dist/configPolicy--TNX7feH.mjs +718 -0
  34. package/dist/configPolicy--TNX7feH.mjs.map +1 -0
  35. package/dist/configPolicy-DmsZEp0t.cjs +795 -0
  36. package/dist/configPolicy-DmsZEp0t.cjs.map +1 -0
  37. package/dist/configSchema.cjs +0 -0
  38. package/dist/configSchema.d.mts +2 -0
  39. package/dist/configSchema.mjs +1 -0
  40. package/dist/configWriter-BTuinLjs.cjs +150 -0
  41. package/dist/configWriter-BTuinLjs.cjs.map +1 -0
  42. package/dist/configWriter-BgkGaLrf.mjs +130 -0
  43. package/dist/configWriter-BgkGaLrf.mjs.map +1 -0
  44. package/dist/configWriter.cjs +5 -0
  45. package/dist/configWriter.d.mts +2 -0
  46. package/dist/configWriter.mjs +2 -0
  47. package/dist/domains-CBYkuxs5.cjs +727 -0
  48. package/dist/domains-CBYkuxs5.cjs.map +1 -0
  49. package/dist/domains-DZOu9vYM.mjs +633 -0
  50. package/dist/domains-DZOu9vYM.mjs.map +1 -0
  51. package/dist/domains.cjs +19 -0
  52. package/dist/domains.d.mts +3 -0
  53. package/dist/domains.mjs +3 -0
  54. package/dist/extensions/mcp-ui.mjs +101 -0
  55. package/dist/extensions/mcp-ui.mjs.map +1 -0
  56. package/dist/extensions/mcp.cjs +171 -0
  57. package/dist/extensions/mcp.cjs.map +1 -0
  58. package/dist/extensions/mcp.d.mts +5 -0
  59. package/dist/extensions/mcp.d.mts.map +1 -0
  60. package/dist/extensions/mcp.mjs +166 -0
  61. package/dist/extensions/mcp.mjs.map +1 -0
  62. package/dist/extensions/server.cjs +504 -0
  63. package/dist/extensions/server.cjs.map +1 -0
  64. package/dist/extensions/server.d.mts +5 -0
  65. package/dist/extensions/server.d.mts.map +1 -0
  66. package/dist/extensions/server.mjs +499 -0
  67. package/dist/extensions/server.mjs.map +1 -0
  68. package/dist/harnessState-CCyBeutB.cjs +188 -0
  69. package/dist/harnessState-CCyBeutB.cjs.map +1 -0
  70. package/dist/harnessState-Dn_S7fB9.mjs +163 -0
  71. package/dist/harnessState-Dn_S7fB9.mjs.map +1 -0
  72. package/dist/harnessState.cjs +6 -0
  73. package/dist/harnessState.d.mts +2 -0
  74. package/dist/harnessState.mjs +2 -0
  75. package/dist/harnessStore-CJSxdSkV.cjs +343 -0
  76. package/dist/harnessStore-CJSxdSkV.cjs.map +1 -0
  77. package/dist/harnessStore-CltwLB7M.mjs +262 -0
  78. package/dist/harnessStore-CltwLB7M.mjs.map +1 -0
  79. package/dist/harnessStore.cjs +15 -0
  80. package/dist/harnessStore.d.mts +2 -0
  81. package/dist/harnessStore.mjs +2 -0
  82. package/dist/index-BAaTySaM.d.mts +35 -0
  83. package/dist/index-BAaTySaM.d.mts.map +1 -0
  84. package/dist/index-BT29GZx5.d.mts +142 -0
  85. package/dist/index-BT29GZx5.d.mts.map +1 -0
  86. package/dist/index-B_lp0W3U.d.mts +114 -0
  87. package/dist/index-B_lp0W3U.d.mts.map +1 -0
  88. package/dist/index-C6GaYjq-.d.mts +6 -0
  89. package/dist/index-C6GaYjq-.d.mts.map +1 -0
  90. package/dist/index-CIQrKS1A.d.mts +50 -0
  91. package/dist/index-CIQrKS1A.d.mts.map +1 -0
  92. package/dist/index-CRcoVYXz.d.mts +39 -0
  93. package/dist/index-CRcoVYXz.d.mts.map +1 -0
  94. package/dist/index-CVlciCJF.d.mts +40 -0
  95. package/dist/index-CVlciCJF.d.mts.map +1 -0
  96. package/dist/index-CeOk2kqs.d.mts +118 -0
  97. package/dist/index-CeOk2kqs.d.mts.map +1 -0
  98. package/dist/index-Ch7ptOkW.d.mts +54 -0
  99. package/dist/index-Ch7ptOkW.d.mts.map +1 -0
  100. package/dist/index-Cvdt-Y-i.d.mts +66 -0
  101. package/dist/index-Cvdt-Y-i.d.mts.map +1 -0
  102. package/dist/index-DJpG-I9K.d.mts +122 -0
  103. package/dist/index-DJpG-I9K.d.mts.map +1 -0
  104. package/dist/index-DNk4q0Qm.d.mts +81 -0
  105. package/dist/index-DNk4q0Qm.d.mts.map +1 -0
  106. package/dist/index.cjs +121 -0
  107. package/dist/index.d.mts +15 -0
  108. package/dist/index.mjs +15 -0
  109. package/dist/init-C_9nyp3-.cjs +448 -0
  110. package/dist/init-C_9nyp3-.cjs.map +1 -0
  111. package/dist/init-DJ6wm-dr.mjs +415 -0
  112. package/dist/init-DJ6wm-dr.mjs.map +1 -0
  113. package/dist/init.cjs +7 -0
  114. package/dist/init.d.mts +2 -0
  115. package/dist/init.mjs +2 -0
  116. package/dist/layeredConfig-B922ibgn.mjs +67 -0
  117. package/dist/layeredConfig-B922ibgn.mjs.map +1 -0
  118. package/dist/layeredConfig-DdI5ysUf.cjs +94 -0
  119. package/dist/layeredConfig-DdI5ysUf.cjs.map +1 -0
  120. package/dist/layeredConfig.cjs +6 -0
  121. package/dist/layeredConfig.d.mts +2 -0
  122. package/dist/layeredConfig.mjs +2 -0
  123. package/dist/majorModes-CmPLEpMT.mjs +333 -0
  124. package/dist/majorModes-CmPLEpMT.mjs.map +1 -0
  125. package/dist/majorModes-DPhVMuhT.cjs +383 -0
  126. package/dist/majorModes-DPhVMuhT.cjs.map +1 -0
  127. package/dist/majorModes.cjs +10 -0
  128. package/dist/majorModes.d.mts +2 -0
  129. package/dist/majorModes.mjs +2 -0
  130. package/dist/piConfig-Bx26HMb_.d.mts +33 -0
  131. package/dist/piConfig-Bx26HMb_.d.mts.map +1 -0
  132. package/dist/piConfig-CCBdZXQr.cjs +260 -0
  133. package/dist/piConfig-CCBdZXQr.cjs.map +1 -0
  134. package/dist/piConfig-qGyCbcVj.mjs +197 -0
  135. package/dist/piConfig-qGyCbcVj.mjs.map +1 -0
  136. package/dist/piConfig.cjs +12 -0
  137. package/dist/piConfig.d.mts +2 -0
  138. package/dist/piConfig.mjs +2 -0
  139. package/dist/piContext.cjs +18 -0
  140. package/dist/piContext.d.mts +3 -0
  141. package/dist/piContext.mjs +2 -0
  142. package/dist/profiles-CTrUCmIh.d.mts +27 -0
  143. package/dist/profiles-CTrUCmIh.d.mts.map +1 -0
  144. package/dist/profiles-DnF9DBpd.cjs +552 -0
  145. package/dist/profiles-DnF9DBpd.cjs.map +1 -0
  146. package/dist/profiles-fujQKrS0.mjs +483 -0
  147. package/dist/profiles-fujQKrS0.mjs.map +1 -0
  148. package/dist/profiles.cjs +13 -0
  149. package/dist/profiles.d.mts +25 -0
  150. package/dist/profiles.d.mts.map +1 -0
  151. package/dist/profiles.mjs +2 -0
  152. package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
  153. package/dist/selectionSwitch.cjs +70 -0
  154. package/dist/selectionSwitch.cjs.map +1 -0
  155. package/dist/selectionSwitch.d.mts +30 -0
  156. package/dist/selectionSwitch.d.mts.map +1 -0
  157. package/dist/selectionSwitch.mjs +64 -0
  158. package/dist/selectionSwitch.mjs.map +1 -0
  159. package/dist/sessionConfig-Boh4Fdje.cjs +342 -0
  160. package/dist/sessionConfig-Boh4Fdje.cjs.map +1 -0
  161. package/dist/sessionConfig-CtLoc-lO.mjs +240 -0
  162. package/dist/sessionConfig-CtLoc-lO.mjs.map +1 -0
  163. package/dist/types.cjs +9 -0
  164. package/dist/types.d.mts +2 -0
  165. package/dist/types.mjs +2 -0
  166. package/llms.txt +12 -0
  167. package/package.json +206 -0
  168. package/src/prompts/po-author-config/SKILL.md +26 -0
  169. package/src/prompts/po-author-config/agents/openai.yaml +4 -0
  170. package/src/prompts/po-author-config/references/config-contract.md +201 -0
@@ -0,0 +1,795 @@
1
+ let yaml = require("yaml");
2
+ //#region src/services/configPolicy/index.ts
3
+ const DEFAULT_PROJECT_TRUST = "ask";
4
+ const DEFAULT_VOICE_MODE = "legacy";
5
+ const DEFAULT_VOICE_ENGINE = "auto";
6
+ const DEFAULT_VOICE_LANGUAGE = "auto";
7
+ const DEFAULT_RECORDER_DEVICE = "none:default";
8
+ const ROOT_KEYS = [
9
+ "modes",
10
+ "computerUse",
11
+ "projectTrust",
12
+ "editor",
13
+ "voice",
14
+ "selection",
15
+ "web",
16
+ "native"
17
+ ];
18
+ const SELECTION_KEYS = [
19
+ "majorMode",
20
+ "domains",
21
+ "profile"
22
+ ];
23
+ const MODE_KEYS = ["planning", "autocompact"];
24
+ const PLANNING_KEYS = [
25
+ "main",
26
+ "subagents",
27
+ "plansDirectory"
28
+ ];
29
+ const AGENT_KEYS = ["model", "thinking"];
30
+ const AUTOCOMPACT_KEYS = [
31
+ "enabled",
32
+ "model",
33
+ "thinking",
34
+ "thresholds",
35
+ "overrides"
36
+ ];
37
+ const AUTOCOMPACT_THRESHOLD_KEYS = [
38
+ "pass1",
39
+ "pass2",
40
+ "pass3"
41
+ ];
42
+ const AUTOCOMPACT_OVERRIDE_KEYS = ["model", "tokens"];
43
+ const COMPUTER_USE_KEYS = ["enabled"];
44
+ /** A pass that fires below this is noise; one above it never fires before Pi compacts natively. */
45
+ const MIN_AUTOCOMPACT_RATIO = .05;
46
+ const MAX_AUTOCOMPACT_RATIO = .99;
47
+ /**
48
+ * A checkpoint at zero tokens fires before the session has said anything. There is
49
+ * deliberately no ceiling: the package takes the lower of the ratio and the token
50
+ * value, so one larger than the window is inert rather than dangerous.
51
+ */
52
+ const MIN_AUTOCOMPACT_TOKENS = 1;
53
+ const EDITOR_KEYS = ["command"];
54
+ const VOICE_KEYS = [
55
+ "mode",
56
+ "engine",
57
+ "language",
58
+ "recorder",
59
+ "adapters",
60
+ "autoCapture"
61
+ ];
62
+ const RECORDER_KEYS = ["binary", "device"];
63
+ const WHISPER_CPP_ENGINE = "whisper-cpp";
64
+ const OPENAI_WHISPER_ENGINE = "openai-whisper";
65
+ const MLX_WHISPER_ENGINE = "mlx-whisper";
66
+ const MACOS_SAY_TTS_ENGINE = "macos-say";
67
+ const ADAPTERS_KEYS = [
68
+ WHISPER_CPP_ENGINE,
69
+ OPENAI_WHISPER_ENGINE,
70
+ MLX_WHISPER_ENGINE
71
+ ];
72
+ const ADAPTER_KEYS = ["binary", "model"];
73
+ const MODEL_KEYS = ["path", "id"];
74
+ const AUTO_CAPTURE_KEYS = [
75
+ "model",
76
+ "startPhrases",
77
+ "stopPhrases",
78
+ "composeOpenPhrases",
79
+ "composeSendPhrases",
80
+ "composeCancelPhrases",
81
+ "utteranceIdleMs",
82
+ "composeUtteranceIdleMs",
83
+ "composeNudgeMs",
84
+ "transcriptionTimeoutMs",
85
+ "tts"
86
+ ];
87
+ const TTS_KEYS = [
88
+ "engine",
89
+ "voice",
90
+ "rate"
91
+ ];
92
+ const DEFAULT_UTTERANCE_IDLE_MS = 3e3;
93
+ const MIN_UTTERANCE_IDLE_MS = 1500;
94
+ const MAX_UTTERANCE_IDLE_MS = 1e4;
95
+ /**
96
+ * The endpoint window used while a composition draft is collecting.
97
+ *
98
+ * Shorter than `utteranceIdleMs` on purpose. A turn produces exactly one transcript, so
99
+ * with the ordinary ~3 s window a short command spoken after a brief pause arrives glued
100
+ * to the end of the preceding sentence and can never be read as a command. Roughly twice
101
+ * the VAD's own 600 ms trailing-silence window: long enough that ordinary speech rhythm
102
+ * does not split a sentence, short enough that a deliberate pause does. Over-splitting is
103
+ * harmless here and only here, because draft segments are joined back together with a
104
+ * space.
105
+ */
106
+ const DEFAULT_COMPOSE_UTTERANCE_IDLE_MS = 1200;
107
+ const MIN_COMPOSE_UTTERANCE_IDLE_MS = 800;
108
+ const MAX_COMPOSE_UTTERANCE_IDLE_MS = 3e3;
109
+ /** Silence before Voice reminds the user a draft is still open. `0` disables it. */
110
+ const DEFAULT_COMPOSE_NUDGE_MS = 1e4;
111
+ const MIN_COMPOSE_NUDGE_MS = 5e3;
112
+ const MAX_COMPOSE_NUDGE_MS = 6e4;
113
+ /**
114
+ * Defaults kept as lists so the phrases that shipped first keep working.
115
+ *
116
+ * `hey doom` and `that's it` read as speech; `doom prompt` and `doom send` are what
117
+ * existing users have in their fingers. Dropping the originals would break them for no
118
+ * gain, and the cost of carrying both is two extra comparisons per command candidate.
119
+ */
120
+ const DEFAULT_COMPOSE_OPEN_PHRASES = ["hey doom", "doom prompt"];
121
+ const DEFAULT_COMPOSE_SEND_PHRASES = ["that's it", "doom send"];
122
+ const DEFAULT_COMPOSE_CANCEL_PHRASES = ["doom cancel", "scratch that"];
123
+ const DEFAULT_TRANSCRIPTION_TIMEOUT_MS = 12e4;
124
+ const MIN_TRANSCRIPTION_TIMEOUT_MS = 1e3;
125
+ const MAX_TRANSCRIPTION_TIMEOUT_MS = 12e4;
126
+ const MAX_CONTROL_PHRASES = 16;
127
+ const MAX_CONTROL_PHRASE_LENGTH = 64;
128
+ const MAX_TTS_VOICE_LENGTH = 64;
129
+ const MIN_TTS_RATE = 80;
130
+ const MAX_TTS_RATE = 500;
131
+ const MODEL_REFERENCE_PATTERN = /^[^/\s]+\/[^/\s]+(?:\/[^/\s]+)*$/u;
132
+ const PHRASE_PUNCTUATION_PATTERN = /[\p{P}\p{S}]+/gu;
133
+ /**
134
+ * The accepted values, in the order a picker should offer them.
135
+ *
136
+ * Exported because the config UI has to offer exactly what the parser accepts,
137
+ * and a second hand-written list in the contributing package would drift. The
138
+ * validating Sets below are built from these for the same reason.
139
+ */
140
+ const DOOM_PLANNING_THINKING_LEVELS = [
141
+ "off",
142
+ "minimal",
143
+ "low",
144
+ "medium",
145
+ "high",
146
+ "xhigh",
147
+ "max"
148
+ ];
149
+ const DOOM_VOICE_MODES = [DEFAULT_VOICE_MODE, "live"];
150
+ const DOOM_VOICE_ENGINES = [
151
+ DEFAULT_VOICE_ENGINE,
152
+ WHISPER_CPP_ENGINE,
153
+ OPENAI_WHISPER_ENGINE,
154
+ MLX_WHISPER_ENGINE
155
+ ];
156
+ const DOOM_VOICE_TTS_ENGINES = [MACOS_SAY_TTS_ENGINE];
157
+ const THINKING_VALUES = new Set(DOOM_PLANNING_THINKING_LEVELS);
158
+ const TRUST_VALUES = /* @__PURE__ */ new Set([
159
+ "ask",
160
+ "always",
161
+ "never"
162
+ ]);
163
+ const VOICE_MODE_VALUES = new Set(DOOM_VOICE_MODES);
164
+ const ENGINE_VALUES = new Set(DOOM_VOICE_ENGINES);
165
+ const TTS_ENGINE_VALUES = new Set(DOOM_VOICE_TTS_ENGINES);
166
+ function isObject(value) {
167
+ return typeof value === "object" && value !== null && !Array.isArray(value);
168
+ }
169
+ /**
170
+ * Where a lenient parse collects unknown keys instead of throwing.
171
+ *
172
+ * Module scope rather than a parameter threaded through the twelve nested
173
+ * parsers: `assertKeys` is the only reader, parsing is synchronous end to end,
174
+ * and `parseDoomConfig` owns the whole window in a try/finally. Threading would
175
+ * touch every parser to change behaviour in one of them.
176
+ */
177
+ let unknownKeySink;
178
+ function assertKeys(value, allowed, location, filePath) {
179
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
180
+ if (unknown.length === 0) return;
181
+ if (unknownKeySink !== void 0) {
182
+ for (const key of unknown) unknownKeySink.push({
183
+ filePath,
184
+ path: location === "root" ? key : `${location}.${key}`,
185
+ message: `unsupported ${location} field`
186
+ });
187
+ return;
188
+ }
189
+ throw new Error(`Doom config at ${filePath} has unsupported ${location} field(s): ${unknown.join(", ")}`);
190
+ }
191
+ function optionalString(value, location, filePath) {
192
+ if (value === void 0) return void 0;
193
+ if (typeof value !== "string" || value.trim().length === 0) throw new Error(`Doom config at ${filePath} requires ${location} to be a non-empty string`);
194
+ return value.trim();
195
+ }
196
+ function hasControlCharacters(value) {
197
+ for (const character of value) {
198
+ const codePoint = character.codePointAt(0);
199
+ if (codePoint !== void 0 && (codePoint <= 31 || codePoint === 127)) return true;
200
+ }
201
+ return false;
202
+ }
203
+ function boundedOptionalString(value, location, filePath, maximumLength) {
204
+ const text = optionalString(value, location, filePath);
205
+ if (!text) return void 0;
206
+ if (text.length > maximumLength) throw new Error(`Doom config at ${filePath} requires ${location} to be at most ${maximumLength} characters`);
207
+ if (hasControlCharacters(text)) throw new Error(`Doom config at ${filePath} does not allow control characters in ${location}`);
208
+ return text;
209
+ }
210
+ function parseModelReference(value, filePath) {
211
+ const location = "voice.autoCapture.model";
212
+ const model = optionalString(value, location, filePath);
213
+ if (!model || !MODEL_REFERENCE_PATTERN.test(model)) throw new Error(`Doom config at ${filePath} requires ${location} in provider/model-id form`);
214
+ return model;
215
+ }
216
+ function normalizeControlPhrase(value) {
217
+ return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(PHRASE_PUNCTUATION_PATTERN, " ").replace(/\s+/gu, " ").trim();
218
+ }
219
+ function parseControlPhrases(value, location, filePath) {
220
+ if (value === void 0) return void 0;
221
+ if (!Array.isArray(value) || value.some((phrase) => typeof phrase !== "string")) throw new Error(`Doom config at ${filePath} requires ${location} to be an array of strings`);
222
+ if (value.length > MAX_CONTROL_PHRASES) throw new Error(`Doom config at ${filePath} requires ${location} to contain at most ${MAX_CONTROL_PHRASES} phrases`);
223
+ const phrases = value.map((phrase) => phrase.trim());
224
+ const normalized = /* @__PURE__ */ new Set();
225
+ for (const phrase of phrases) {
226
+ if (phrase.length === 0) throw new Error(`Doom config at ${filePath} requires ${location} to contain non-empty strings`);
227
+ if (phrase.length > MAX_CONTROL_PHRASE_LENGTH) throw new Error(`Doom config at ${filePath} requires ${location} phrases to be at most ${MAX_CONTROL_PHRASE_LENGTH} characters`);
228
+ if (hasControlCharacters(phrase)) throw new Error(`Doom config at ${filePath} does not allow control characters in ${location}`);
229
+ const key = normalizeControlPhrase(phrase);
230
+ if (!key) throw new Error(`Doom config at ${filePath} requires ${location} to contain words`);
231
+ if (normalized.has(key)) throw new Error(`Doom config at ${filePath} does not allow normalized duplicates in ${location}`);
232
+ normalized.add(key);
233
+ }
234
+ return phrases;
235
+ }
236
+ function parseBoundedInteger(value, location, filePath, minimum, maximum) {
237
+ if (value === void 0) return void 0;
238
+ if (typeof value !== "number" || !Number.isInteger(value) || value < minimum || value > maximum) throw new Error(`Doom config at ${filePath} requires ${location} to be an integer from ${minimum} to ${maximum}`);
239
+ return value;
240
+ }
241
+ /**
242
+ * Rejects a phrase claimed by two control roles at once.
243
+ *
244
+ * Composition commands are adjudicated before stop phrases and before leading
245
+ * start-phrase removal, so a phrase shared with a later role is not ambiguous, it is
246
+ * silently unreachable. Without a screen there is nothing to show which role won.
247
+ */
248
+ function assertDisjointPhrases(left, right, leftName, rightName, filePath) {
249
+ const keys = new Set((left ?? []).map(normalizeControlPhrase));
250
+ if ((right ?? []).some((phrase) => keys.has(normalizeControlPhrase(phrase)))) throw new Error(`Doom config at ${filePath} does not allow a phrase in both ${leftName} and ${rightName}`);
251
+ }
252
+ function parseTtsConfig(value, filePath) {
253
+ const location = "voice.autoCapture.tts";
254
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
255
+ assertKeys(value, TTS_KEYS, location, filePath);
256
+ if (typeof value.engine !== "string" || !TTS_ENGINE_VALUES.has(value.engine)) throw new Error(`Doom config at ${filePath} requires ${location}.engine to be one of: ${[...TTS_ENGINE_VALUES].join(", ")}`);
257
+ const voice = boundedOptionalString(value.voice, `${location}.voice`, filePath, MAX_TTS_VOICE_LENGTH);
258
+ const rate = value.rate;
259
+ if (rate !== void 0 && (typeof rate !== "number" || !Number.isInteger(rate) || rate < MIN_TTS_RATE || rate > MAX_TTS_RATE)) throw new Error(`Doom config at ${filePath} requires ${location}.rate to be an integer from ${MIN_TTS_RATE} to ${MAX_TTS_RATE}`);
260
+ return {
261
+ engine: value.engine,
262
+ ...voice ? { voice } : {},
263
+ ...rate === void 0 ? {} : { rate }
264
+ };
265
+ }
266
+ function parseAutoCapture(value, filePath) {
267
+ if (value === void 0) return void 0;
268
+ const location = "voice.autoCapture";
269
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
270
+ assertKeys(value, AUTO_CAPTURE_KEYS, location, filePath);
271
+ const startPhrases = parseControlPhrases(value.startPhrases, `${location}.startPhrases`, filePath);
272
+ const stopPhrases = parseControlPhrases(value.stopPhrases, `${location}.stopPhrases`, filePath);
273
+ const composeOpenPhrases = parseControlPhrases(value.composeOpenPhrases, `${location}.composeOpenPhrases`, filePath);
274
+ const composeSendPhrases = parseControlPhrases(value.composeSendPhrases, `${location}.composeSendPhrases`, filePath);
275
+ const composeCancelPhrases = parseControlPhrases(value.composeCancelPhrases, `${location}.composeCancelPhrases`, filePath);
276
+ const utteranceIdleMs = parseBoundedInteger(value.utteranceIdleMs, `${location}.utteranceIdleMs`, filePath, MIN_UTTERANCE_IDLE_MS, MAX_UTTERANCE_IDLE_MS);
277
+ const composeUtteranceIdleMs = parseBoundedInteger(value.composeUtteranceIdleMs, `${location}.composeUtteranceIdleMs`, filePath, MIN_COMPOSE_UTTERANCE_IDLE_MS, MAX_COMPOSE_UTTERANCE_IDLE_MS);
278
+ const composeNudgeMs = value.composeNudgeMs === 0 ? 0 : parseBoundedInteger(value.composeNudgeMs, `${location}.composeNudgeMs`, filePath, MIN_COMPOSE_NUDGE_MS, MAX_COMPOSE_NUDGE_MS);
279
+ const transcriptionTimeoutMs = parseBoundedInteger(value.transcriptionTimeoutMs, `${location}.transcriptionTimeoutMs`, filePath, MIN_TRANSCRIPTION_TIMEOUT_MS, MAX_TRANSCRIPTION_TIMEOUT_MS);
280
+ assertDisjointPhrases(startPhrases, stopPhrases, "startPhrases", "stopPhrases", filePath);
281
+ assertDisjointPhrases(composeSendPhrases, composeCancelPhrases, "composeSendPhrases", "composeCancelPhrases", filePath);
282
+ assertDisjointPhrases(composeOpenPhrases, composeSendPhrases, "composeOpenPhrases", "composeSendPhrases", filePath);
283
+ assertDisjointPhrases(composeOpenPhrases, composeCancelPhrases, "composeOpenPhrases", "composeCancelPhrases", filePath);
284
+ assertDisjointPhrases(stopPhrases, composeSendPhrases, "stopPhrases", "composeSendPhrases", filePath);
285
+ assertDisjointPhrases(stopPhrases, composeCancelPhrases, "stopPhrases", "composeCancelPhrases", filePath);
286
+ return {
287
+ model: parseModelReference(value.model, filePath),
288
+ ...startPhrases ? { startPhrases } : {},
289
+ ...stopPhrases ? { stopPhrases } : {},
290
+ ...composeOpenPhrases ? { composeOpenPhrases } : {},
291
+ ...composeSendPhrases ? { composeSendPhrases } : {},
292
+ ...composeCancelPhrases ? { composeCancelPhrases } : {},
293
+ ...utteranceIdleMs === void 0 ? {} : { utteranceIdleMs },
294
+ ...composeUtteranceIdleMs === void 0 ? {} : { composeUtteranceIdleMs },
295
+ ...composeNudgeMs === void 0 ? {} : { composeNudgeMs },
296
+ ...transcriptionTimeoutMs === void 0 ? {} : { transcriptionTimeoutMs },
297
+ tts: parseTtsConfig(value.tts, filePath)
298
+ };
299
+ }
300
+ function parseThinking(value, location, filePath) {
301
+ if (value === void 0) return void 0;
302
+ if (typeof value !== "string" || !THINKING_VALUES.has(value)) throw new Error(`Doom config at ${filePath} requires ${location}.thinking to be one of: ${[...THINKING_VALUES].join(", ")}`);
303
+ return value;
304
+ }
305
+ function parseAgent(value, location, filePath) {
306
+ if (value === void 0) return void 0;
307
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
308
+ assertKeys(value, AGENT_KEYS, location, filePath);
309
+ const thinking = parseThinking(value.thinking, location, filePath);
310
+ const model = optionalString(value.model, `${location}.model`, filePath);
311
+ return {
312
+ ...model ? { model } : {},
313
+ ...thinking ? { thinking } : {}
314
+ };
315
+ }
316
+ /**
317
+ * A flag the cockpit can write.
318
+ *
319
+ * The settings page writes strings and nothing else, so `'true'` has to mean
320
+ * what a hand-edited `true` means. Anything else is rejected rather than
321
+ * treated as false, because a typo that silently disables a feature is the
322
+ * worst reading of it.
323
+ */
324
+ function parseFlexibleBoolean(value, location, filePath) {
325
+ if (value === void 0) return void 0;
326
+ if (typeof value === "boolean") return value;
327
+ if (value === "true") return true;
328
+ if (value === "false") return false;
329
+ throw new Error(`Doom config at ${filePath} requires ${location} to be true or false`);
330
+ }
331
+ /** Same story as the flag: a number, or the string form the settings page writes. */
332
+ function parseFlexibleRatio(value, location, filePath) {
333
+ if (value === void 0) return void 0;
334
+ const ratio = typeof value === "string" ? Number(value.trim()) : value;
335
+ if (typeof ratio !== "number" || !Number.isFinite(ratio) || ratio < MIN_AUTOCOMPACT_RATIO || ratio > MAX_AUTOCOMPACT_RATIO) throw new Error(`Doom config at ${filePath} requires ${location} to be a number from ${MIN_AUTOCOMPACT_RATIO} to ${MAX_AUTOCOMPACT_RATIO}`);
336
+ return ratio;
337
+ }
338
+ function parseAutocompactThresholds(value, location, filePath) {
339
+ if (value === void 0) return void 0;
340
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
341
+ assertKeys(value, AUTOCOMPACT_THRESHOLD_KEYS, location, filePath);
342
+ const pass1 = parseFlexibleRatio(value.pass1, `${location}.pass1`, filePath);
343
+ const pass2 = parseFlexibleRatio(value.pass2, `${location}.pass2`, filePath);
344
+ const pass3 = parseFlexibleRatio(value.pass3, `${location}.pass3`, filePath);
345
+ return {
346
+ ...pass1 === void 0 ? {} : { pass1 },
347
+ ...pass2 === void 0 ? {} : { pass2 },
348
+ ...pass3 === void 0 ? {} : { pass3 }
349
+ };
350
+ }
351
+ /** Absolute token counts, unlike the ratios, are only ever written by hand, so no string form is accepted. */
352
+ function parseAutocompactTokenCount(value, location, filePath) {
353
+ if (value === void 0) return void 0;
354
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < MIN_AUTOCOMPACT_TOKENS) throw new Error(`Doom config at ${filePath} requires ${location} to be an integer of at least ${MIN_AUTOCOMPACT_TOKENS}`);
355
+ return value;
356
+ }
357
+ function parseAutocompactOverrideEntry(value, location, filePath) {
358
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
359
+ assertKeys(value, AUTOCOMPACT_OVERRIDE_KEYS, location, filePath);
360
+ const model = optionalString(value.model, `${location}.model`, filePath);
361
+ if (!model) throw new Error(`Doom config at ${filePath} requires ${location}.model`);
362
+ if (!isObject(value.tokens)) throw new Error(`Doom config at ${filePath} requires ${location}.tokens to be an object`);
363
+ assertKeys(value.tokens, AUTOCOMPACT_THRESHOLD_KEYS, `${location}.tokens`, filePath);
364
+ const pass1 = parseAutocompactTokenCount(value.tokens.pass1, `${location}.tokens.pass1`, filePath);
365
+ const pass2 = parseAutocompactTokenCount(value.tokens.pass2, `${location}.tokens.pass2`, filePath);
366
+ const pass3 = parseAutocompactTokenCount(value.tokens.pass3, `${location}.tokens.pass3`, filePath);
367
+ if (pass1 === void 0 && pass2 === void 0 && pass3 === void 0) throw new Error(`Doom config at ${filePath} requires one of ${location}.tokens.pass1, pass2, or pass3`);
368
+ return {
369
+ model,
370
+ tokens: {
371
+ ...pass1 === void 0 ? {} : { pass1 },
372
+ ...pass2 === void 0 ? {} : { pass2 },
373
+ ...pass3 === void 0 ? {} : { pass3 }
374
+ }
375
+ };
376
+ }
377
+ function parseAutocompactOverrides(value, location, filePath) {
378
+ if (value === void 0) return void 0;
379
+ if (!Array.isArray(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be a list`);
380
+ return value.map((entry, index) => parseAutocompactOverrideEntry(entry, `${location}[${index}]`, filePath));
381
+ }
382
+ function parseAutocompactModeConfig(value, filePath, location = "modes.autocompact") {
383
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
384
+ assertKeys(value, AUTOCOMPACT_KEYS, location, filePath);
385
+ const enabled = parseFlexibleBoolean(value.enabled, `${location}.enabled`, filePath);
386
+ const model = optionalString(value.model, `${location}.model`, filePath);
387
+ const thinking = parseThinking(value.thinking, location, filePath);
388
+ const thresholds = parseAutocompactThresholds(value.thresholds, `${location}.thresholds`, filePath);
389
+ const overrides = parseAutocompactOverrides(value.overrides, `${location}.overrides`, filePath);
390
+ return {
391
+ ...enabled === void 0 ? {} : { enabled },
392
+ ...model ? { model } : {},
393
+ ...thinking ? { thinking } : {},
394
+ ...thresholds ? { thresholds } : {},
395
+ ...overrides ? { overrides } : {}
396
+ };
397
+ }
398
+ function parsePlanningModeConfig(value, filePath, location = "modes.planning") {
399
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
400
+ assertKeys(value, PLANNING_KEYS, location, filePath);
401
+ return {
402
+ main: parseAgent(value.main, `${location}.main`, filePath),
403
+ subagents: parseAgent(value.subagents, `${location}.subagents`, filePath),
404
+ plansDirectory: optionalString(value.plansDirectory, `${location}.plansDirectory`, filePath)
405
+ };
406
+ }
407
+ function parseModel(value, location, filePath, allowId) {
408
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
409
+ assertKeys(value, MODEL_KEYS, location, filePath);
410
+ const modelPath = optionalString(value.path, `${location}.path`, filePath);
411
+ const id = optionalString(value.id, `${location}.id`, filePath);
412
+ if (Boolean(modelPath) === Boolean(id)) throw new Error(`Doom config at ${filePath} requires exactly one of ${location}.path or ${location}.id`);
413
+ if (id && !allowId) throw new Error(`Doom config at ${filePath} does not support ${location}.id for whisper-cpp`);
414
+ return {
415
+ ...modelPath ? { path: modelPath } : {},
416
+ ...id ? { id } : {}
417
+ };
418
+ }
419
+ function parseAdapter(value, location, filePath, allowId) {
420
+ if (value === void 0) return void 0;
421
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
422
+ assertKeys(value, ADAPTER_KEYS, location, filePath);
423
+ const binary = optionalString(value.binary, `${location}.binary`, filePath);
424
+ return {
425
+ ...binary ? { binary } : {},
426
+ model: parseModel(value.model, `${location}.model`, filePath, allowId)
427
+ };
428
+ }
429
+ function parseVoice(value, filePath) {
430
+ if (value === void 0) return void 0;
431
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires voice to be an object`);
432
+ assertKeys(value, VOICE_KEYS, "voice", filePath);
433
+ const result = {};
434
+ if (value.mode !== void 0) {
435
+ if (typeof value.mode !== "string" || !VOICE_MODE_VALUES.has(value.mode)) throw new Error(`Doom config at ${filePath} requires voice.mode to be one of: ${[...VOICE_MODE_VALUES].join(", ")}`);
436
+ result.mode = value.mode;
437
+ }
438
+ if (value.engine !== void 0) {
439
+ if (typeof value.engine !== "string" || !ENGINE_VALUES.has(value.engine)) throw new Error(`Doom config at ${filePath} requires voice.engine to be one of: ${[...ENGINE_VALUES].join(", ")}`);
440
+ result.engine = value.engine;
441
+ }
442
+ const language = optionalString(value.language, "voice.language", filePath);
443
+ if (language) result.language = language;
444
+ if (value.recorder !== void 0) {
445
+ if (!isObject(value.recorder)) throw new Error(`Doom config at ${filePath} requires voice.recorder to be an object`);
446
+ assertKeys(value.recorder, RECORDER_KEYS, "voice.recorder", filePath);
447
+ result.recorder = {};
448
+ const binary = optionalString(value.recorder.binary, "voice.recorder.binary", filePath);
449
+ const device = optionalString(value.recorder.device, "voice.recorder.device", filePath);
450
+ if (binary) result.recorder.binary = binary;
451
+ if (device) result.recorder.device = device;
452
+ }
453
+ if (value.adapters !== void 0) {
454
+ if (!isObject(value.adapters)) throw new Error(`Doom config at ${filePath} requires voice.adapters to be an object`);
455
+ assertKeys(value.adapters, ADAPTERS_KEYS, "voice.adapters", filePath);
456
+ result.adapters = {
457
+ [WHISPER_CPP_ENGINE]: parseAdapter(value.adapters[WHISPER_CPP_ENGINE], `voice.adapters.${WHISPER_CPP_ENGINE}`, filePath, false),
458
+ [OPENAI_WHISPER_ENGINE]: parseAdapter(value.adapters[OPENAI_WHISPER_ENGINE], `voice.adapters.${OPENAI_WHISPER_ENGINE}`, filePath, true),
459
+ [MLX_WHISPER_ENGINE]: parseAdapter(value.adapters[MLX_WHISPER_ENGINE], `voice.adapters.${MLX_WHISPER_ENGINE}`, filePath, true)
460
+ };
461
+ }
462
+ const autoCapture = parseAutoCapture(value.autoCapture, filePath);
463
+ if (autoCapture) result.autoCapture = autoCapture;
464
+ return result;
465
+ }
466
+ function parseSelection(value, filePath) {
467
+ if (value === void 0) return void 0;
468
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires selection to be an object`);
469
+ assertKeys(value, SELECTION_KEYS, "selection", filePath);
470
+ const domains = value.domains;
471
+ if (domains !== void 0 && (!Array.isArray(domains) || domains.some((name) => typeof name !== "string"))) throw new Error(`Doom config at ${filePath} requires selection.domains to be an array of strings`);
472
+ return {
473
+ majorMode: optionalString(value.majorMode, "selection.majorMode", filePath),
474
+ domains,
475
+ profile: optionalString(value.profile, "selection.profile", filePath)
476
+ };
477
+ }
478
+ function parseEditor(value, filePath) {
479
+ if (value === void 0) return void 0;
480
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires editor to be an object`);
481
+ assertKeys(value, EDITOR_KEYS, "editor", filePath);
482
+ return { command: optionalString(value.command, "editor.command", filePath) };
483
+ }
484
+ /** `web` and `native` each choose a template, so both read the same `{ template }` shape. */
485
+ function parseTemplateChoice(value, root, filePath) {
486
+ if (value === void 0) return void 0;
487
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${root} to be an object`);
488
+ assertKeys(value, ["template"], root, filePath);
489
+ if (value.template === void 0) return {};
490
+ const template = optionalString(value.template, `${root}.template`, filePath);
491
+ if (!template || template.length > 160 || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(template)) throw new Error(`Doom config at ${filePath} requires ${root}.template to be a kebab-case template identifier`);
492
+ return { template };
493
+ }
494
+ function parseComputerUse(value, filePath) {
495
+ if (value === void 0) return void 0;
496
+ if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires computerUse to be an object`);
497
+ assertKeys(value, COMPUTER_USE_KEYS, "computerUse", filePath);
498
+ const enabled = parseFlexibleBoolean(value.enabled, "computerUse.enabled", filePath);
499
+ return enabled === void 0 ? {} : { enabled };
500
+ }
501
+ function parseDoomConfig(content, filePath, options) {
502
+ if (options?.lenient !== true) return parseStrict(content, filePath);
503
+ const diagnostics = [];
504
+ const previous = unknownKeySink;
505
+ unknownKeySink = diagnostics;
506
+ try {
507
+ return {
508
+ config: parseStrict(content, filePath),
509
+ diagnostics
510
+ };
511
+ } finally {
512
+ unknownKeySink = previous;
513
+ }
514
+ }
515
+ function parseStrict(content, filePath) {
516
+ let parsed;
517
+ try {
518
+ parsed = (0, yaml.parse)(content) ?? {};
519
+ } catch (error) {
520
+ throw new Error(`Could not parse Doom config at ${filePath}`, { cause: error });
521
+ }
522
+ if (!isObject(parsed)) throw new Error(`Doom config at ${filePath} must be a YAML object`);
523
+ assertKeys(parsed, ROOT_KEYS, "root", filePath);
524
+ let modes;
525
+ if (parsed.modes !== void 0) {
526
+ if (!isObject(parsed.modes)) throw new Error(`Doom config at ${filePath} requires modes to be an object`);
527
+ assertKeys(parsed.modes, MODE_KEYS, "modes", filePath);
528
+ modes = {
529
+ ...parsed.modes.planning === void 0 ? {} : { planning: parsePlanningModeConfig(parsed.modes.planning, filePath) },
530
+ ...parsed.modes.autocompact === void 0 ? {} : { autocompact: parseAutocompactModeConfig(parsed.modes.autocompact, filePath) }
531
+ };
532
+ }
533
+ const trust = parsed.projectTrust === void 0 ? DEFAULT_PROJECT_TRUST : parsed.projectTrust;
534
+ if (typeof trust !== "string" || !TRUST_VALUES.has(trust)) throw new Error(`Doom config at ${filePath} projectTrust must be ask, always, or never`);
535
+ return {
536
+ modes,
537
+ computerUse: parseComputerUse(parsed.computerUse, filePath),
538
+ projectTrust: trust,
539
+ editor: parseEditor(parsed.editor, filePath),
540
+ voice: parseVoice(parsed.voice, filePath),
541
+ selection: parseSelection(parsed.selection, filePath),
542
+ web: parseTemplateChoice(parsed.web, "web", filePath),
543
+ native: parseTemplateChoice(parsed.native, "native", filePath)
544
+ };
545
+ }
546
+ function mergeAgent(globalValue, repositoryValue) {
547
+ return globalValue || repositoryValue ? {
548
+ ...globalValue,
549
+ ...repositoryValue
550
+ } : void 0;
551
+ }
552
+ function mergeDoomConfigs(globalConfig, repositoryConfig) {
553
+ if (repositoryConfig.voice?.mode) throw new Error("Repository Doom config voice.mode is global-only; configure it in ~/.po/config.yaml");
554
+ if (repositoryConfig.voice?.autoCapture) throw new Error("Repository Doom config voice.autoCapture is global-only; configure it in ~/.po/config.yaml");
555
+ if (repositoryConfig.computerUse) throw new Error("Repository Doom config computerUse is global-only; configure it in ~/.po/config.yaml");
556
+ const globalPlanning = globalConfig.modes?.planning;
557
+ const repositoryPlanning = repositoryConfig.modes?.planning;
558
+ const planning = globalPlanning || repositoryPlanning ? {
559
+ main: mergeAgent(globalPlanning?.main, repositoryPlanning?.main),
560
+ subagents: mergeAgent(globalPlanning?.subagents, repositoryPlanning?.subagents),
561
+ plansDirectory: repositoryPlanning?.plansDirectory ?? globalPlanning?.plansDirectory
562
+ } : void 0;
563
+ const globalAutocompact = globalConfig.modes?.autocompact;
564
+ const repositoryAutocompact = repositoryConfig.modes?.autocompact;
565
+ const autocompact = globalAutocompact || repositoryAutocompact ? {
566
+ ...globalAutocompact,
567
+ ...repositoryAutocompact,
568
+ ...globalAutocompact?.thresholds || repositoryAutocompact?.thresholds ? { thresholds: {
569
+ ...globalAutocompact?.thresholds,
570
+ ...repositoryAutocompact?.thresholds
571
+ } } : {}
572
+ } : void 0;
573
+ const voice = mergeVoice(globalConfig.voice, repositoryConfig.voice);
574
+ return {
575
+ modes: planning || autocompact ? {
576
+ ...planning ? { planning } : {},
577
+ ...autocompact ? { autocompact } : {}
578
+ } : void 0,
579
+ computerUse: globalConfig.computerUse,
580
+ projectTrust: repositoryConfig.projectTrust,
581
+ editor: globalConfig.editor,
582
+ voice,
583
+ web: globalConfig.web || repositoryConfig.web ? { template: repositoryConfig.web?.template ?? globalConfig.web?.template } : void 0,
584
+ native: globalConfig.native || repositoryConfig.native ? { template: repositoryConfig.native?.template ?? globalConfig.native?.template } : void 0,
585
+ selection: globalConfig.selection || repositoryConfig.selection ? {
586
+ ...globalConfig.selection,
587
+ ...repositoryConfig.selection
588
+ } : void 0
589
+ };
590
+ }
591
+ function mergeAdapter(globalValue, repositoryValue) {
592
+ if (!globalValue && !repositoryValue) return void 0;
593
+ return {
594
+ ...globalValue,
595
+ ...repositoryValue,
596
+ model: repositoryValue?.model ?? globalValue.model
597
+ };
598
+ }
599
+ function mergeVoice(globalValue, repositoryValue) {
600
+ if (!globalValue && !repositoryValue) return void 0;
601
+ return {
602
+ ...globalValue,
603
+ ...repositoryValue,
604
+ ...globalValue?.mode ? { mode: globalValue.mode } : {},
605
+ recorder: globalValue?.recorder || repositoryValue?.recorder ? {
606
+ ...globalValue?.recorder,
607
+ ...repositoryValue?.recorder
608
+ } : void 0,
609
+ adapters: globalValue?.adapters || repositoryValue?.adapters ? {
610
+ [WHISPER_CPP_ENGINE]: mergeAdapter(globalValue?.adapters?.[WHISPER_CPP_ENGINE], repositoryValue?.adapters?.[WHISPER_CPP_ENGINE]),
611
+ [OPENAI_WHISPER_ENGINE]: mergeAdapter(globalValue?.adapters?.[OPENAI_WHISPER_ENGINE], repositoryValue?.adapters?.[OPENAI_WHISPER_ENGINE]),
612
+ [MLX_WHISPER_ENGINE]: mergeAdapter(globalValue?.adapters?.[MLX_WHISPER_ENGINE], repositoryValue?.adapters?.[MLX_WHISPER_ENGINE])
613
+ } : void 0,
614
+ ...globalValue?.autoCapture ? { autoCapture: globalValue.autoCapture } : {}
615
+ };
616
+ }
617
+ /**
618
+ * Resolves voice settings, optionally shadowed by the active profile's override.
619
+ *
620
+ * The override is per field, so a persona that sets only `rate` keeps the voice
621
+ * from config.yaml. `engine` is deliberately not overridable: it selects the
622
+ * synthesiser binary, which is a machine-level fact rather than a persona's.
623
+ *
624
+ * The result is read once when autonomous voice activates, because
625
+ * `VoiceNarrationPlayback.config` is assigned in `activate()` and has no setter.
626
+ * That is sufficient today only because every profile switch reloads the
627
+ * session, which deactivates and reactivates voice. A future in-place profile
628
+ * switch would leave the spoken voice stale until the next activation.
629
+ */
630
+ function resolveVoiceConfig(config, ttsOverride) {
631
+ return {
632
+ mode: config.mode ?? DEFAULT_VOICE_MODE,
633
+ engine: config.engine ?? DEFAULT_VOICE_ENGINE,
634
+ language: config.language ?? DEFAULT_VOICE_LANGUAGE,
635
+ recorder: {
636
+ ...config.recorder,
637
+ device: config.recorder?.device ?? DEFAULT_RECORDER_DEVICE
638
+ },
639
+ adapters: config.adapters ?? {},
640
+ ...config.autoCapture ? { autoCapture: {
641
+ ...config.autoCapture,
642
+ startPhrases: [...config.autoCapture.startPhrases ?? []],
643
+ stopPhrases: [...config.autoCapture.stopPhrases ?? []],
644
+ composeOpenPhrases: [...config.autoCapture.composeOpenPhrases ?? DEFAULT_COMPOSE_OPEN_PHRASES],
645
+ composeSendPhrases: [...config.autoCapture.composeSendPhrases ?? DEFAULT_COMPOSE_SEND_PHRASES],
646
+ composeCancelPhrases: [...config.autoCapture.composeCancelPhrases ?? DEFAULT_COMPOSE_CANCEL_PHRASES],
647
+ utteranceIdleMs: config.autoCapture.utteranceIdleMs ?? DEFAULT_UTTERANCE_IDLE_MS,
648
+ composeUtteranceIdleMs: config.autoCapture.composeUtteranceIdleMs ?? DEFAULT_COMPOSE_UTTERANCE_IDLE_MS,
649
+ composeNudgeMs: config.autoCapture.composeNudgeMs ?? DEFAULT_COMPOSE_NUDGE_MS,
650
+ transcriptionTimeoutMs: config.autoCapture.transcriptionTimeoutMs ?? DEFAULT_TRANSCRIPTION_TIMEOUT_MS,
651
+ tts: {
652
+ ...config.autoCapture.tts,
653
+ ...ttsOverride?.voice === void 0 ? {} : { voice: ttsOverride.voice },
654
+ ...ttsOverride?.rate === void 0 ? {} : { rate: ttsOverride.rate }
655
+ }
656
+ } } : {}
657
+ };
658
+ }
659
+ /** Root keys the merge reads from one side only. */
660
+ const GLOBAL_ONLY_ROOTS = ["computerUse", "editor"];
661
+ const REPOSITORY_ONLY_ROOTS = ["projectTrust"];
662
+ /** Nested exceptions; a repository declaration of either throws in the merge. */
663
+ const GLOBAL_ONLY_PATHS = [["voice", "mode"], ["voice", "autoCapture"]];
664
+ function startsWith(keyPath, prefix) {
665
+ return prefix.length <= keyPath.length && prefix.every((segment, index) => keyPath[index] === segment);
666
+ }
667
+ /**
668
+ * The scopes a key path accepts. An unknown root answers `both` rather than
669
+ * throwing: the parser is what rejects unsupported keys, and it reports them
670
+ * far better than a scope lookup could.
671
+ */
672
+ function configScopeOf(keyPath) {
673
+ if (keyPath.length === 0) return "both";
674
+ if (GLOBAL_ONLY_PATHS.some((prefix) => startsWith(keyPath, prefix))) return "global";
675
+ const root = keyPath[0];
676
+ if (GLOBAL_ONLY_ROOTS.includes(root)) return "global";
677
+ if (REPOSITORY_ONLY_ROOTS.includes(root)) return "repository";
678
+ return "both";
679
+ }
680
+ /** Every root key the config accepts, so a caller can enumerate without importing the parser's internals. */
681
+ function configRootKeys() {
682
+ return ROOT_KEYS;
683
+ }
684
+ const KEY_SEPARATOR = ".";
685
+ function isPlainRecord(value) {
686
+ return typeof value === "object" && value !== null && !Array.isArray(value);
687
+ }
688
+ /**
689
+ * Every key path a parsed document actually sets, as dotted strings.
690
+ *
691
+ * A settings surface needs to tell "this file set this key" from "this file is
692
+ * silent about it", and the parsed `DoomConfig` cannot answer: an absent file
693
+ * reads as `{ projectTrust: 'ask' }` and `parseDoomConfig` always returns all
694
+ * known root keys, some undefined. So the question is asked of the raw document
695
+ * instead, before any defaulting has happened.
696
+ *
697
+ * Leaves only. A list is a leaf because it is replaced whole, and an empty
698
+ * record sets nothing.
699
+ */
700
+ function configLeafKeys(value, prefix = []) {
701
+ if (!isPlainRecord(value)) return prefix.length > 0 ? [prefix.join(KEY_SEPARATOR)] : [];
702
+ const keys = Object.keys(value);
703
+ if (keys.length === 0) return prefix.length > 0 ? [prefix.join(KEY_SEPARATOR)] : [];
704
+ return keys.flatMap((key) => configLeafKeys(value[key], [...prefix, key]));
705
+ }
706
+ /** The value a key path points at, or undefined when any step is missing. */
707
+ function valueAtKeyPath(value, keyPath) {
708
+ let current = value;
709
+ for (const segment of keyPath) {
710
+ if (!isPlainRecord(current)) return void 0;
711
+ current = current[segment];
712
+ }
713
+ return current;
714
+ }
715
+ //#endregion
716
+ Object.defineProperty(exports, "DOOM_PLANNING_THINKING_LEVELS", {
717
+ enumerable: true,
718
+ get: function() {
719
+ return DOOM_PLANNING_THINKING_LEVELS;
720
+ }
721
+ });
722
+ Object.defineProperty(exports, "DOOM_VOICE_ENGINES", {
723
+ enumerable: true,
724
+ get: function() {
725
+ return DOOM_VOICE_ENGINES;
726
+ }
727
+ });
728
+ Object.defineProperty(exports, "DOOM_VOICE_MODES", {
729
+ enumerable: true,
730
+ get: function() {
731
+ return DOOM_VOICE_MODES;
732
+ }
733
+ });
734
+ Object.defineProperty(exports, "DOOM_VOICE_TTS_ENGINES", {
735
+ enumerable: true,
736
+ get: function() {
737
+ return DOOM_VOICE_TTS_ENGINES;
738
+ }
739
+ });
740
+ Object.defineProperty(exports, "configLeafKeys", {
741
+ enumerable: true,
742
+ get: function() {
743
+ return configLeafKeys;
744
+ }
745
+ });
746
+ Object.defineProperty(exports, "configRootKeys", {
747
+ enumerable: true,
748
+ get: function() {
749
+ return configRootKeys;
750
+ }
751
+ });
752
+ Object.defineProperty(exports, "configScopeOf", {
753
+ enumerable: true,
754
+ get: function() {
755
+ return configScopeOf;
756
+ }
757
+ });
758
+ Object.defineProperty(exports, "mergeDoomConfigs", {
759
+ enumerable: true,
760
+ get: function() {
761
+ return mergeDoomConfigs;
762
+ }
763
+ });
764
+ Object.defineProperty(exports, "parseAutocompactModeConfig", {
765
+ enumerable: true,
766
+ get: function() {
767
+ return parseAutocompactModeConfig;
768
+ }
769
+ });
770
+ Object.defineProperty(exports, "parseDoomConfig", {
771
+ enumerable: true,
772
+ get: function() {
773
+ return parseDoomConfig;
774
+ }
775
+ });
776
+ Object.defineProperty(exports, "parsePlanningModeConfig", {
777
+ enumerable: true,
778
+ get: function() {
779
+ return parsePlanningModeConfig;
780
+ }
781
+ });
782
+ Object.defineProperty(exports, "resolveVoiceConfig", {
783
+ enumerable: true,
784
+ get: function() {
785
+ return resolveVoiceConfig;
786
+ }
787
+ });
788
+ Object.defineProperty(exports, "valueAtKeyPath", {
789
+ enumerable: true,
790
+ get: function() {
791
+ return valueAtKeyPath;
792
+ }
793
+ });
794
+
795
+ //# sourceMappingURL=configPolicy-DmsZEp0t.cjs.map