@cortexkit/opencode-magic-context 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/dist/config/agent-disable.d.ts +22 -0
  2. package/dist/config/agent-disable.d.ts.map +1 -1
  3. package/dist/config/index.d.ts.map +1 -1
  4. package/dist/config/project-security.d.ts +7 -0
  5. package/dist/config/project-security.d.ts.map +1 -1
  6. package/dist/config/schema/magic-context.d.ts +144 -24
  7. package/dist/config/schema/magic-context.d.ts.map +1 -1
  8. package/dist/config/transform-mode.d.ts +2 -0
  9. package/dist/config/transform-mode.d.ts.map +1 -1
  10. package/dist/{conflict-warning-hook-bwa99a04.js → conflict-warning-hook-fvpfvt0w.js} +1 -1
  11. package/dist/{derive-budgets-e9j09sjr.js → derive-budgets-9crp0cvd.js} +1 -1
  12. package/dist/features/builtin-commands/commands.d.ts +1 -1
  13. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  14. package/dist/features/magic-context/compaction-marker.d.ts +106 -0
  15. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  16. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +0 -2
  17. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  21. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  23. package/dist/features/magic-context/dreamer/task-registry.d.ts +0 -1
  24. package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
  25. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  26. package/dist/features/magic-context/fail-closed-block.d.ts +10 -0
  27. package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
  28. package/dist/features/magic-context/git-commits/index.d.ts +2 -2
  29. package/dist/features/magic-context/git-commits/index.d.ts.map +1 -1
  30. package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts +0 -2
  31. package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts.map +1 -1
  32. package/dist/features/magic-context/git-commits/storage-git-commits.d.ts +0 -13
  33. package/dist/features/magic-context/git-commits/storage-git-commits.d.ts.map +1 -1
  34. package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
  35. package/dist/features/magic-context/memory/embedding-local.d.ts +7 -1
  36. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  37. package/dist/features/magic-context/memory/embedding.d.ts +1 -2
  38. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  39. package/dist/features/magic-context/memory/memory-migration.d.ts +0 -6
  40. package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -1
  41. package/dist/features/magic-context/memory/project-identity.d.ts +2 -6
  42. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  43. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts +0 -1
  44. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
  45. package/dist/features/magic-context/memory/storage-memory.d.ts +1 -6
  46. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  47. package/dist/features/magic-context/memory/verification-paths.d.ts +0 -1
  48. package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
  49. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  50. package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
  51. package/dist/features/magic-context/mural/render-mural.d.ts.map +1 -1
  52. package/dist/features/magic-context/mural/storage-mural.d.ts +0 -1
  53. package/dist/features/magic-context/mural/storage-mural.d.ts.map +1 -1
  54. package/dist/features/magic-context/overflow-detection.d.ts +8 -1
  55. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  56. package/dist/features/magic-context/project-embedding-registry.d.ts +16 -2
  57. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  58. package/dist/features/magic-context/scheduler.d.ts.map +1 -1
  59. package/dist/features/magic-context/schema-fence-probe.d.ts +28 -0
  60. package/dist/features/magic-context/schema-fence-probe.d.ts.map +1 -0
  61. package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
  62. package/dist/features/magic-context/smart-notes/capabilities.d.ts +0 -2
  63. package/dist/features/magic-context/smart-notes/capabilities.d.ts.map +1 -1
  64. package/dist/features/magic-context/smart-notes/compiler.d.ts +0 -1
  65. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  66. package/dist/features/magic-context/smart-notes/types.d.ts +0 -16
  67. package/dist/features/magic-context/smart-notes/types.d.ts.map +1 -1
  68. package/dist/features/magic-context/storage-db.d.ts +35 -1
  69. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  70. package/dist/features/magic-context/storage-meta-persisted.d.ts +38 -5
  71. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  72. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  73. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  74. package/dist/features/magic-context/storage.d.ts +1 -1
  75. package/dist/features/magic-context/storage.d.ts.map +1 -1
  76. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  77. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +0 -2
  78. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
  79. package/dist/hooks/auto-update-checker/types.d.ts +0 -3
  80. package/dist/hooks/auto-update-checker/types.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/boundary-execution.d.ts +1 -1
  82. package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
  83. package/dist/hooks/magic-context/cache-busting-signals.d.ts +1 -1
  84. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  86. package/dist/hooks/magic-context/child-session-spawn.d.ts +26 -0
  87. package/dist/hooks/magic-context/child-session-spawn.d.ts.map +1 -0
  88. package/dist/hooks/magic-context/command-handler.d.ts +2 -0
  89. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/compaction-marker-manager.d.ts +49 -0
  91. package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
  92. package/dist/hooks/magic-context/compaction-off-transition.d.ts +96 -0
  93. package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -0
  94. package/dist/hooks/magic-context/compartment-parser.d.ts +12 -0
  95. package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -0
  97. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts +0 -12
  100. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/compartment-trigger.d.ts +12 -5
  104. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +47 -1
  106. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/emergency-drop.d.ts +2 -2
  108. package/dist/hooks/magic-context/event-handler.d.ts +7 -0
  109. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/event-resolvers.d.ts +13 -21
  111. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  113. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -1
  115. package/dist/hooks/magic-context/historian-prompt.generated.d.ts +1 -1
  116. package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -1
  117. package/dist/hooks/magic-context/hook-handlers.d.ts +1 -0
  118. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  119. package/dist/hooks/magic-context/hook.d.ts +11 -1
  120. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  121. package/dist/hooks/magic-context/inject-compartments.d.ts +18 -0
  122. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  123. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  124. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  125. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  126. package/dist/hooks/magic-context/rust-mode-transform.d.ts +3 -0
  127. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  128. package/dist/hooks/magic-context/send-session-notification.d.ts +20 -1
  129. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  130. package/dist/hooks/magic-context/sentinel.d.ts +39 -0
  131. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  132. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  133. package/dist/hooks/magic-context/tool-drop-target.d.ts +19 -0
  134. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  135. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -0
  136. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  137. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +30 -0
  138. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  139. package/dist/hooks/magic-context/transform.d.ts +27 -0
  140. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  141. package/dist/{index-kxz4tn6m.js → index-56yjaeb2.js} +56 -4
  142. package/dist/{index-bmkea2dq.js → index-b563sga5.js} +40 -287
  143. package/dist/{index-y1c9tcm3.js → index-cn960ffd.js} +101 -20
  144. package/dist/{index-k5y60ymm.js → index-dtas91s5.js} +27 -6
  145. package/dist/index-fcyvznch.js +279 -0
  146. package/dist/{index-63dq2g4d.js → index-ry8rh8cv.js} +138 -24
  147. package/dist/{index-h86twgsp.js → index-s0mdn690.js} +2113 -1067
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +1356 -627
  150. package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
  151. package/dist/plugin/hooks/create-session-hooks.d.ts +19 -8
  152. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  153. package/dist/plugin/messages-transform.d.ts +10 -0
  154. package/dist/plugin/messages-transform.d.ts.map +1 -1
  155. package/dist/plugin/rpc-handlers.d.ts +3 -7
  156. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  157. package/dist/plugin/tool-registry.d.ts +8 -0
  158. package/dist/plugin/tool-registry.d.ts.map +1 -1
  159. package/dist/{recomp-orchestrator-yh7pepvx.js → recomp-orchestrator-8e82vmvb.js} +6 -5
  160. package/dist/send-session-notification-6njqewzk.js +20 -0
  161. package/dist/shared/conflict-detector.d.ts +38 -1
  162. package/dist/shared/conflict-detector.d.ts.map +1 -1
  163. package/dist/shared/conflict-fixer.d.ts +18 -1
  164. package/dist/shared/conflict-fixer.d.ts.map +1 -1
  165. package/dist/shared/context-limit-provenance.d.ts +3 -0
  166. package/dist/shared/context-limit-provenance.d.ts.map +1 -0
  167. package/dist/shared/escalation-bands.d.ts +9 -0
  168. package/dist/shared/escalation-bands.d.ts.map +1 -0
  169. package/dist/shared/format-threshold.d.ts +1 -1
  170. package/dist/shared/models-dev-cache.d.ts +29 -3
  171. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  172. package/dist/shared/rpc-server.d.ts.map +1 -1
  173. package/dist/shared/rpc-types.d.ts +46 -1
  174. package/dist/shared/rpc-types.d.ts.map +1 -1
  175. package/dist/shared/sqlite.d.ts +9 -4
  176. package/dist/shared/sqlite.d.ts.map +1 -1
  177. package/dist/shared/storage-permissions.d.ts +5 -0
  178. package/dist/shared/storage-permissions.d.ts.map +1 -0
  179. package/dist/{storage-meta-session-1phy0b1z.js → storage-meta-session-xt41zbw4.js} +2 -1
  180. package/dist/{task-config-dn9k9szr.js → task-config-5nx3qfz8.js} +1 -2
  181. package/dist/tui/compaction-off.d.ts +13 -0
  182. package/dist/tui/compaction-off.d.ts.map +1 -0
  183. package/dist/tui/data/context-db.d.ts.map +1 -1
  184. package/dist/tui-compiled/compaction-off.d.ts +13 -0
  185. package/dist/tui-compiled/compaction-off.d.ts.map +1 -0
  186. package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
  187. package/package.json +8 -3
  188. package/src/agents/dreamer.ts +68 -0
  189. package/src/agents/hidden-agent-registrations.ts +331 -0
  190. package/src/agents/historian.ts +3 -0
  191. package/src/agents/language-directive.test.ts +138 -0
  192. package/src/agents/language-directive.ts +121 -0
  193. package/src/agents/magic-context-prompt.language.test.ts +35 -0
  194. package/src/agents/magic-context-prompt.test.ts +292 -0
  195. package/src/agents/magic-context-prompt.ts +182 -0
  196. package/src/agents/permissions.test.ts +252 -0
  197. package/src/agents/permissions.ts +186 -0
  198. package/src/agents/sidekick.ts +1 -0
  199. package/src/agents/smart-note-compiler.ts +1 -0
  200. package/src/config/agent-disable.ts +106 -0
  201. package/src/config/compaction-accessor-guard.test.ts +82 -0
  202. package/src/config/compaction-backcompat.test.ts +52 -0
  203. package/src/config/index.test.ts +904 -0
  204. package/src/config/index.ts +601 -0
  205. package/src/config/migrate-config-location.test.ts +332 -0
  206. package/src/config/migrate-config-location.ts +635 -0
  207. package/src/config/migrate-dreamer-v2.test.ts +287 -0
  208. package/src/config/migrate-dreamer-v2.ts +336 -0
  209. package/src/config/migrate-experimental.ts +159 -0
  210. package/src/config/project-security.test.ts +366 -0
  211. package/src/config/project-security.ts +588 -0
  212. package/src/config/prune-config-leaf.test.ts +78 -0
  213. package/src/config/prune-config-leaf.ts +68 -0
  214. package/src/config/removed-config-keys.test.ts +116 -0
  215. package/src/config/schema/agent-overrides.ts +45 -0
  216. package/src/config/schema/magic-context.test.ts +358 -0
  217. package/src/config/schema/magic-context.ts +1002 -0
  218. package/src/config/transform-mode.test.ts +62 -0
  219. package/src/config/transform-mode.ts +34 -0
  220. package/src/config/variable.test.ts +323 -0
  221. package/src/config/variable.ts +192 -0
  222. package/src/features/magic-context/defaults.ts +1 -0
  223. package/src/features/magic-context/dreamer/cron.ts +243 -0
  224. package/src/features/magic-context/dreamer/task-registry.ts +104 -0
  225. package/src/shared/conflict-detector.test.ts +251 -1
  226. package/src/shared/conflict-detector.ts +84 -5
  227. package/src/shared/conflict-fixer.test.ts +309 -0
  228. package/src/shared/conflict-fixer.ts +82 -5
  229. package/src/shared/context-limit-provenance.ts +6 -0
  230. package/src/shared/escalation-bands.test.ts +23 -0
  231. package/src/shared/escalation-bands.ts +18 -0
  232. package/src/shared/format-threshold.ts +1 -1
  233. package/src/shared/models-dev-cache.test.ts +171 -0
  234. package/src/shared/models-dev-cache.ts +195 -38
  235. package/src/shared/rpc-server.ts +27 -18
  236. package/src/shared/rpc-types.ts +46 -1
  237. package/src/shared/sqlite.ts +13 -33
  238. package/src/shared/storage-permissions.ts +20 -0
  239. package/src/tui/compaction-off.test.ts +56 -0
  240. package/src/tui/compaction-off.ts +29 -0
  241. package/src/tui/data/context-db.ts +6 -0
  242. package/src/tui/index.tsx +113 -65
  243. package/src/tui/slots/sidebar-content.tsx +139 -83
  244. package/src/tui-compiled/compaction-off.ts +29 -0
  245. package/src/tui-compiled/data/context-db.ts +6 -0
  246. package/src/tui-compiled/index.tsx +558 -455
  247. package/src/tui-compiled/slots/sidebar-content.tsx +237 -161
  248. package/dist/config/schema.d.ts +0 -3
  249. package/dist/config/schema.d.ts.map +0 -1
  250. package/dist/features/magic-context/plugin-messages.d.ts +0 -75
  251. package/dist/features/magic-context/plugin-messages.d.ts.map +0 -1
  252. package/dist/hooks/magic-context/compartment-runner-state-xml.d.ts +0 -21
  253. package/dist/hooks/magic-context/compartment-runner-state-xml.d.ts.map +0 -1
  254. package/dist/index-anhrfygk.js +0 -57
  255. package/dist/send-session-notification-tghq9gnh.js +0 -11
  256. package/dist/shared/opencode-compaction-detector.d.ts +0 -2
  257. package/dist/shared/opencode-compaction-detector.d.ts.map +0 -1
  258. package/dist/shared/transcript-opencode.d.ts +0 -71
  259. package/dist/shared/transcript-opencode.d.ts.map +0 -1
  260. package/dist/tools/index.d.ts +0 -6
  261. package/dist/tools/index.d.ts.map +0 -1
  262. package/src/shared/opencode-compaction-detector.test.ts +0 -239
  263. package/src/shared/opencode-compaction-detector.ts +0 -81
  264. package/src/shared/transcript-opencode.ts +0 -302
  265. /package/dist/{index-v7fmc1s2.js → index-ygcng9t8.js} +0 -0
