@wrongstack/core 0.313.0 → 0.313.1

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 (182) hide show
  1. package/dist/chronicle/index.js +84 -6391
  2. package/dist/chronicle/project-server.js +84 -4868
  3. package/dist/chunk-2BUUIMQB.js +225 -0
  4. package/dist/chunk-2FK4WTFF.js +207 -0
  5. package/dist/chunk-2FQL4PDC.js +292 -0
  6. package/dist/chunk-2KKODIQU.js +2256 -0
  7. package/dist/chunk-2YU4WVBW.js +134 -0
  8. package/dist/chunk-3E244UDA.js +356 -0
  9. package/dist/chunk-3XPVB7BA.js +135 -0
  10. package/dist/chunk-4LJJYBAU.js +388 -0
  11. package/dist/chunk-4VWO3ZXL.js +342 -0
  12. package/dist/chunk-4XV6IR37.js +187 -0
  13. package/dist/chunk-4ZHTKJHM.js +183 -0
  14. package/dist/chunk-52RTVH4M.js +361 -0
  15. package/dist/chunk-57GEWK5X.js +26 -0
  16. package/dist/chunk-5DSMEVA4.js +931 -0
  17. package/dist/chunk-5RP3QTRL.js +1223 -0
  18. package/dist/chunk-5S437PRC.js +50 -0
  19. package/dist/chunk-5XDUGTKN.js +511 -0
  20. package/dist/chunk-62V2AFVG.js +422 -0
  21. package/dist/chunk-65RDIOR2.js +2470 -0
  22. package/dist/chunk-6AKOTGLM.js +597 -0
  23. package/dist/chunk-7ZHZE5EO.js +28 -0
  24. package/dist/chunk-A455ATZB.js +140 -0
  25. package/dist/chunk-AD7ZFZCS.js +46 -0
  26. package/dist/chunk-AI4LLQ2U.js +856 -0
  27. package/dist/chunk-ASX6AW36.js +789 -0
  28. package/dist/chunk-AT4V4XGI.js +3522 -0
  29. package/dist/chunk-AWNQ6ED6.js +311 -0
  30. package/dist/chunk-B4AMLZPB.js +9 -0
  31. package/dist/chunk-BAKVJLEV.js +417 -0
  32. package/dist/chunk-BJHVULHO.js +55 -0
  33. package/dist/chunk-CB2HFKTP.js +9 -0
  34. package/dist/chunk-CE4ZOCOI.js +1640 -0
  35. package/dist/chunk-CQQL4ZOQ.js +151 -0
  36. package/dist/chunk-DEIQMBD2.js +65 -0
  37. package/dist/chunk-E6I5RXVK.js +58 -0
  38. package/dist/chunk-EFEHDQGY.js +1589 -0
  39. package/dist/chunk-EODDVJBM.js +82 -0
  40. package/dist/chunk-EPC3GZD4.js +308 -0
  41. package/dist/chunk-FAYRX2AD.js +13 -0
  42. package/dist/chunk-FFSX2QDQ.js +362 -0
  43. package/dist/chunk-FHAMQS7Q.js +5856 -0
  44. package/dist/chunk-FIV4IJL7.js +2263 -0
  45. package/dist/chunk-FNXVT7HF.js +52 -0
  46. package/dist/chunk-FSWCWSTS.js +395 -0
  47. package/dist/chunk-FW5LDDMJ.js +16464 -0
  48. package/dist/chunk-FXQLIX3K.js +211 -0
  49. package/dist/chunk-G5AAKEEZ.js +228 -0
  50. package/dist/chunk-G5S3S5ZN.js +38 -0
  51. package/dist/chunk-GAZEWQEZ.js +231 -0
  52. package/dist/chunk-GFJOMSHG.js +125 -0
  53. package/dist/chunk-GFXZTHNI.js +158 -0
  54. package/dist/chunk-GM54GJBU.js +960 -0
  55. package/dist/chunk-GPQ3NJPR.js +900 -0
  56. package/dist/chunk-GTCU6DZ3.js +1 -0
  57. package/dist/chunk-H5E77KUJ.js +15 -0
  58. package/dist/chunk-H72H4XDU.js +1023 -0
  59. package/dist/chunk-HA4ZYA6T.js +178 -0
  60. package/dist/chunk-I45KAW2N.js +5020 -0
  61. package/dist/chunk-IQ35HMQL.js +1396 -0
  62. package/dist/chunk-IYFBFX34.js +86 -0
  63. package/dist/chunk-JJPMWPB6.js +109 -0
  64. package/dist/chunk-JTHKOYWU.js +663 -0
  65. package/dist/chunk-JVGPR3TS.js +365 -0
  66. package/dist/chunk-JVKB2TBL.js +48 -0
  67. package/dist/chunk-JX7VBZDK.js +31 -0
  68. package/dist/chunk-K3G6JBUC.js +5232 -0
  69. package/dist/chunk-K4YUNKU7.js +589 -0
  70. package/dist/chunk-KBZGE6LN.js +710 -0
  71. package/dist/chunk-KDQJCT2J.js +45 -0
  72. package/dist/chunk-KI4L2DUL.js +560 -0
  73. package/dist/chunk-KJ2ZTQSL.js +47 -0
  74. package/dist/chunk-KSJ5UTP7.js +154 -0
  75. package/dist/chunk-LHOA5DWA.js +28 -0
  76. package/dist/chunk-LX5QWLBL.js +27 -0
  77. package/dist/chunk-LZDDLNCZ.js +279 -0
  78. package/dist/chunk-M2WF6AXG.js +115 -0
  79. package/dist/chunk-M7BS2K6U.js +1 -0
  80. package/dist/chunk-NDBIPFNY.js +102 -0
  81. package/dist/chunk-NEMIWEXC.js +1199 -0
  82. package/dist/chunk-NFAYHPRH.js +323 -0
  83. package/dist/chunk-O37BQ65S.js +21 -0
  84. package/dist/chunk-O74MJHXU.js +187 -0
  85. package/dist/chunk-OJZEX73F.js +112 -0
  86. package/dist/chunk-PAAWLLZX.js +74 -0
  87. package/dist/chunk-PFXFQQRT.js +339 -0
  88. package/dist/chunk-PFZO3C6H.js +14 -0
  89. package/dist/chunk-PHH7LD45.js +113 -0
  90. package/dist/chunk-QRTG4XTI.js +188 -0
  91. package/dist/chunk-QVOVMXJR.js +1351 -0
  92. package/dist/chunk-RCQYT2I4.js +757 -0
  93. package/dist/chunk-RD6DEE36.js +1178 -0
  94. package/dist/chunk-RJIZEJ42.js +30 -0
  95. package/dist/chunk-RPOA7IAE.js +150 -0
  96. package/dist/chunk-SN2DQRL6.js +1172 -0
  97. package/dist/chunk-SQJVFEBV.js +41 -0
  98. package/dist/chunk-SQP64RCS.js +5391 -0
  99. package/dist/chunk-SVT4RNTR.js +49 -0
  100. package/dist/chunk-SWUFN7XM.js +43 -0
  101. package/dist/chunk-SXMVYC5H.js +174 -0
  102. package/dist/chunk-SZ6AYUIZ.js +23 -0
  103. package/dist/chunk-T3KGZBU5.js +3171 -0
  104. package/dist/chunk-TBRL2RCC.js +9 -0
  105. package/dist/chunk-TRT2HRWZ.js +18 -0
  106. package/dist/chunk-TYW2GI5I.js +1077 -0
  107. package/dist/chunk-UIWONWVG.js +4959 -0
  108. package/dist/chunk-VB3H5WWB.js +151 -0
  109. package/dist/chunk-W4A6QA3D.js +258 -0
  110. package/dist/chunk-WBPVKEEE.js +227 -0
  111. package/dist/chunk-WEBMSQP2.js +123 -0
  112. package/dist/chunk-WOEZV3Q4.js +333 -0
  113. package/dist/chunk-WQQON3SV.js +473 -0
  114. package/dist/chunk-WQTOVMGV.js +119 -0
  115. package/dist/chunk-WZO23AWO.js +2779 -0
  116. package/dist/chunk-XBDP4QPW.js +7 -0
  117. package/dist/chunk-XEBHSJLT.js +103 -0
  118. package/dist/chunk-XQ2ECNK3.js +1 -0
  119. package/dist/chunk-XQ2XGIBF.js +2086 -0
  120. package/dist/chunk-Y2GE5TSI.js +355 -0
  121. package/dist/chunk-YBUJCZ57.js +351 -0
  122. package/dist/chunk-YNCVSFGF.js +149 -0
  123. package/dist/chunk-YUPMXOGP.js +282 -0
  124. package/dist/chunk-YY6APYUM.js +49 -0
  125. package/dist/chunk-Z2IJUO45.js +691 -0
  126. package/dist/chunk-ZEHUCJHY.js +129 -0
  127. package/dist/chunk-ZYU2YOJ6.js +1496 -0
  128. package/dist/chunk-ZZ4V3WIZ.js +163 -0
  129. package/dist/coordination/agents/index.js +145 -6260
  130. package/dist/coordination/index.js +365 -34324
  131. package/dist/coordination/mailbox-project-server.js +48 -3276
  132. package/dist/coordination/provider-status-tracker.d.ts +4 -0
  133. package/dist/core/agent.d.ts +14 -1
  134. package/dist/core/index.js +267 -12480
  135. package/dist/core/model-ref.js +5 -23
  136. package/dist/core/provider-runner.d.ts +36 -1
  137. package/dist/design/index.js +50 -1087
  138. package/dist/execution/index.d.ts +1 -1
  139. package/dist/execution/index.js +1599 -21888
  140. package/dist/execution/prompt-enhancer.js +16 -535
  141. package/dist/execution/retry-policy.d.ts +31 -1
  142. package/dist/extension/index.js +7 -311
  143. package/dist/goal/index.js +33 -3435
  144. package/dist/hooks/index.js +14 -1138
  145. package/dist/hq/index.js +112 -5139
  146. package/dist/hq/protocol.js +21 -668
  147. package/dist/index.js +1410 -92656
  148. package/dist/infrastructure/index.js +64 -4811
  149. package/dist/kernel/events/provider-events.d.ts +15 -0
  150. package/dist/kernel/index.js +14 -992
  151. package/dist/models/index.js +40 -1117
  152. package/dist/notifications/index.js +3 -108
  153. package/dist/observability/index.js +17 -279
  154. package/dist/plugin/index.js +584 -14835
  155. package/dist/prompts/index.js +14 -948
  156. package/dist/registry/index.js +9 -822
  157. package/dist/registry-R23UBE7H.js +14 -0
  158. package/dist/replay/index.js +6 -84
  159. package/dist/review-finding-store-DKAV3AS6.js +21 -0
  160. package/dist/review-report-store-OWY3EC3N.js +19 -0
  161. package/dist/security/index.js +114 -2202
  162. package/dist/session-catalog/index.js +36 -2103
  163. package/dist/session-catalog/project-server.js +42 -1435
  164. package/dist/skills/index.js +35 -1578
  165. package/dist/storage/index.js +182 -15600
  166. package/dist/tasking/index.js +10 -676
  167. package/dist/tools/index.js +52 -9590
  168. package/dist/types/config/autonomy.d.ts +5 -0
  169. package/dist/types/config/ui.d.ts +1 -1
  170. package/dist/types/errors.d.ts +18 -0
  171. package/dist/types/index.js +121 -1342
  172. package/dist/types/provider-runner.d.ts +8 -0
  173. package/dist/types/session-markers.js +8 -144
  174. package/dist/utils/error.js +3 -4
  175. package/dist/utils/expect-defined.js +3 -9
  176. package/dist/utils/heap-watchdog.js +6 -706
  177. package/dist/utils/index.js +269 -5685
  178. package/dist/utils/sage-output-block.js +5 -42
  179. package/dist/wiring/proxy-rewrite.d.ts +18 -0
  180. package/dist/wiring/proxy-rewrite.js +18 -121
  181. package/dist/worktree/index.js +6 -887
  182. package/package.json +4 -4
