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