@@ -0,0 +1,601 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+
3
+ import { detectConfigFile, parseJsonc } from "../shared/jsonc-parser";
4
+ import { setOutputReserveConfig } from "../shared/models-dev-cache";
5
+ import { isCompactionEnabled, migrateLegacyAgentEnabledInMemory } from "./agent-disable";
6
+ import {
7
+ cortexKitProjectConfigBasePath,
8
+ cortexKitUserConfigBasePath,
9
+ type LegacyConfigSource,
10
+ resolveLegacyConfigSources,
11
+ resolveLegacyConfigSourcesForHarness,
12
+ } from "./migrate-config-location";
13
+ import { migrateDreamerV2 } from "./migrate-dreamer-v2";
14
+ import { migrateLegacyExperimental } from "./migrate-experimental";
15
+ import {
16
+ constrainProjectThresholdOverrides,
17
+ dropInheritedEmbeddingKeyOnRedirect,
18
+ stripUnsafeProjectConfigFields,
19
+ } from "./project-security";
20
+ import { pruneNestedConfigLeaf } from "./prune-config-leaf";
21
+ import { type MagicContextConfig, MagicContextConfigSchema } from "./schema/magic-context";
22
+ import { resolveTransformMode } from "./transform-mode";
23
+ import { substituteConfigVariables } from "./variable";
24
+
25
+ export interface MagicContextPluginConfig extends MagicContextConfig {
26
+ disabled_hooks?: string[];
27
+ command?: Record<
28
+ string,
29
+ {
30
+ template: string;
31
+ description?: string;
32
+ agent?: string;
33
+ model?: string;
34
+ subtask?: boolean;
35
+ }
36
+ >;
37
+ }
38
+
39
+ // Config is read from the shared CortexKit location. The location migrator
40
+ // (migrate-config-location.ts) runs at plugin init and moves legacy per-harness
41
+ // files to the CortexKit path before the loader runs. When the migration could
42
+ // NOT complete (it refuses on an OpenCode-vs-Pi config that differs, or it never
43
+ // ran in this process), the CortexKit base is absent — in that case the loader
44
+ // reads THIS harness's own legacy file as a non-destructive fallback rather than
45
+ // silently using schema defaults (which would re-enable features the user's real
46
+ // config disabled). See resolveLegacyReadFallback.
47
+ function getUserConfigBasePath(): string {
48
+ return cortexKitUserConfigBasePath();
49
+ }
50
+
51
+ function getProjectConfigBasePath(directory: string): string {
52
+ return cortexKitProjectConfigBasePath(directory);
53
+ }
54
+
55
+ interface LegacyReadFallback {
56
+ /** The legacy file we read, or null if no harness-owned legacy file exists. */
57
+ source: LegacyConfigSource | null;
58
+ }
59
+
60
+ /**
61
+ * First existing legacy source owned by the OpenCode harness, for the read
62
+ * fallback when the CortexKit base is absent. Pi has its own loader with the
63
+ * symmetric Pi-scoped fallback.
64
+ */
65
+ function resolveLegacyReadFallback(sources: readonly LegacyConfigSource[]): LegacyReadFallback {
66
+ return { source: sources.find((s) => existsSync(s.path)) ?? null };
67
+ }
68
+
69
+ interface LoadedConfigFile {
70
+ config: Record<string, unknown>;
71
+ /** Warnings from {env:} / {file:} substitution, with config-path prefix applied. */
72
+ warnings: string[];
73
+ }
74
+
75
+ export type LoadOutcome =
76
+ | "ok"
77
+ | "project-file-parse-error"
78
+ | "project-file-io-error"
79
+ | "legacy-config-unmigrated"
80
+ | "schema-recovery"
81
+ | "substitution-failure";
82
+
83
+ export interface LoadResultDetailed {
84
+ config: MagicContextPluginConfig & { configWarnings?: string[] };
85
+ loadOutcome: LoadOutcome;
86
+ sources: {
87
+ userConfig: LoadOutcome;
88
+ projectConfig: LoadOutcome;
89
+ };
90
+ substitutionFailures: Array<{ keyPath: string; source: "user" | "project"; message: string }>;
91
+ recoveredTopLevelKeys: string[];
92
+ }
93
+
94
+ interface LoadedConfigFileDetailed extends LoadedConfigFile {
95
+ outcome: LoadOutcome;
96
+ source: "user" | "project";
97
+ }
98
+
99
+ function loadConfigFileDetailed(
100
+ configPath: string,
101
+ source: "user" | "project",
102
+ ): LoadedConfigFileDetailed | null {
103
+ if (!existsSync(configPath)) {
104
+ return null;
105
+ }
106
+
107
+ let rawText: string;
108
+ try {
109
+ rawText = readFileSync(configPath, "utf-8");
110
+ } catch (error) {
111
+ return {
112
+ config: {},
113
+ warnings: [
114
+ `${configPath}: failed to read config: ${error instanceof Error ? error.message : String(error)}`,
115
+ ],
116
+ outcome: "project-file-io-error",
117
+ source,
118
+ };
119
+ }
120
+
121
+ try {
122
+ const substituted = substituteConfigVariables({
123
+ text: rawText,
124
+ configPath,
125
+ isProjectConfig: source === "project",
126
+ });
127
+ return {
128
+ config: parseJsonc<Record<string, unknown>>(substituted.text),
129
+ warnings: substituted.warnings.map((w) => `${configPath}: ${w}`),
130
+ outcome: substituted.warnings.length > 0 ? "substitution-failure" : "ok",
131
+ source,
132
+ };
133
+ } catch (error) {
134
+ return {
135
+ config: {},
136
+ warnings: [
137
+ `${configPath}: failed to load config: ${error instanceof Error ? error.message : String(error)}`,
138
+ ],
139
+ outcome: "project-file-parse-error",
140
+ source,
141
+ };
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Deep-merge two raw JSON objects. Both inputs must come from BEFORE Zod
147
+ * parsing — otherwise Zod-filled defaults appear as if they were explicit
148
+ * overrides and clobber genuine values from the other source.
149
+ *
150
+ * Plain object values merge recursively. Arrays, primitives, and `null` are
151
+ * replaced atomically (override wins). This matches typical config-merge
152
+ * semantics: arrays like `disabled_hooks` should be set whole, not interleaved
153
+ * element-wise.
154
+ *
155
+ * `disabled_hooks` is the one exception: we union-merge it below so user
156
+ * and project can both contribute hook IDs without one silently losing the
157
+ * other's entries.
158
+ */
159
+ function deepMergeRawConfig(
160
+ base: Record<string, unknown>,
161
+ override: Record<string, unknown>,
162
+ ): Record<string, unknown> {
163
+ const result: Record<string, unknown> = { ...base };
164
+ for (const key of Object.keys(override)) {
165
+ const baseVal = base[key];
166
+ const overrideVal = override[key];
167
+ if (
168
+ baseVal !== null &&
169
+ typeof baseVal === "object" &&
170
+ !Array.isArray(baseVal) &&
171
+ overrideVal !== null &&
172
+ typeof overrideVal === "object" &&
173
+ !Array.isArray(overrideVal)
174
+ ) {
175
+ result[key] = deepMergeRawConfig(
176
+ baseVal as Record<string, unknown>,
177
+ overrideVal as Record<string, unknown>,
178
+ );
179
+ } else if (
180
+ key === "disabled_hooks" &&
181
+ Array.isArray(baseVal) &&
182
+ Array.isArray(overrideVal)
183
+ ) {
184
+ // Union-merge so user + project can both disable hooks without
185
+ // one source erasing the other's entries.
186
+ result[key] = [...new Set([...baseVal, ...overrideVal])];
187
+ } else {
188
+ result[key] = overrideVal;
189
+ }
190
+ }
191
+ return result;
192
+ }
193
+
194
+ /**
195
+ * Render a config value for a warning message in a way that never leaks resolved
196
+ * secrets from `{env:API_KEY}` / `{file:...}` substitution.
197
+ *
198
+ * Strings, numbers, booleans, and nulls are shown as type-plus-length so the
199
+ * user can still diagnose the problem ("string, 48 chars", "number 200001") but
200
+ * never see the resolved content. Objects and arrays are shown as their
201
+ * structural shape only. `undefined` / missing values are reported as
202
+ * `<missing>`.
203
+ */
204
+ function redactConfigValue(value: unknown): string {
205
+ if (value === undefined) return "<missing>";
206
+ if (value === null) return "null";
207
+ if (typeof value === "string")
208
+ return `string, ${value.length} char${value.length === 1 ? "" : "s"}`;
209
+ if (typeof value === "number") return `number ${value}`;
210
+ if (typeof value === "boolean") return `boolean ${value}`;
211
+ if (Array.isArray(value)) return `array, ${value.length} item${value.length === 1 ? "" : "s"}`;
212
+ if (typeof value === "object") {
213
+ const keys = Object.keys(value as Record<string, unknown>);
214
+ return `object with keys [${keys.join(", ")}]`;
215
+ }
216
+ return typeof value;
217
+ }
218
+
219
+ function parsePluginConfig(
220
+ rawConfig: Record<string, unknown>,
221
+ recoveredTopLevelKeys: string[] = [],
222
+ ): MagicContextPluginConfig & { configWarnings?: string[] } {
223
+ // Pre-Zod shim: reshape legacy experimental.* graduated keys so the user's
224
+ // opt-in/out state survives upgrades even when they never run `doctor`.
225
+ const preMigrationWarnings: string[] = [];
226
+ const migratedExperimental = migrateLegacyExperimental(rawConfig, preMigrationWarnings);
227
+ // Dreamer v2: convert the legacy v1 dreamer shape (window schedule, tasks
228
+ // array, user_memories/pin_key_files blocks) into the per-task `tasks` record.
229
+ // Runs AFTER migrate-experimental so experimental.user_memories (already
230
+ // relocated to dreamer.user_memories above) is folded into the v2 tasks here.
231
+ const migratedDreamer = migrateDreamerV2(migratedExperimental, preMigrationWarnings);
232
+ const migrated = migrateLegacyAgentEnabledInMemory(migratedDreamer, preMigrationWarnings);
233
+ const parsed = MagicContextConfigSchema.safeParse(migrated);
234
+ const disabledHooks = Array.isArray(rawConfig.disabled_hooks)
235
+ ? rawConfig.disabled_hooks.filter((value): value is string => typeof value === "string")
236
+ : undefined;
237
+ const command =
238
+ typeof rawConfig.command === "object" && rawConfig.command !== null
239
+ ? (rawConfig.command as MagicContextPluginConfig["command"])
240
+ : undefined;
241
+
242
+ if (parsed.success) {
243
+ return {
244
+ ...parsed.data,
245
+ disabled_hooks: disabledHooks,
246
+ command,
247
+ ...(preMigrationWarnings.length > 0 ? { configWarnings: preMigrationWarnings } : {}),
248
+ };
249
+ }
250
+
251
+ // Full parse failed — recover field-by-field using defaults for invalid fields.
252
+ // Agent configs (historian, dreamer, sidekick) are dropped on error rather than defaulted
253
+ // because wrong model config could run expensive models or fail silently.
254
+ const defaults = MagicContextConfigSchema.parse({});
255
+ const warnings: string[] = [];
256
+
257
+ // Build a patched copy of rawConfig, replacing invalid fields with undefined
258
+ // so Zod fills in defaults on the second parse.
259
+ const errorPaths = new Set<string>();
260
+ // Collect any custom Zod messages per top-level key so a field with an
261
+ // explanatory `.max(..., "why")` / `.refine(..., "why")` message surfaces the
262
+ // reason to the user instead of a bare "invalid value" (e.g. issue #111's
263
+ // execute_threshold cache-safety explanation). Only non-default Zod messages
264
+ // are kept — the generic "Too big"/"Invalid input" boilerplate adds nothing.
265
+ const customMessagesByKey = new Map<string, string>();
266
+ // Per top-level key, the set of FULL error paths (e.g. ["memory","auto_search"]).
267
+ // Used to prune only the invalid nested leaf instead of the whole block.
268
+ const issuePathsByKey = new Map<string, PropertyKey[][]>();
269
+ const GENERIC_ZOD_PREFIXES = ["Too big", "Too small", "Invalid input", "Invalid", "Expected"];
270
+ for (const issue of parsed.error.issues) {
271
+ const topKey = issue.path[0];
272
+ if (topKey !== undefined) {
273
+ const key = String(topKey);
274
+ errorPaths.add(key);
275
+ const paths = issuePathsByKey.get(key) ?? [];
276
+ paths.push([...issue.path]);
277
+ issuePathsByKey.set(key, paths);
278
+ const msg = issue.message;
279
+ if (msg && !GENERIC_ZOD_PREFIXES.some((p) => msg.startsWith(p))) {
280
+ if (!customMessagesByKey.has(key)) {
281
+ customMessagesByKey.set(key, msg);
282
+ }
283
+ }
284
+ }
285
+ }
286
+
287
+ const patched: Record<string, unknown> = { ...rawConfig };
288
+ for (const key of errorPaths) {
289
+ recoveredTopLevelKeys.push(key);
290
+ const isAgentConfig = key === "historian" || key === "dreamer" || key === "sidekick";
291
+ if (isAgentConfig) {
292
+ // Drop agent configs entirely on error — don't default them
293
+ delete patched[key];
294
+ warnings.push(
295
+ `"${key}": invalid agent configuration, ignoring. Check your magic-context.jsonc.`,
296
+ );
297
+ continue;
298
+ }
299
+
300
+ // For object-valued keys (e.g. `memory`), prune ONLY the invalid nested
301
+ // leaves and keep valid siblings, so one bad nested field doesn't wipe the
302
+ // whole block — which would silently drop already-migrated graduated keys
303
+ // like memory.auto_search / memory.git_commit_indexing. Falls back to
304
+ // whole-key deletion when the issue is at the key itself or the value
305
+ // isn't a prunable object.
306
+ const issuePaths = issuePathsByKey.get(key) ?? [];
307
+ const rawValue = rawConfig[key];
308
+ const allNested =
309
+ issuePaths.length > 0 &&
310
+ issuePaths.every((p) => p.length >= 2) &&
311
+ typeof rawValue === "object" &&
312
+ rawValue !== null &&
313
+ !Array.isArray(rawValue);
314
+ if (allNested) {
315
+ let prunedBlock: Record<string, unknown> = {
316
+ ...(rawValue as Record<string, unknown>),
317
+ };
318
+ const prunedLeaves: string[] = [];
319
+ for (const p of issuePaths) {
320
+ // p is the full Zod issue path ([key, ...nested]); prune the
321
+ // DEEPEST invalid leaf, not just the first child (p[1]), so a
322
+ // 3-level path like memory.git_commit_indexing.since_days drops
323
+ // only `since_days` and keeps a sibling `enabled: false`.
324
+ const relative = p.slice(1);
325
+ const result = pruneNestedConfigLeaf(prunedBlock, relative);
326
+ if (result) {
327
+ prunedBlock = result.block;
328
+ prunedLeaves.push(result.removed);
329
+ }
330
+ }
331
+ patched[key] = prunedBlock;
332
+ const reason = customMessagesByKey.get(key);
333
+ warnings.push(
334
+ `"${key}": invalid nested field(s) ${prunedLeaves.map((l) => `"${l}"`).join(", ")}, using defaults for those.${reason ? ` ${reason}` : ""}`,
335
+ );
336
+ continue;
337
+ }
338
+
339
+ // Use Zod default for this field.
340
+ // Intentional: redactConfigValue reports type+length, never the
341
+ // resolved value itself, because `{env:...}` / `{file:...}`
342
+ // substitution may have already expanded secrets into rawConfig.
343
+ delete patched[key];
344
+ const defaultVal = (defaults as unknown as Record<string, unknown>)[key];
345
+ const reason = customMessagesByKey.get(key);
346
+ warnings.push(
347
+ `"${key}": invalid value (${redactConfigValue(rawConfig[key])}), using default ${JSON.stringify(defaultVal)}.${reason ? ` ${reason}` : ""}`,
348
+ );
349
+ }
350
+
351
+ // Re-run migration on the field-recovered patched config so legacy
352
+ // experimental + dreamer-v1 blocks still migrate on the recovery path.
353
+ const retryMigrated = migrateLegacyAgentEnabledInMemory(
354
+ migrateDreamerV2(
355
+ migrateLegacyExperimental(patched, preMigrationWarnings),
356
+ preMigrationWarnings,
357
+ ),
358
+ preMigrationWarnings,
359
+ );
360
+ const retryParsed = MagicContextConfigSchema.safeParse(retryMigrated);
361
+ if (retryParsed.success) {
362
+ return {
363
+ ...retryParsed.data,
364
+ disabled_hooks: disabledHooks,
365
+ command,
366
+ configWarnings: [...preMigrationWarnings, ...warnings],
367
+ };
368
+ }
369
+
370
+ // If even the patched version fails (shouldn't happen), fall back to full defaults
371
+ // but keep enabled:true — the user intended to use the plugin.
372
+ warnings.push("Config recovery failed, using all defaults.");
373
+ return {
374
+ ...defaults,
375
+ disabled_hooks: disabledHooks,
376
+ command,
377
+ configWarnings: [...preMigrationWarnings, ...warnings],
378
+ };
379
+ }
380
+
381
+ export function loadPluginConfig(
382
+ directory: string,
383
+ ): MagicContextPluginConfig & { configWarnings?: string[] } {
384
+ // Delegate to the detailed loader so there is exactly ONE config-resolution
385
+ // path. The detailed variant owns the read-legacy-on-conflict fallback (when
386
+ // the shared CortexKit base is absent, read THIS harness's own legacy config
387
+ // instead of falling to schema defaults), the project-config hardening, and
388
+ // the embedding-redirect guard. Having a second hand-maintained copy here is
389
+ // how the read-legacy fallback silently missed the runtime init path
390
+ // (index.ts calls loadPluginConfig) while only the embedding bootstrap got
391
+ // it — a config-drop bug. The extra detailed fields (outcome, sources,
392
+ // substitutionFailures) are simply dropped for callers that only need the
393
+ // config + warnings.
394
+ return loadPluginConfigDetailed(directory).config;
395
+ }
396
+
397
+ function hasUserTierSubcConfig(config: Record<string, unknown> | undefined): boolean {
398
+ const subc = config?.subc;
399
+ if (typeof subc !== "object" || subc === null || Array.isArray(subc)) return false;
400
+ const connectionFile = (subc as Record<string, unknown>).connection_file;
401
+ return typeof connectionFile === "string" && connectionFile.trim().length > 0;
402
+ }
403
+
404
+ function collectEmptyStringPaths(value: unknown, prefix = ""): string[] {
405
+ if (typeof value === "string") {
406
+ return value === "" && prefix ? [prefix] : [];
407
+ }
408
+ if (Array.isArray(value) || value === null || typeof value !== "object") {
409
+ return [];
410
+ }
411
+
412
+ const paths: string[] = [];
413
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
414
+ const nextPrefix = prefix ? `${prefix}.${key}` : key;
415
+ paths.push(...collectEmptyStringPaths(child, nextPrefix));
416
+ }
417
+ return paths;
418
+ }
419
+
420
+ function bindSubstitutionFailures(
421
+ loaded: LoadedConfigFileDetailed | null,
422
+ ): Array<{ keyPath: string; source: "user" | "project"; message: string }> {
423
+ if (!loaded || loaded.warnings.length === 0 || loaded.outcome !== "substitution-failure") {
424
+ return [];
425
+ }
426
+
427
+ const emptyPaths = collectEmptyStringPaths(loaded.config);
428
+ return loaded.warnings.map((message) => {
429
+ const matchedPath = emptyPaths.find((path) => {
430
+ const tail = path.split(".").at(-1) ?? path;
431
+ return message.includes(path) || message.toLowerCase().includes(tail.toLowerCase());
432
+ });
433
+ return { keyPath: matchedPath ?? "<unknown>", source: loaded.source, message };
434
+ });
435
+ }
436
+
437
+ function combinedOutcome(args: {
438
+ sources: LoadResultDetailed["sources"];
439
+ substitutionFailures: LoadResultDetailed["substitutionFailures"];
440
+ recoveredTopLevelKeys: string[];
441
+ }): LoadOutcome {
442
+ const sourceOutcomes = Object.values(args.sources);
443
+ if (sourceOutcomes.includes("project-file-parse-error")) return "project-file-parse-error";
444
+ if (sourceOutcomes.includes("project-file-io-error")) return "project-file-io-error";
445
+ if (sourceOutcomes.includes("legacy-config-unmigrated")) return "legacy-config-unmigrated";
446
+ if (args.recoveredTopLevelKeys.length > 0) return "schema-recovery";
447
+ if (args.substitutionFailures.length > 0) return "substitution-failure";
448
+ return "ok";
449
+ }
450
+
451
+ export function loadPluginConfigDetailed(directory: string): LoadResultDetailed {
452
+ const userDetected = detectConfigFile(getUserConfigBasePath());
453
+ const projectDetected = detectConfigFile(getProjectConfigBasePath(directory));
454
+ // Both-harness sources drive the GC-suppression signal; this-harness sources
455
+ // (OpenCode) drive the non-destructive read fallback when the base is absent.
456
+ const legacySources = resolveLegacyConfigSources(directory);
457
+ const harnessLegacy = resolveLegacyConfigSourcesForHarness(directory, "opencode");
458
+
459
+ // When the CortexKit base is absent (migration refused on a differing
460
+ // OpenCode/Pi pair, or hasn't run), read THIS harness's own legacy file
461
+ // instead of falling to schema defaults that would silently re-enable
462
+ // features the user disabled.
463
+ const userLegacyFallback =
464
+ userDetected.format === "none"
465
+ ? resolveLegacyReadFallback(harnessLegacy.user)
466
+ : { source: null };
467
+ const projectLegacyFallback =
468
+ projectDetected.format === "none"
469
+ ? resolveLegacyReadFallback(harnessLegacy.project)
470
+ : { source: null };
471
+
472
+ // "Unmigrated" (→ untrusted, GC suppressed) ONLY when the base is absent,
473
+ // some legacy config exists, AND we did NOT read this harness's own legacy.
474
+ // If we read our own legacy the config is real → trusted. If only the OTHER
475
+ // harness's legacy exists we fell to defaults → keep GC suppressed so a
476
+ // default-config start can't reap the other harness's embedding vectors.
477
+ const legacyUserUnmigrated =
478
+ userDetected.format === "none" &&
479
+ !userLegacyFallback.source &&
480
+ legacySources.user.some((source) => existsSync(source.path));
481
+ const legacyProjectUnmigrated =
482
+ projectDetected.format === "none" &&
483
+ !projectLegacyFallback.source &&
484
+ legacySources.project.some((source) => existsSync(source.path));
485
+
486
+ const userLoaded =
487
+ userDetected.format !== "none"
488
+ ? loadConfigFileDetailed(userDetected.path, "user")
489
+ : userLegacyFallback.source
490
+ ? loadConfigFileDetailed(userLegacyFallback.source.path, "user")
491
+ : null;
492
+ const projectLoaded =
493
+ projectDetected.format !== "none"
494
+ ? loadConfigFileDetailed(projectDetected.path, "project")
495
+ : projectLegacyFallback.source
496
+ ? loadConfigFileDetailed(projectLegacyFallback.source.path, "project")
497
+ : null;
498
+
499
+ const allWarnings: string[] = [];
500
+ let mergedRaw: Record<string, unknown> = {};
501
+ // Threshold trust boundary is relative to the USER/default effective config:
502
+ // a cloned repo may delay compaction, but it may not lower thresholds in a
503
+ // way that forces extra historian work on the user's account.
504
+ const trustedBaseConfig = parsePluginConfig(userLoaded?.config ?? {});
505
+
506
+ if (userLegacyFallback.source) {
507
+ allWarnings.push(
508
+ `[user config] reading legacy config from ${userLegacyFallback.source.path} until migration completes; run \`npx @cortexkit/magic-context doctor\` to consolidate into the shared CortexKit location.`,
509
+ );
510
+ } else if (legacyUserUnmigrated) {
511
+ allWarnings.push(
512
+ "[user config] legacy Magic Context config exists but the shared CortexKit config is absent; embedding registration is paused until config migration completes.",
513
+ );
514
+ }
515
+
516
+ if (projectLegacyFallback.source) {
517
+ allWarnings.push(
518
+ `[project config] reading legacy config from ${projectLegacyFallback.source.path} until migration completes; run \`npx @cortexkit/magic-context doctor\` to consolidate into the shared CortexKit location.`,
519
+ );
520
+ } else if (legacyProjectUnmigrated) {
521
+ allWarnings.push(
522
+ "[project config] legacy Magic Context config exists but the shared CortexKit config is absent; embedding registration is paused until config migration completes.",
523
+ );
524
+ }
525
+
526
+ if (userLoaded) {
527
+ allWarnings.push(...userLoaded.warnings.map((w) => `[user config] ${w}`));
528
+ mergedRaw = deepMergeRawConfig(mergedRaw, userLoaded.config);
529
+ }
530
+
531
+ if (projectLoaded) {
532
+ allWarnings.push(...projectLoaded.warnings.map((w) => `[project config] ${w}`));
533
+ const projectRaw = { ...projectLoaded.config };
534
+ for (const warning of stripUnsafeProjectConfigFields(projectRaw)) {
535
+ allWarnings.push(`[project config] ${warning}`);
536
+ }
537
+ mergedRaw = deepMergeRawConfig(mergedRaw, projectRaw);
538
+ for (const warning of dropInheritedEmbeddingKeyOnRedirect(
539
+ projectRaw,
540
+ mergedRaw,
541
+ userLoaded?.config,
542
+ )) {
543
+ allWarnings.push(`[project config] ${warning}`);
544
+ }
545
+ for (const warning of constrainProjectThresholdOverrides({
546
+ mergedRaw,
547
+ projectRaw,
548
+ trustedBaseConfig,
549
+ })) {
550
+ allWarnings.push(`[project config] ${warning}`);
551
+ }
552
+ }
553
+
554
+ const recoveredTopLevelKeys: string[] = [];
555
+ const config = parsePluginConfig(mergedRaw, recoveredTopLevelKeys);
556
+ setOutputReserveConfig(config.output_reserve);
557
+ if (config.configWarnings?.length) {
558
+ allWarnings.push(
559
+ ...config.configWarnings.map((w) => {
560
+ if (userLoaded && projectLoaded) return `[config] ${w}`;
561
+ if (userLoaded) return `[user config] ${w}`;
562
+ return `[project config] ${w}`;
563
+ }),
564
+ );
565
+ }
566
+
567
+ const resolvedTransformMode = resolveTransformMode({
568
+ configured: config.transform_mode,
569
+ userTierHasSubc: hasUserTierSubcConfig(userLoaded?.config),
570
+ compactionEnabled: isCompactionEnabled(config),
571
+ });
572
+ config.transform_mode = resolvedTransformMode.mode;
573
+ allWarnings.push(...resolvedTransformMode.warnings.map((warning) => `[config] ${warning}`));
574
+
575
+ if (allWarnings.length > 0) {
576
+ config.configWarnings = allWarnings;
577
+ } else if ("configWarnings" in config) {
578
+ config.configWarnings = undefined;
579
+ }
580
+
581
+ const substitutionFailures = [
582
+ ...bindSubstitutionFailures(userLoaded),
583
+ ...bindSubstitutionFailures(projectLoaded),
584
+ ];
585
+ const sources = {
586
+ userConfig:
587
+ userLoaded?.outcome ??
588
+ (legacyUserUnmigrated ? "legacy-config-unmigrated" : ("ok" as LoadOutcome)),
589
+ projectConfig:
590
+ projectLoaded?.outcome ??
591
+ (legacyProjectUnmigrated ? "legacy-config-unmigrated" : ("ok" as LoadOutcome)),
592
+ };
593
+
594
+ return {
595
+ config,
596
+ loadOutcome: combinedOutcome({ sources, substitutionFailures, recoveredTopLevelKeys }),
597
+ sources,
598
+ substitutionFailures,
599
+ recoveredTopLevelKeys,
600
+ };
601
+ }