@@ -0,0 +1,1396 @@
1
+ import {
2
+ decryptConfigSecrets
3
+ } from "./chunk-YY6APYUM.js";
4
+ import {
5
+ DEFAULT_AUTONOMY_CONFIG,
6
+ DEFAULT_CIRCUIT_BREAKER_CONFIG,
7
+ DEFAULT_CONTEXT_CONFIG,
8
+ DEFAULT_CONTEXT_WINDOW_MODE_ID,
9
+ DEFAULT_SESSION_LOGGING_CONFIG,
10
+ DEFAULT_TOOLS_CONFIG,
11
+ isContextWindowModeSelectionId,
12
+ listContextWindowModes,
13
+ normalizeContextWindowModeId
14
+ } from "./chunk-ZZ4V3WIZ.js";
15
+ import {
16
+ backupConfigFile
17
+ } from "./chunk-CQQL4ZOQ.js";
18
+ import {
19
+ safeParse
20
+ } from "./chunk-RPOA7IAE.js";
21
+ import {
22
+ FORBIDDEN_PROTO_KEYS,
23
+ deepMerge
24
+ } from "./chunk-DEIQMBD2.js";
25
+ import {
26
+ atomicWrite,
27
+ withFileLock
28
+ } from "./chunk-7ZHZE5EO.js";
29
+ import {
30
+ safeProfileName
31
+ } from "./chunk-YNCVSFGF.js";
32
+ import {
33
+ DEFAULT_TUI_THINKING_WORD
34
+ } from "./chunk-KJ2ZTQSL.js";
35
+ import {
36
+ ConfigError,
37
+ ERROR_CODES
38
+ } from "./chunk-WOEZV3Q4.js";
39
+ import {
40
+ toErrorMessage
41
+ } from "./chunk-TBRL2RCC.js";
42
+
43
+ // src/storage/config-loader/defaults.ts
44
+ var CONFIG_BEHAVIOR_DEFAULTS = {
45
+ version: 1,
46
+ context: {
47
+ mode: DEFAULT_CONTEXT_WINDOW_MODE_ID,
48
+ warnThreshold: DEFAULT_CONTEXT_CONFIG.warnThreshold,
49
+ softThreshold: DEFAULT_CONTEXT_CONFIG.softThreshold,
50
+ hardThreshold: DEFAULT_CONTEXT_CONFIG.hardThreshold,
51
+ targetLoad: DEFAULT_CONTEXT_CONFIG.targetLoad,
52
+ autoCompact: true,
53
+ preserveK: DEFAULT_CONTEXT_CONFIG.preserveK,
54
+ eliseThreshold: DEFAULT_CONTEXT_CONFIG.eliseThreshold,
55
+ strategy: "hybrid"
56
+ },
57
+ tools: {
58
+ defaultExecutionStrategy: DEFAULT_TOOLS_CONFIG.defaultExecutionStrategy,
59
+ maxIterations: DEFAULT_TOOLS_CONFIG.maxIterations,
60
+ iterationTimeoutMs: DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
61
+ maxToolTimeoutMs: DEFAULT_TOOLS_CONFIG.maxToolTimeoutMs,
62
+ sessionTimeoutMs: DEFAULT_TOOLS_CONFIG.sessionTimeoutMs,
63
+ perIterationOutputCapBytes: DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
64
+ descriptionMode: DEFAULT_TOOLS_CONFIG.descriptionMode,
65
+ disabledTools: DEFAULT_TOOLS_CONFIG.disabledTools,
66
+ autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
67
+ restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot,
68
+ kanbanGovernance: DEFAULT_TOOLS_CONFIG.kanbanGovernance,
69
+ loopDetection: DEFAULT_TOOLS_CONFIG.loopDetection
70
+ },
71
+ log: { level: "info" },
72
+ features: {
73
+ mcp: true,
74
+ plugins: true,
75
+ memory: true,
76
+ modelsRegistry: true,
77
+ skills: true,
78
+ prompts: true,
79
+ // 'auto' → resolveTokenSavingTier picks a concrete tier from the model's
80
+ // context window ONCE per session (cache-safe): lean prompt on small
81
+ // windows (<32k medium, <128k light) where the fixed identity+tool prose
82
+ // is a big fraction; minimal trimming on >=128k so modern large-window
83
+ // models still get cost savings without capability loss. Explicit tiers
84
+ // are respected verbatim.
85
+ tokenSavingMode: "auto",
86
+ // Derived, never written by hand: this and `tools.restrictToProjectRoot`
87
+ // (line 41) are the two halves of one switch, and they used to be declared
88
+ // independently in this same object — one `true`, one `true` — with
89
+ // session.ts resolving the contradiction via `??` in favour of the
90
+ // permissive half. The confinement guard therefore shipped disabled on a
91
+ // fresh install even though the other half asked for it (WS-075).
92
+ allowOutsideProjectRoot: !DEFAULT_TOOLS_CONFIG.restrictToProjectRoot
93
+ },
94
+ Sage: {
95
+ enabled: true,
96
+ storage: {
97
+ projectLocal: true,
98
+ directory: ".wrongstack/memories"
99
+ },
100
+ inject: {
101
+ // Keep the ordinary turn prompt clean. Project memory is surfaced
102
+ // on-demand beside relevant tool results, where the path/query that
103
+ // caused retrieval is known and the hint can be independently capped.
104
+ turnContext: false,
105
+ toolResults: true,
106
+ // Off: task text searches for what the operator is doing, not for the
107
+ // file the tool touched, and every match it adds is unrelated to the
108
+ // result it lands on.
109
+ taskAware: false,
110
+ maxHintsPerTool: 8,
111
+ maxCharsPerTool: 2800,
112
+ maxTurnMemories: 8,
113
+ maxCharsPerTurn: 2400,
114
+ minScore: 0.72,
115
+ minImportance: 0.5,
116
+ // 0 = once per session. Re-showing a memory the model has already been
117
+ // given spends context and adds nothing.
118
+ repeatCooldownMs: 0
119
+ },
120
+ hygiene: {
121
+ autoAfterSession: true,
122
+ autoOnFileChange: true,
123
+ retentionDays: 90,
124
+ archiveLowConfidenceAfterDays: 30
125
+ },
126
+ embeddings: {
127
+ enabled: false
128
+ }
129
+ },
130
+ skills: { readClaudeSkills: true, mode: "progressive" },
131
+ mcpServers: {},
132
+ fallbackAuto: true,
133
+ maxConcurrent: 4,
134
+ // YOLO opt-in: users who want auto-approve behaviour set `yolo: true`.
135
+ // Off by default so prompt injection in repository content, fetched
136
+ // content, or mailbox messages cannot induce arbitrary shell calls or
137
+ // write operations without human confirmation.
138
+ yolo: false,
139
+ nextPrediction: false,
140
+ hints: true,
141
+ debugStream: false,
142
+ configScope: "global",
143
+ indexing: {
144
+ onSessionStart: true,
145
+ onEdit: true,
146
+ watchExternal: true,
147
+ debounceMs: 400
148
+ },
149
+ session: { ...DEFAULT_SESSION_LOGGING_CONFIG },
150
+ autonomy: {
151
+ // 'auto' by default: the agent self-drives (picks the top next-step after
152
+ // each turn). This is a startup default, not a runtime flip — the
153
+ // autoProceedMaxIterations cap + autoProceedDelayMs cooldown + Ctrl+C /
154
+ // [GOAL_COMPLETE] stops remain in force. Explicit 'off'
155
+ // in a config file is preserved (opt-out respected).
156
+ defaultMode: "auto",
157
+ autoProceedDelayMs: DEFAULT_AUTONOMY_CONFIG.autoProceedDelayMs,
158
+ autoProceedMaxIterations: 50,
159
+ autonomyNextPrompt: "auto {{suggestion}}",
160
+ terminalTitleAnimation: true,
161
+ // Mirrored from the top-level yolo default so the autonomy subsystem
162
+ // (which reads autonomy.yolo) stays consistent with config.yolo.
163
+ yolo: false,
164
+ fleetChatVerbosity: "off",
165
+ chime: false,
166
+ confirmExit: true,
167
+ mouseMode: false,
168
+ enhance: true,
169
+ enhanceDelayMs: 6e4,
170
+ enhanceLanguage: "original",
171
+ // Product-wide statusline density default. Mirrored by the TUI's
172
+ // DEFAULT_STATUSLINE_MODE (packages/tui/src/components/settings-picker-model.ts).
173
+ statuslineMode: "minimum",
174
+ thinkingWord: DEFAULT_TUI_THINKING_WORD,
175
+ showAgentSwarmPanel: "bottom"
176
+ },
177
+ circuitBreaker: { ...DEFAULT_CIRCUIT_BREAKER_CONFIG },
178
+ modelRuntime: {
179
+ // `effort` is intentionally undefined by default. Leaving it unset lets
180
+ // each model use its provider-recommended reasoning effort (or none at
181
+ // all) instead of forcing an opinionated value that may be unsupported,
182
+ // silently omitted, and surfaced as a per-request warning. Users who
183
+ // want a specific effort can opt in via `/settings` or the WebUI panel.
184
+ reasoning: { mode: "auto" },
185
+ cache: { ttl: "1h" }
186
+ },
187
+ systemPrompt: { variant: "default" }
188
+ };
189
+
190
+ // src/storage/config-loader/default-repair.ts
191
+ function isPlainRecord(value) {
192
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
193
+ }
194
+ function cloneJsonValue(value) {
195
+ return structuredClone(value);
196
+ }
197
+ function fillMissingDefaults(target, defaults) {
198
+ const value = cloneJsonValue(target);
199
+ const changed = fillMissingDefaultsInPlace(value, defaults);
200
+ return { value, changed };
201
+ }
202
+ function fillMissingDefaultsInPlace(target, defaults) {
203
+ let changed = false;
204
+ for (const [key, defaultValue] of Object.entries(defaults)) {
205
+ if (!Object.hasOwn(target, key)) {
206
+ target[key] = cloneJsonValue(defaultValue);
207
+ changed = true;
208
+ continue;
209
+ }
210
+ const current = target[key];
211
+ if (isPlainRecord(current) && isPlainRecord(defaultValue)) {
212
+ changed = fillMissingDefaultsInPlace(current, defaultValue) || changed;
213
+ }
214
+ }
215
+ return changed;
216
+ }
217
+ function repairConfigDefaults(input) {
218
+ const fixed = cloneJsonValue(input);
219
+ const changes = [];
220
+ const MAX_REPAIR_DEPTH = 20;
221
+ const repair = (target, defaults, prefix, depth = 0) => {
222
+ if (depth > MAX_REPAIR_DEPTH) {
223
+ throw new ConfigError({
224
+ message: `Config repair exceeded maximum depth (${MAX_REPAIR_DEPTH}) at "${prefix}". This likely indicates a circular reference or deeply nested user-provided config.`,
225
+ code: ERROR_CODES.CONFIG_INVALID,
226
+ context: { depth, prefix }
227
+ });
228
+ }
229
+ for (const [key, defaultValue] of Object.entries(defaults)) {
230
+ const path2 = prefix ? `${prefix}.${key}` : key;
231
+ if (!Object.hasOwn(target, key)) {
232
+ target[key] = cloneJsonValue(defaultValue);
233
+ changes.push({ path: path2, action: "added" });
234
+ continue;
235
+ }
236
+ const current = target[key];
237
+ if (isPlainRecord(defaultValue)) {
238
+ if (isPlainRecord(current)) {
239
+ repair(current, defaultValue, path2, depth + 1);
240
+ } else {
241
+ target[key] = cloneJsonValue(defaultValue);
242
+ changes.push({ path: path2, action: "replaced" });
243
+ }
244
+ continue;
245
+ }
246
+ const defaultIsArray = Array.isArray(defaultValue);
247
+ const compatible = defaultIsArray ? Array.isArray(current) : !Array.isArray(current) && typeof current === typeof defaultValue;
248
+ if (!compatible) {
249
+ target[key] = cloneJsonValue(defaultValue);
250
+ changes.push({ path: path2, action: "replaced" });
251
+ }
252
+ }
253
+ };
254
+ repair(fixed, CONFIG_BEHAVIOR_DEFAULTS, "");
255
+ return { fixed, changes, changed: changes.length > 0 };
256
+ }
257
+
258
+ // src/storage/config-loader.ts
259
+ import * as fs from "node:fs/promises";
260
+
261
+ // src/storage/config-loader/bootstrap.ts
262
+ var BOOTSTRAP_KEYS = /* @__PURE__ */ new Set(["version", "activeProfile"]);
263
+ function isBootstrapOnly(config) {
264
+ return Object.keys(config).every((k) => BOOTSTRAP_KEYS.has(k));
265
+ }
266
+
267
+ // src/storage/config-loader/diagnostics.ts
268
+ function storageErrorString(err) {
269
+ if (err instanceof Error) {
270
+ const code = err.code;
271
+ return code ? `${code}: ${err.message}` : err.message;
272
+ }
273
+ return String(err);
274
+ }
275
+
276
+ // src/storage/config-loader/env-overrides.ts
277
+ function envBool(v) {
278
+ return !/^(0|false|no|off)$/i.test(v.trim());
279
+ }
280
+ function envBoolOptional(v) {
281
+ return v !== void 0 && envBool(v);
282
+ }
283
+ var LOG_LEVELS = /* @__PURE__ */ new Set(["error", "warn", "info", "debug", "trace"]);
284
+ function envLogLevel(v) {
285
+ return LOG_LEVELS.has(v) ? v : "info";
286
+ }
287
+ var defaultIndexing = {
288
+ onSessionStart: true,
289
+ onEdit: true,
290
+ watchExternal: true,
291
+ debounceMs: 400
292
+ };
293
+ var ENV_MAP = {
294
+ WRONGSTACK_PROVIDER: (c, v) => {
295
+ c.provider = v;
296
+ if (c._envSource === void 0) c._envSource = /* @__PURE__ */ new Set();
297
+ c._envSource.add("provider");
298
+ },
299
+ WRONGSTACK_MODEL: (c, v) => {
300
+ c.model = v;
301
+ if (c._envSource === void 0) c._envSource = /* @__PURE__ */ new Set();
302
+ c._envSource.add("model");
303
+ },
304
+ WRONGSTACK_API_KEY: (c, v) => {
305
+ c.apiKey = v;
306
+ if (c._envSource === void 0) c._envSource = /* @__PURE__ */ new Set();
307
+ c._envSource.add("apiKey");
308
+ },
309
+ WRONGSTACK_BASE_URL: (c, v) => {
310
+ c.baseUrl = v;
311
+ if (c._envSource === void 0) c._envSource = /* @__PURE__ */ new Set();
312
+ c._envSource.add("baseUrl");
313
+ },
314
+ WRONGSTACK_LOG_LEVEL: (c, v) => {
315
+ if (!c.log) c.log = { level: "info" };
316
+ c.log.level = envLogLevel(v);
317
+ },
318
+ WRONGSTACK_INDEX_ON_START: (c, v) => {
319
+ c.indexing = { ...defaultIndexing, ...c.indexing, onSessionStart: envBool(v) };
320
+ },
321
+ WRONGSTACK_INDEX_ON_EDIT: (c, v) => {
322
+ c.indexing = { ...defaultIndexing, ...c.indexing, onEdit: envBool(v) };
323
+ },
324
+ WRONGSTACK_INDEX_WATCH: (c, v) => {
325
+ c.indexing = { ...defaultIndexing, ...c.indexing, watchExternal: envBool(v) };
326
+ }
327
+ };
328
+
329
+ // src/storage/config-loader/in-project-policy.ts
330
+ var IN_PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
331
+ "version",
332
+ "model",
333
+ "cwd",
334
+ "context",
335
+ "tools",
336
+ "features",
337
+ "Sage",
338
+ "skills",
339
+ "autonomy",
340
+ "indexing",
341
+ "session",
342
+ "log",
343
+ "launch",
344
+ "nextPrediction",
345
+ "hints",
346
+ "debugStream",
347
+ "configScope",
348
+ "maxConcurrent",
349
+ "uiLocale",
350
+ "fallbackModels",
351
+ "fallbackBridge",
352
+ "fallbackProfiles",
353
+ // The profile SELECTOR. No broader than its siblings: a repo that can write
354
+ // `fallbackModels` and `fallbackProfiles` already controls the chain outright,
355
+ // and this one can only name a profile the user already defined.
356
+ "fallbackProfile",
357
+ "favoriteModels",
358
+ "favoriteModelsOnly",
359
+ "modelAvailabilitySchedule",
360
+ "fallbackAuto",
361
+ "fallbackStickiness",
362
+ "models",
363
+ "modelMatrix",
364
+ "circuitBreaker",
365
+ "adaptiveConcurrency",
366
+ "modelRuntime"
367
+ ]);
368
+ var KNOWN_DENIED_IN_PROJECT = [
369
+ {
370
+ key: "activeProfile",
371
+ reason: "Only the trusted root bootstrap may select the active profile."
372
+ },
373
+ { key: "provider", reason: "Provider id override; can intercept prompts/responses." },
374
+ { key: "apiKey", reason: "Overrides user API key; exfiltrates prompts." },
375
+ { key: "baseUrl", reason: "Redirects provider endpoint; leaks real API key." },
376
+ { key: "providers", reason: "Per-provider apiKey/baseUrl/oauthConfig; same redirect/exfil." },
377
+ { key: "mcpServers", reason: "Arbitrary command/args/env spawned at boot (RCE)." },
378
+ { key: "hooks", reason: "Shell command arrays on lifecycle events (RCE)." },
379
+ { key: "plugins", reason: "Dynamic npm package load at boot (RCE)." },
380
+ {
381
+ key: "pluginManager",
382
+ reason: "Controls which plugin boot states the LLM may mutate; user-owned trust policy."
383
+ },
384
+ { key: "sync", reason: "Carries githubToken credential and target repo." },
385
+ {
386
+ key: "cloudSync",
387
+ reason: "Carries the my.wrongstack.com machine token credential and endpoint URL."
388
+ },
389
+ { key: "yolo", reason: "Disables all permission confirmation prompts." },
390
+ { key: "extensions", reason: "Per-plugin config can carry command/credential fields." },
391
+ { key: "hq", reason: "Carries HQ client token credential and endpoint URL." },
392
+ { key: "acp", reason: "Per-agent ACP command/args/env override \u2192 arbitrary command exec (RCE)." },
393
+ {
394
+ key: "fleet",
395
+ reason: "Fleet supervision knobs: a repo-committed config could enable autonomous subagent spawning/steering/termination and mailbox traffic on the victim machine."
396
+ },
397
+ {
398
+ key: "brain",
399
+ reason: "Brain decision-layer knobs: a repo-committed config could raise the autonomy risk ceiling, switch the Brain to headless (removing the human tier), or reroute Brain decisions to an attacker-chosen provider/model."
400
+ },
401
+ {
402
+ key: "git",
403
+ reason: "Carries git.identity (GIT_AUTHOR_*/GIT_COMMITTER_* injection): a repo-committed config could spoof the author identity written into the victim's commit history (impersonation)."
404
+ },
405
+ {
406
+ key: "fallbackMaxLastResortCandidates",
407
+ reason: "Bounds how many of the user's OTHER configured providers may be swept in as last-resort failover. Setting it to 0 from a repo-committed config would silently strip that depth during an outage. It was already stripped in practice (absent from the allow-list) but was missing from the key registry, so this gate never checked it."
408
+ }
409
+ ];
410
+ var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
411
+ "version",
412
+ "activeProfile",
413
+ "provider",
414
+ "model",
415
+ "apiKey",
416
+ "baseUrl",
417
+ "maxConcurrent",
418
+ "uiLocale",
419
+ "providers",
420
+ "models",
421
+ "modelMatrix",
422
+ "context",
423
+ "tools",
424
+ "mcpServers",
425
+ "fallbackModels",
426
+ "fallbackBridge",
427
+ "fallbackProfiles",
428
+ "fallbackProfile",
429
+ "favoriteModels",
430
+ "favoriteModelsOnly",
431
+ "modelAvailabilitySchedule",
432
+ "fallbackAuto",
433
+ "fallbackStickiness",
434
+ "fallbackMaxLastResortCandidates",
435
+ "hooks",
436
+ "plugins",
437
+ "pluginManager",
438
+ "log",
439
+ "features",
440
+ "Sage",
441
+ "skills",
442
+ "yolo",
443
+ "nextPrediction",
444
+ "cwd",
445
+ "autonomy",
446
+ "hints",
447
+ "debugStream",
448
+ "configScope",
449
+ "indexing",
450
+ "circuitBreaker",
451
+ "adaptiveConcurrency",
452
+ "launch",
453
+ "session",
454
+ "modelRuntime",
455
+ "hq",
456
+ "sync",
457
+ "cloudSync",
458
+ "extensions",
459
+ "acp",
460
+ "fleet",
461
+ "brain",
462
+ "git"
463
+ ]);
464
+ var IN_PROJECT_DENIED_PATHS = [
465
+ {
466
+ path: "tools.exec.allow",
467
+ reason: "Extends the exec allow-list; a repo could authorise its own binaries."
468
+ },
469
+ { path: "tools.exec.danger", reason: "Weakens the destructive-command banner." },
470
+ {
471
+ // The whole subtree, not just the dangerous leaves: a persona's
472
+ // `instruction` is rendered into the voter SYSTEM prompt, a profile seat
473
+ // may pin providerId/model, and `defaultProfile` selects which of those
474
+ // runs when the agent names no profile. Denying the parent leaves no leaf
475
+ // to reclassify wrongly later.
476
+ path: "tools.council",
477
+ reason: "Council tool panel definitions: persona instructions are injected into the voter SYSTEM prompt and profile seats can pin an attacker-chosen providerId/model."
478
+ },
479
+ { path: "skills.extraDirs", reason: "Loads skill definitions from repo-chosen directories." },
480
+ { path: "skills.registryUrl", reason: "Redirects skill installs to a repo-chosen host." },
481
+ // Deliberately NOT denied: skills.mode and skills.eagerMaxChars. The audit
482
+ // suggested stripping both, but config-loader-extra.test.ts classifies `mode`
483
+ // as a safe preference with a stated rationale, and a repo can only use these
484
+ // to move injection volume in either direction — the trust problem was that
485
+ // repo-supplied skill bodies were indistinguishable from the user's own, which
486
+ // the provenance tag in system-prompt-skill-bodies.ts now fixes. Re-classifying
487
+ // them is a product call.
488
+ { path: "Sage.storage.directory", reason: "Redirects memory storage outside the profile." },
489
+ {
490
+ path: "autonomy.yolo",
491
+ reason: "Alias for the denied top-level `yolo`, and it wins over the user setting \u2014 a repo-committed config could disable every permission prompt."
492
+ },
493
+ {
494
+ path: "autonomy.defaultMode",
495
+ reason: "Sets the startup autonomy mode; autonomy is user-owned, never repo-owned."
496
+ },
497
+ // Deliberately NOT denied: autonomy.autoProceedDelayMs and
498
+ // autoProceedMaxIterations. They tune a mode the user has already switched on
499
+ // rather than granting it, and config-loader-extra.test.ts classifies the
500
+ // delay as a benign project preference. Re-classifying them is a product call.
501
+ {
502
+ path: "launch.autonomy",
503
+ reason: "Launch-time autonomy mode; same user-owned boundary as autonomy.defaultMode."
504
+ },
505
+ {
506
+ path: "features.allowOutsideProjectRoot",
507
+ reason: "Short-circuits both the project-root containment check and the symlink realpath check in the file tools."
508
+ },
509
+ {
510
+ path: "tools.restrictToProjectRoot",
511
+ reason: "The other half of the filesystem confinement switch."
512
+ },
513
+ {
514
+ // Denied for the direction that loosens: the flag defaults to false, so a
515
+ // repo can only ever use it to turn OFF a gate the user deliberately
516
+ // enabled. Same class as `tools.restrictToProjectRoot` — a control the
517
+ // operator owns, not the checked-out repository.
518
+ path: "tools.kanbanGovernance",
519
+ reason: "Repo-committed config could disable a Kanban governance gate the operator switched on, letting product mutations run outside any managed card."
520
+ },
521
+ {
522
+ // The bridge spawn path resolves the CLI entry by walking UP from the
523
+ // project root, so a repo that ships its own `packages/cli/dist/index.js`
524
+ // gets that file spawned with `process.execPath` on WebUI boot — no
525
+ // prompt, no banner. Turning the feature on is therefore equivalent to
526
+ // arbitrary code execution for a hostile checkout, which makes this an
527
+ // operator-owned switch and never a repo-owned one.
528
+ // See discover-mailbox-bridge.ts:findWorkspaceCliEntry.
529
+ path: "features.mailboxBridge",
530
+ reason: "Enables the mailbox bridge, whose CLI-entry resolution walks up from the project root \u2014 a repo-supplied packages/cli/dist/index.js would be spawned on WebUI boot."
531
+ },
532
+ {
533
+ // `plugins` is already denied above, so a repo cannot ADD a plugin. This
534
+ // closes the other half: a repo could previously ship
535
+ // `{"features":{"pluginsTrust":false}}` and switch off the integrity gate
536
+ // for plugins the user had ALREADY installed globally — disarming the
537
+ // trust-on-first-use pin that exists to catch a supply-chain update
538
+ // rewriting a plugin's entry file. Same operator-owned class as the
539
+ // switches above.
540
+ path: "features.pluginsTrust",
541
+ reason: "Disables the plugin trust-on-first-use integrity gate, re-trusting changed code in already-installed global plugins."
542
+ }
543
+ ];
544
+ function deleteNestedPath(target, path2) {
545
+ const segments = path2.split(".");
546
+ const last = segments[segments.length - 1];
547
+ if (last === void 0) return false;
548
+ let cursor = target;
549
+ const chain = [];
550
+ for (const segment of segments.slice(0, -1)) {
551
+ const next = cursor[segment];
552
+ if (!next || typeof next !== "object" || Array.isArray(next)) return false;
553
+ chain.push({ parent: cursor, key: segment });
554
+ cursor = next;
555
+ }
556
+ if (!(last in cursor)) return false;
557
+ const clonedLeaf = { ...cursor };
558
+ delete clonedLeaf[last];
559
+ let replacement = clonedLeaf;
560
+ for (let i = chain.length - 1; i >= 0; i--) {
561
+ const link = chain[i];
562
+ if (!link) continue;
563
+ replacement = { ...link.parent, [link.key]: replacement };
564
+ if (i === 0) {
565
+ for (const [k, v] of Object.entries(replacement)) target[k] = v;
566
+ return true;
567
+ }
568
+ }
569
+ for (const [k, v] of Object.entries(replacement)) target[k] = v;
570
+ return true;
571
+ }
572
+ function assertInProjectAllowListComplete() {
573
+ const missingFromBoth = [];
574
+ for (const key of KNOWN_CONFIG_TOP_LEVEL_KEYS) {
575
+ if (IN_PROJECT_ALLOWED_KEYS.has(key)) continue;
576
+ const denied = KNOWN_DENIED_IN_PROJECT.find((d) => d.key === key);
577
+ if (!denied) missingFromBoth.push(key);
578
+ }
579
+ const staleDenials = KNOWN_DENIED_IN_PROJECT.filter(
580
+ (d) => !KNOWN_CONFIG_TOP_LEVEL_KEYS.has(d.key)
581
+ ).map((d) => d.key);
582
+ const duplicate = KNOWN_DENIED_IN_PROJECT.filter((d) => IN_PROJECT_ALLOWED_KEYS.has(d.key)).map(
583
+ (d) => d.key
584
+ );
585
+ const problems = [];
586
+ if (missingFromBoth.length > 0) {
587
+ problems.push(
588
+ `new Config field(s) not classified as allowed or denied for in-project config: ` + missingFromBoth.join(", ") + ". Add each to IN_PROJECT_ALLOWED_KEYS (if safe) or KNOWN_DENIED_IN_PROJECT (with a reason)."
589
+ );
590
+ }
591
+ if (staleDenials.length > 0) {
592
+ problems.push(
593
+ `KNOWN_DENIED_IN_PROJECT references keys that no longer exist on Config: ` + staleDenials.join(", ") + ". Remove them or restore the field on Config."
594
+ );
595
+ }
596
+ if (duplicate.length > 0) {
597
+ problems.push(
598
+ `field(s) appear in BOTH IN_PROJECT_ALLOWED_KEYS and KNOWN_DENIED_IN_PROJECT: ` + duplicate.join(", ") + ". The allow-list wins at runtime; remove from one of the two."
599
+ );
600
+ }
601
+ const orphanedPaths = IN_PROJECT_DENIED_PATHS.filter(
602
+ ({ path: path2 }) => !IN_PROJECT_ALLOWED_KEYS.has(path2.split(".")[0] ?? "")
603
+ ).map(({ path: path2 }) => path2);
604
+ if (orphanedPaths.length > 0) {
605
+ problems.push(
606
+ `IN_PROJECT_DENIED_PATHS entr(ies) whose top-level parent is not allowed: ` + orphanedPaths.join(", ") + ". The parent is already stripped wholesale, so the nested denial is dead \u2014 remove it."
607
+ );
608
+ }
609
+ const malformedPaths = IN_PROJECT_DENIED_PATHS.filter(
610
+ ({ path: path2 }) => path2.split(".").length < 2 || path2.split(".").some((s) => s.length === 0)
611
+ ).map(({ path: path2 }) => path2);
612
+ if (malformedPaths.length > 0) {
613
+ problems.push(
614
+ `IN_PROJECT_DENIED_PATHS entr(ies) are not dotted nested paths: ` + malformedPaths.join(", ") + ". Top-level keys belong in KNOWN_DENIED_IN_PROJECT instead."
615
+ );
616
+ }
617
+ if (problems.length > 0) {
618
+ throw new Error(
619
+ `stripUnsafeInProjectFields drift check failed:
620
+ - ${problems.join("\n - ")}`
621
+ );
622
+ }
623
+ }
624
+ var driftChecked = false;
625
+ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => console.warn(msg)) {
626
+ if (!driftChecked) {
627
+ assertInProjectAllowListComplete();
628
+ driftChecked = true;
629
+ }
630
+ const stripped = [];
631
+ const out = {};
632
+ for (const [k, v] of Object.entries(inProject)) {
633
+ if (IN_PROJECT_ALLOWED_KEYS.has(k)) {
634
+ out[k] = v;
635
+ continue;
636
+ }
637
+ stripped.push(k);
638
+ }
639
+ for (const { path: path2 } of IN_PROJECT_DENIED_PATHS) {
640
+ if (deleteNestedPath(out, path2)) stripped.push(path2);
641
+ }
642
+ if (stripped.length > 0) {
643
+ warn(
644
+ JSON.stringify({
645
+ level: "warn",
646
+ event: "config.in_project_unsafe_fields_ignored",
647
+ path: sourcePath,
648
+ ignoredKeys: stripped,
649
+ message: `Ignored ${stripped.length} field(s) from the repo-committed config "${sourcePath}": ${stripped.join(", ")}. Only a small allow-list of benign preferences (model, context, tools limits, features, \u2026) may be set by <project>/.wrongstack/config.json. Everything else must live in your active profile config.`,
650
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
651
+ })
652
+ );
653
+ }
654
+ return out;
655
+ }
656
+
657
+ // src/storage/config-loader/inline-provider-models.ts
658
+ var BOOLEAN_CAPABILITIES = [
659
+ "tools",
660
+ "parallelTools",
661
+ "vision",
662
+ "streaming",
663
+ "promptCache",
664
+ "systemPrompt",
665
+ "jsonMode",
666
+ "reasoning",
667
+ "topK",
668
+ "frequencyPenalty",
669
+ "presencePenalty",
670
+ "seed",
671
+ "structuredOutput",
672
+ "logprobs",
673
+ "audio",
674
+ "multipleCompletions"
675
+ ];
676
+ function isRecord(value) {
677
+ return typeof value === "object" && value !== null && !Array.isArray(value);
678
+ }
679
+ function positiveNumber(value) {
680
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
681
+ }
682
+ function readCapabilities(model) {
683
+ const raw = isRecord(model["capabilities"]) ? model["capabilities"] : void 0;
684
+ const capabilities = {};
685
+ if (raw) {
686
+ for (const key of BOOLEAN_CAPABILITIES) {
687
+ const value = raw[key];
688
+ if (typeof value === "boolean") capabilities[key] = value;
689
+ }
690
+ const cacheControl = raw["cacheControl"];
691
+ if (cacheControl === "native" || cacheControl === "auto" || cacheControl === "none") {
692
+ capabilities.cacheControl = cacheControl;
693
+ }
694
+ }
695
+ const limit = isRecord(model["limit"]) ? model["limit"] : void 0;
696
+ const maxContext = positiveNumber(raw?.["maxContext"]) ?? positiveNumber(model["maxContext"]) ?? positiveNumber(model["contextWindow"]) ?? positiveNumber(limit?.["context"]);
697
+ if (maxContext !== void 0) capabilities.maxContext = maxContext;
698
+ const capabilityMaxOutput = positiveNumber(raw?.["maxOutput"]);
699
+ if (capabilityMaxOutput !== void 0) capabilities.maxOutput = capabilityMaxOutput;
700
+ if (typeof model["tool_call"] === "boolean" && capabilities.tools === void 0) {
701
+ capabilities.tools = model["tool_call"];
702
+ }
703
+ if (typeof model["reasoning"] === "boolean" && capabilities.reasoning === void 0) {
704
+ capabilities.reasoning = model["reasoning"];
705
+ }
706
+ const modalities = isRecord(model["modalities"]) ? model["modalities"] : void 0;
707
+ const inputModalities = modalities?.["input"];
708
+ if (Array.isArray(inputModalities) && capabilities.vision === void 0) {
709
+ capabilities.vision = inputModalities.includes("image");
710
+ }
711
+ return Object.keys(capabilities).length > 0 ? capabilities : void 0;
712
+ }
713
+ function inlineDefinition(model) {
714
+ const definition = {};
715
+ const name = model["name"];
716
+ if (typeof name === "string" && name.trim().length > 0) definition.name = name;
717
+ const provider = model["provider"];
718
+ if (typeof provider === "string" && provider.trim().length > 0) {
719
+ definition.provider = provider;
720
+ }
721
+ const limit = isRecord(model["limit"]) ? model["limit"] : void 0;
722
+ const maxOutput = positiveNumber(model["maxOutput"]) ?? positiveNumber(limit?.["output"]) ?? positiveNumber(
723
+ isRecord(model["capabilities"]) ? model["capabilities"]["maxOutput"] : void 0
724
+ );
725
+ if (maxOutput !== void 0) definition.maxOutput = maxOutput;
726
+ const capabilities = readCapabilities(model);
727
+ if (capabilities) definition.capabilities = capabilities;
728
+ const { id: _id, ...rest } = model;
729
+ if (Object.keys(rest).length > 0) {
730
+ definition.modelsDev = rest;
731
+ }
732
+ return definition;
733
+ }
734
+ function mergeDefinitions(base, patch) {
735
+ const mergedModelsDev = (() => {
736
+ if (!base?.modelsDev && !patch.modelsDev) return void 0;
737
+ const baseMd = base?.modelsDev ?? {};
738
+ const patchMd = patch.modelsDev ?? {};
739
+ const result = { ...baseMd, ...patchMd };
740
+ for (const nestedKey of ["limit", "cost", "modalities"]) {
741
+ const bn = baseMd?.[nestedKey];
742
+ const pn = patchMd?.[nestedKey];
743
+ if (bn || pn) {
744
+ result[nestedKey] = { ...bn ?? {}, ...pn ?? {} };
745
+ }
746
+ }
747
+ return result;
748
+ })();
749
+ return {
750
+ ...base,
751
+ ...patch,
752
+ ...base?.capabilities || patch.capabilities ? { capabilities: { ...base?.capabilities, ...patch.capabilities } } : {},
753
+ ...mergedModelsDev ? { modelsDev: mergedModelsDev } : {}
754
+ };
755
+ }
756
+ function normalizeInlineProviderModels(config, warn) {
757
+ const providers = config["providers"];
758
+ if (!isRecord(providers)) return;
759
+ for (const [providerId, rawProvider] of Object.entries(providers)) {
760
+ if (!isRecord(rawProvider) || !Array.isArray(rawProvider["models"])) continue;
761
+ const ids = [];
762
+ const seen = /* @__PURE__ */ new Set();
763
+ const derived = /* @__PURE__ */ new Map();
764
+ for (const [index, entry] of rawProvider["models"].entries()) {
765
+ let id;
766
+ let definition;
767
+ if (typeof entry === "string" && entry.length > 0) {
768
+ id = entry;
769
+ } else if (isRecord(entry)) {
770
+ const rawId = entry["id"];
771
+ if (typeof rawId === "string" && rawId.trim().length > 0) {
772
+ id = rawId.trim();
773
+ if (!FORBIDDEN_PROTO_KEYS.has(id)) definition = inlineDefinition(entry);
774
+ }
775
+ }
776
+ if (!id || FORBIDDEN_PROTO_KEYS.has(id)) {
777
+ warn?.("Ignoring invalid inline provider model entry", {
778
+ event: "config.invalid_inline_provider_model",
779
+ provider: providerId,
780
+ index
781
+ });
782
+ continue;
783
+ }
784
+ if (!seen.has(id)) {
785
+ seen.add(id);
786
+ ids.push(id);
787
+ }
788
+ if (definition) derived.set(id, mergeDefinitions(derived.get(id), definition));
789
+ }
790
+ rawProvider["models"] = ids;
791
+ if (derived.size === 0) continue;
792
+ const explicit = isRecord(rawProvider["customModels"]) ? rawProvider["customModels"] : void 0;
793
+ const normalized = /* @__PURE__ */ Object.create(null);
794
+ for (const [id, definition] of derived) normalized[id] = definition;
795
+ if (explicit) {
796
+ for (const [id, value] of Object.entries(explicit)) {
797
+ if (FORBIDDEN_PROTO_KEYS.has(id) || !isRecord(value)) continue;
798
+ normalized[id] = mergeDefinitions(normalized[id], value);
799
+ }
800
+ }
801
+ rawProvider["customModels"] = normalized;
802
+ }
803
+ }
804
+
805
+ // src/storage/config-loader/legacy-sage-migration.ts
806
+ function migrateLegacySuperMemoryKey(config) {
807
+ const legacy = config["superMemory"];
808
+ if (legacy === void 0) return false;
809
+ if (isPlainRecord(legacy)) {
810
+ const current = config["Sage"];
811
+ config["Sage"] = isPlainRecord(current) ? { ...legacy, ...current } : { ...legacy };
812
+ }
813
+ delete config["superMemory"];
814
+ return true;
815
+ }
816
+ function removeLegacySageEngine(config) {
817
+ const migratedKey = migrateLegacySuperMemoryKey(config);
818
+ const Sage = config["Sage"];
819
+ if (!isPlainRecord(Sage)) return migratedKey;
820
+ const storage = Sage["storage"];
821
+ if (!isPlainRecord(storage) || !Object.hasOwn(storage, "engine")) {
822
+ return migratedKey;
823
+ }
824
+ delete storage["engine"];
825
+ return true;
826
+ }
827
+
828
+ // src/storage/config-loader/path-identity.ts
829
+ import * as path from "node:path";
830
+ function samePath(a, b) {
831
+ let ra = path.resolve(a);
832
+ let rb = path.resolve(b);
833
+ if (process.platform === "win32" || process.platform === "darwin") {
834
+ ra = ra.toLowerCase();
835
+ rb = rb.toLowerCase();
836
+ }
837
+ return ra === rb;
838
+ }
839
+
840
+ // src/storage/config-loader.ts
841
+ function deepMerge2(base, patch) {
842
+ const opts = { arrayMode: "concat-primitives" };
843
+ if (envBoolOptional(process.env.WRONGSTACK_DEBUG_CONFIG)) {
844
+ opts.onNonPrimitiveArrayReplace = (key, existingLen, patchLen) => {
845
+ console.warn(
846
+ `[config] Non-primitive array for "${key}" replaced (global + local config merge). Global entries: ${existingLen}, local entries: ${patchLen}.`
847
+ );
848
+ };
849
+ }
850
+ return deepMerge(
851
+ base,
852
+ patch,
853
+ opts
854
+ );
855
+ }
856
+ var DefaultConfigLoader = class _DefaultConfigLoader {
857
+ paths;
858
+ strict;
859
+ vault;
860
+ extraSources;
861
+ events;
862
+ traceId;
863
+ logger;
864
+ jsonCache = /* @__PURE__ */ new Map();
865
+ constructor(opts) {
866
+ this.paths = opts.paths;
867
+ this.strict = opts.strict ?? false;
868
+ this.vault = opts.vault;
869
+ this.extraSources = opts.sources ?? [];
870
+ this.events = opts.events;
871
+ this.traceId = opts.traceId;
872
+ this.logger = opts.logger;
873
+ }
874
+ /**
875
+ * Emit a structured warning. Uses the configured Logger when available;
876
+ * falls back to console.warn(JSON) so warnings are never silently dropped
877
+ * during early boot (before a Logger is constructed).
878
+ */
879
+ logWarn(msg, ctx) {
880
+ if (this.logger) {
881
+ this.logger.warn(msg, ctx);
882
+ } else {
883
+ console.warn(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
884
+ }
885
+ }
886
+ async load(opts = {}) {
887
+ let cfg = { ...CONFIG_BEHAVIOR_DEFAULTS };
888
+ await this.ensureGlobalDefaults();
889
+ const inProjectCollides = samePath(this.paths.inProjectConfig, this.paths.globalConfig) || samePath(this.paths.inProjectConfig, this.paths.projectLocalConfig);
890
+ const [bootstrap, local, inProject] = await Promise.all([
891
+ this.readJson(this.paths.globalConfig),
892
+ this.readJson(this.paths.projectLocalConfig),
893
+ inProjectCollides ? Promise.resolve({}) : this.readJson(this.paths.inProjectConfig)
894
+ ]);
895
+ const bootstrapMetadata = {
896
+ ...typeof bootstrap.version === "number" ? { version: bootstrap.version } : {},
897
+ ...typeof bootstrap.activeProfile === "string" ? { activeProfile: safeProfileName(bootstrap.activeProfile) } : {}
898
+ };
899
+ cfg = deepMerge2(cfg, bootstrapMetadata);
900
+ const profileName = safeProfileName(
901
+ bootstrap.activeProfile
902
+ );
903
+ const profileCfg = await this.readJson(this.paths.profileConfig(profileName));
904
+ if (Object.keys(profileCfg).length > 0) {
905
+ const profileSettings = { ...profileCfg };
906
+ delete profileSettings["version"];
907
+ delete profileSettings["activeProfile"];
908
+ cfg = deepMerge2(cfg, profileSettings);
909
+ }
910
+ const localSettings = { ...local };
911
+ delete localSettings["version"];
912
+ delete localSettings["activeProfile"];
913
+ cfg = deepMerge2(cfg, localSettings);
914
+ cfg = deepMerge2(
915
+ cfg,
916
+ stripUnsafeInProjectFields(
917
+ inProject,
918
+ this.paths.inProjectConfig,
919
+ this.logger ? (msg) => this.logger.warn(msg) : void 0
920
+ )
921
+ );
922
+ for (const [key, fn] of Object.entries(ENV_MAP)) {
923
+ const v = process.env[key];
924
+ if (v) fn(cfg, v);
925
+ }
926
+ const sorted = [...this.extraSources].sort((a, b) => {
927
+ const pd = (a.priority ?? 50) - (b.priority ?? 50);
928
+ if (pd !== 0) return pd;
929
+ return a.name.localeCompare(b.name);
930
+ });
931
+ for (const src of sorted) {
932
+ try {
933
+ const patch = await src.read();
934
+ if (patch && Object.keys(patch).length > 0) {
935
+ cfg = deepMerge2(cfg, patch);
936
+ }
937
+ } catch (err) {
938
+ this.logWarn("Config source load failed", {
939
+ event: "config.source_load_failed",
940
+ source: src.name,
941
+ message: toErrorMessage(err)
942
+ });
943
+ }
944
+ }
945
+ if (opts.cliFlags) {
946
+ cfg = deepMerge2(cfg, opts.cliFlags);
947
+ }
948
+ normalizeInlineProviderModels(
949
+ cfg,
950
+ (message, context) => this.logWarn(message, context)
951
+ );
952
+ if (this.vault) {
953
+ cfg = decryptConfigSecrets(cfg, this.vault);
954
+ }
955
+ if (cfg.providers) {
956
+ for (const pcfg of Object.values(cfg.providers)) {
957
+ if (!pcfg || typeof pcfg !== "object") continue;
958
+ const rawKeys = pcfg.apiKeys;
959
+ if (!Array.isArray(rawKeys) || rawKeys.length === 0) continue;
960
+ const keys = rawKeys.filter(
961
+ (k) => !!k && typeof k === "object" && typeof k.label === "string" && typeof k.apiKey === "string"
962
+ );
963
+ if (keys.length === 0) continue;
964
+ const existing = pcfg.apiKey;
965
+ if (existing && existing.length > 0) continue;
966
+ const activeLabel = pcfg.activeKey;
967
+ const chosen = activeLabel ? keys.find((k) => k.label === activeLabel) ?? keys[0] : keys[0];
968
+ if (chosen?.apiKey) {
969
+ pcfg.apiKey = chosen.apiKey;
970
+ }
971
+ }
972
+ }
973
+ removeLegacySageEngine(cfg);
974
+ this.validateBehavior(cfg);
975
+ if (this.strict && !opts.skipIdentityValidation) {
976
+ this.validateIdentity(cfg);
977
+ }
978
+ return Object.freeze(cfg);
979
+ }
980
+ /** Check whether a config object contains only the two bootstrap keys. */
981
+ static isBootstrapOnly(config) {
982
+ return isBootstrapOnly(config);
983
+ }
984
+ async ensureGlobalDefaults() {
985
+ const fp = this.paths.globalConfig;
986
+ const t0 = Date.now();
987
+ try {
988
+ await withFileLock(fp, async () => {
989
+ let parsed;
990
+ let fileExisted = true;
991
+ try {
992
+ const raw = await fs.readFile(fp, "utf8");
993
+ const result = safeParse(raw);
994
+ if (!result.ok || !isPlainRecord(result.value)) {
995
+ return;
996
+ }
997
+ parsed = result.value;
998
+ } catch (err) {
999
+ if (err.code !== "ENOENT") {
1000
+ this.events?.emit("storage.error", {
1001
+ sessionId: "~config~",
1002
+ store: "config",
1003
+ filePath: fp,
1004
+ operation: "ensure_defaults",
1005
+ outcome: "failure",
1006
+ error: storageErrorString(err),
1007
+ recoverable: false,
1008
+ durationMs: Date.now() - t0,
1009
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
1010
+ });
1011
+ this.logWarn("Config defaults read failed", {
1012
+ event: "config.defaults_read_failed",
1013
+ path: fp,
1014
+ message: toErrorMessage(err)
1015
+ });
1016
+ return;
1017
+ }
1018
+ fileExisted = false;
1019
+ parsed = {};
1020
+ }
1021
+ if (parsed["version"] !== void 0 && parsed["version"] !== 1) return;
1022
+ const rawProfileName = parsed.activeProfile;
1023
+ const profileName = safeProfileName(rawProfileName);
1024
+ const profileFp = this.paths.profileConfig(profileName);
1025
+ await this.ensureProfileConfig(profileFp, parsed, fileExisted);
1026
+ const bootstrap = {
1027
+ version: 1,
1028
+ activeProfile: profileName
1029
+ };
1030
+ let needsBootstrapWrite = false;
1031
+ if (parsed.version !== 1) {
1032
+ needsBootstrapWrite = true;
1033
+ }
1034
+ if (parsed.activeProfile === void 0 || parsed.activeProfile !== profileName) {
1035
+ needsBootstrapWrite = true;
1036
+ }
1037
+ if (!_DefaultConfigLoader.isBootstrapOnly(parsed)) {
1038
+ needsBootstrapWrite = true;
1039
+ }
1040
+ if (needsBootstrapWrite) {
1041
+ await backupConfigFile(fp, this.paths);
1042
+ await atomicWrite(fp, JSON.stringify(bootstrap, null, 2), { mode: 384 });
1043
+ this.events?.emit("storage.write", {
1044
+ sessionId: "~config~",
1045
+ store: "config",
1046
+ filePath: fp,
1047
+ operation: "ensure_defaults",
1048
+ outcome: "success",
1049
+ durationMs: Date.now() - t0,
1050
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
1051
+ });
1052
+ }
1053
+ });
1054
+ } catch (err) {
1055
+ this.events?.emit("storage.error", {
1056
+ sessionId: "~config~",
1057
+ store: "config",
1058
+ filePath: fp,
1059
+ operation: "ensure_defaults",
1060
+ outcome: "failure",
1061
+ error: storageErrorString(err),
1062
+ recoverable: false,
1063
+ durationMs: Date.now() - t0,
1064
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
1065
+ });
1066
+ this.logWarn("Config defaults write failed", {
1067
+ event: "config.defaults_write_failed",
1068
+ path: fp,
1069
+ message: toErrorMessage(err)
1070
+ });
1071
+ }
1072
+ }
1073
+ /**
1074
+ * Ensure a profile config file exists at the given path.
1075
+ * On first boot: migrate content from the old flat global config, or seed
1076
+ * with behavior defaults. Subsequent boots: fill any missing keys from
1077
+ * CONFIG_BEHAVIOR_DEFAULTS (keeping user settings intact).
1078
+ *
1079
+ * Once a profile file exists, settings found in the root bootstrap are
1080
+ * ignored. This prevents stale or accidentally-written root values from
1081
+ * becoming a hidden settings source alongside profiles.
1082
+ */
1083
+ async ensureProfileConfig(profileFp, oldGlobalParsed, oldFileExisted) {
1084
+ const t0 = Date.now();
1085
+ let parsed;
1086
+ let existed = true;
1087
+ try {
1088
+ const raw = await fs.readFile(profileFp, "utf8");
1089
+ const result = safeParse(raw);
1090
+ if (!result.ok || !isPlainRecord(result.value)) {
1091
+ this.logWarn("Profile config parse failed \u2014 falling back to defaults", {
1092
+ event: "config.profile_parse_failed",
1093
+ path: profileFp
1094
+ });
1095
+ parsed = {};
1096
+ } else {
1097
+ parsed = result.value;
1098
+ }
1099
+ } catch (err) {
1100
+ if (err.code !== "ENOENT") {
1101
+ this.logWarn("Profile config read failed", {
1102
+ event: "config.profile_read_failed",
1103
+ path: profileFp,
1104
+ message: toErrorMessage(err)
1105
+ });
1106
+ return;
1107
+ }
1108
+ existed = false;
1109
+ parsed = {};
1110
+ }
1111
+ const removedLegacyEngine = removeLegacySageEngine(parsed);
1112
+ let seed;
1113
+ if (!existed && oldFileExisted && Object.keys(oldGlobalParsed).length >= 1) {
1114
+ seed = { ...oldGlobalParsed };
1115
+ delete seed["activeProfile"];
1116
+ removeLegacySageEngine(seed);
1117
+ const filled = fillMissingDefaults(seed, CONFIG_BEHAVIOR_DEFAULTS);
1118
+ seed = filled.value;
1119
+ } else {
1120
+ const filled = fillMissingDefaults(
1121
+ parsed,
1122
+ CONFIG_BEHAVIOR_DEFAULTS
1123
+ );
1124
+ if (!filled.changed && !removedLegacyEngine) return;
1125
+ seed = filled.value;
1126
+ }
1127
+ await backupConfigFile(profileFp, this.paths);
1128
+ await atomicWrite(profileFp, JSON.stringify(seed, null, 2), { mode: 384 });
1129
+ this.events?.emit("storage.write", {
1130
+ sessionId: "~config~",
1131
+ store: "config",
1132
+ filePath: profileFp,
1133
+ operation: "ensure_profile_defaults",
1134
+ outcome: "success",
1135
+ durationMs: Date.now() - t0,
1136
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
1137
+ });
1138
+ }
1139
+ /**
1140
+ * Persist sync config to the active profile's sync.json, with the token encrypted
1141
+ * by the vault (if provided). The file is isolated from the main config
1142
+ * hierarchy to prevent accidental commits.
1143
+ */
1144
+ async persistSyncConfig(cfg) {
1145
+ let toWrite = { ...cfg };
1146
+ if (this.vault && toWrite.githubToken && !toWrite.githubToken.startsWith("enc:")) {
1147
+ toWrite = { ...toWrite, githubToken: this.vault.encrypt(toWrite.githubToken) };
1148
+ }
1149
+ const fp = this.paths.syncConfig;
1150
+ const t0 = Date.now();
1151
+ try {
1152
+ await atomicWrite(fp, JSON.stringify(toWrite, null, 2), { mode: 384 });
1153
+ this.events?.emit("storage.write", {
1154
+ sessionId: "~config~",
1155
+ store: "config",
1156
+ filePath: fp,
1157
+ operation: "persist_sync",
1158
+ outcome: "success",
1159
+ durationMs: Date.now() - t0,
1160
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
1161
+ });
1162
+ } catch (err) {
1163
+ this.events?.emit("storage.error", {
1164
+ sessionId: "~config~",
1165
+ store: "config",
1166
+ filePath: fp,
1167
+ operation: "persist_sync",
1168
+ outcome: "failure",
1169
+ error: storageErrorString(err),
1170
+ recoverable: false,
1171
+ durationMs: Date.now() - t0,
1172
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
1173
+ });
1174
+ throw err;
1175
+ }
1176
+ }
1177
+ /**
1178
+ * Read the active profile's sync.json (encrypted GitHub token storage) and decrypt
1179
+ * the token if a vault is available. Returns null if the file doesn't exist.
1180
+ * This is separate from main config loading because sync.json is intentionally
1181
+ * isolated — it should never be part of project-local or env-driven config.
1182
+ */
1183
+ async loadSyncConfig() {
1184
+ const fp = this.paths.syncConfig;
1185
+ const t0 = Date.now();
1186
+ try {
1187
+ const raw = await fs.readFile(fp, "utf8");
1188
+ const parsed = safeParse(raw);
1189
+ if (!parsed.ok || !parsed.value) {
1190
+ this.events?.emit("storage.read", {
1191
+ sessionId: "~config~",
1192
+ store: "config",
1193
+ filePath: fp,
1194
+ operation: "load_sync",
1195
+ outcome: "failure",
1196
+ durationMs: Date.now() - t0,
1197
+ error: "parse error or empty file",
1198
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
1199
+ });
1200
+ return null;
1201
+ }
1202
+ if (this.vault) {
1203
+ const decrypted = decryptConfigSecrets({ sync: parsed.value }, this.vault);
1204
+ const result = decrypted.sync ?? null;
1205
+ this.events?.emit("storage.read", {
1206
+ sessionId: "~config~",
1207
+ store: "config",
1208
+ filePath: fp,
1209
+ operation: "load_sync",
1210
+ outcome: "success",
1211
+ durationMs: Date.now() - t0,
1212
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
1213
+ });
1214
+ return result;
1215
+ }
1216
+ this.events?.emit("storage.read", {
1217
+ sessionId: "~config~",
1218
+ store: "config",
1219
+ filePath: fp,
1220
+ operation: "load_sync",
1221
+ outcome: "success",
1222
+ durationMs: Date.now() - t0,
1223
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
1224
+ });
1225
+ return parsed.value;
1226
+ } catch (err) {
1227
+ if (err.code === "ENOENT") return null;
1228
+ this.events?.emit("storage.read", {
1229
+ sessionId: "~config~",
1230
+ store: "config",
1231
+ filePath: fp,
1232
+ operation: "load_sync",
1233
+ outcome: "failure",
1234
+ durationMs: Date.now() - t0,
1235
+ error: storageErrorString(err),
1236
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
1237
+ });
1238
+ this.logWarn("Config sync load failed", {
1239
+ event: "config.sync_load_failed",
1240
+ message: toErrorMessage(err)
1241
+ });
1242
+ return null;
1243
+ }
1244
+ }
1245
+ async readJson(file) {
1246
+ const t0 = Date.now();
1247
+ let mtimeMs = null;
1248
+ try {
1249
+ const stat2 = await fs.stat(file);
1250
+ mtimeMs = stat2.mtimeMs;
1251
+ const cached = this.jsonCache.get(file);
1252
+ if (cached && cached.mtimeMs === mtimeMs) {
1253
+ return structuredClone(cached.value);
1254
+ }
1255
+ } catch (err) {
1256
+ if (err.code === "ENOENT") {
1257
+ this.jsonCache.set(file, { mtimeMs: null, value: {} });
1258
+ return {};
1259
+ }
1260
+ this.events?.emit("storage.read", {
1261
+ sessionId: "~config~",
1262
+ store: "config",
1263
+ filePath: file,
1264
+ operation: "read_json",
1265
+ outcome: "failure",
1266
+ durationMs: Date.now() - t0,
1267
+ error: storageErrorString(err),
1268
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
1269
+ });
1270
+ this.logWarn("Config read failed (stat)", {
1271
+ event: "config.read_failed",
1272
+ path: file,
1273
+ message: toErrorMessage(err)
1274
+ });
1275
+ return {};
1276
+ }
1277
+ let raw;
1278
+ try {
1279
+ raw = await fs.readFile(file, "utf8");
1280
+ } catch (err) {
1281
+ if (err.code !== "ENOENT") {
1282
+ this.events?.emit("storage.read", {
1283
+ sessionId: "~config~",
1284
+ store: "config",
1285
+ filePath: file,
1286
+ operation: "read_json",
1287
+ outcome: "failure",
1288
+ durationMs: Date.now() - t0,
1289
+ error: storageErrorString(err),
1290
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
1291
+ });
1292
+ this.logWarn("Config read failed (read)", {
1293
+ event: "config.read_failed",
1294
+ path: file,
1295
+ message: toErrorMessage(err)
1296
+ });
1297
+ }
1298
+ this.jsonCache.set(file, { mtimeMs: null, value: {} });
1299
+ return {};
1300
+ }
1301
+ const parsed = safeParse(raw);
1302
+ if (!parsed.ok || !parsed.value) {
1303
+ this.events?.emit("storage.read", {
1304
+ sessionId: "~config~",
1305
+ store: "config",
1306
+ filePath: file,
1307
+ operation: "read_json",
1308
+ outcome: "failure",
1309
+ durationMs: Date.now() - t0,
1310
+ error: "parse error or empty file",
1311
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
1312
+ });
1313
+ this.logWarn("Config parse failed \u2014 falling back to defaults", {
1314
+ event: "config.parse_failed",
1315
+ path: file
1316
+ });
1317
+ return {};
1318
+ }
1319
+ migrateLegacySuperMemoryKey(parsed.value);
1320
+ this.jsonCache.set(file, { mtimeMs, value: structuredClone(parsed.value) });
1321
+ return parsed.value;
1322
+ }
1323
+ validateBehavior(cfg) {
1324
+ if (cfg.version === void 0)
1325
+ throw new ConfigError({
1326
+ message: "Config: missing version field",
1327
+ code: ERROR_CODES.CONFIG_INVALID,
1328
+ context: { field: "version" }
1329
+ });
1330
+ if (cfg.version !== 1)
1331
+ throw new ConfigError({
1332
+ message: `Config: unsupported version ${cfg.version}`,
1333
+ code: ERROR_CODES.CONFIG_INVALID,
1334
+ context: { field: "version", actual: cfg.version }
1335
+ });
1336
+ const c = cfg.context;
1337
+ if (!c)
1338
+ throw new ConfigError({
1339
+ message: "Config: missing context section",
1340
+ code: ERROR_CODES.CONFIG_INVALID,
1341
+ context: { field: "context" }
1342
+ });
1343
+ const fields = ["warnThreshold", "softThreshold", "hardThreshold"];
1344
+ for (const f of fields) {
1345
+ const v = c[f];
1346
+ if (typeof v !== "number" || !Number.isFinite(v)) {
1347
+ throw new ConfigError({
1348
+ message: `Config: context.${String(f)} must be a finite number (got ${typeof v})`,
1349
+ code: ERROR_CODES.CONFIG_INVALID,
1350
+ context: { field: `context.${String(f)}`, actualType: typeof v }
1351
+ });
1352
+ }
1353
+ }
1354
+ if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
1355
+ throw new ConfigError({
1356
+ message: "Config: context thresholds must satisfy warn < soft < hard",
1357
+ code: ERROR_CODES.CONFIG_INVALID,
1358
+ context: { warn: c.warnThreshold, soft: c.softThreshold, hard: c.hardThreshold }
1359
+ });
1360
+ }
1361
+ if (c.mode !== void 0 && !isContextWindowModeSelectionId(c.mode)) {
1362
+ const known = listContextWindowModes().map((m) => m.id).join(", ");
1363
+ this.logWarn(
1364
+ `Ignoring unknown context.mode "${c.mode}" \u2014 falling back to "${DEFAULT_CONTEXT_WINDOW_MODE_ID}"`,
1365
+ { event: "config.unknown_context_mode", mode: c.mode, known }
1366
+ );
1367
+ c.mode = DEFAULT_CONTEXT_WINDOW_MODE_ID;
1368
+ } else if (c.mode !== void 0) {
1369
+ c.mode = normalizeContextWindowModeId(c.mode) ?? DEFAULT_CONTEXT_WINDOW_MODE_ID;
1370
+ }
1371
+ }
1372
+ validateIdentity(cfg) {
1373
+ if (!cfg.provider) {
1374
+ throw new ConfigError({
1375
+ message: "Config: no provider configured. Run `wstack init` or set WRONGSTACK_PROVIDER.",
1376
+ code: ERROR_CODES.CONFIG_INVALID,
1377
+ context: { field: "provider" }
1378
+ });
1379
+ }
1380
+ if (!cfg.model) {
1381
+ throw new ConfigError({
1382
+ message: "Config: no model configured. Run `wstack init` or set WRONGSTACK_MODEL.",
1383
+ code: ERROR_CODES.CONFIG_INVALID,
1384
+ context: { field: "model" }
1385
+ });
1386
+ }
1387
+ }
1388
+ };
1389
+
1390
+ export {
1391
+ CONFIG_BEHAVIOR_DEFAULTS,
1392
+ repairConfigDefaults,
1393
+ normalizeInlineProviderModels,
1394
+ DefaultConfigLoader
1395
+ };
1396
+ //# sourceMappingURL=chunk-IQ35HMQL.js.map