@cortexkit/opencode-magic-context 0.33.1 → 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 (205) 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 +82 -2
  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-n48c783v.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 +57 -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.map +1 -1
  17. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  21. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  23. package/dist/features/magic-context/fail-closed-block.d.ts +10 -0
  24. package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
  25. package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts.map +1 -1
  26. package/dist/features/magic-context/git-commits/storage-git-commits.d.ts.map +1 -1
  27. package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
  28. package/dist/features/magic-context/memory/embedding-local.d.ts +7 -1
  29. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  30. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  31. package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -1
  32. package/dist/features/magic-context/memory/project-identity.d.ts +2 -6
  33. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  34. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
  35. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  36. package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
  37. package/dist/features/magic-context/mural/render-mural.d.ts.map +1 -1
  38. package/dist/features/magic-context/overflow-detection.d.ts +8 -1
  39. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  40. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  41. package/dist/features/magic-context/scheduler.d.ts.map +1 -1
  42. package/dist/features/magic-context/schema-fence-probe.d.ts +28 -0
  43. package/dist/features/magic-context/schema-fence-probe.d.ts.map +1 -0
  44. package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
  45. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  46. package/dist/features/magic-context/storage-db.d.ts +35 -1
  47. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  48. package/dist/features/magic-context/storage-meta-persisted.d.ts +38 -4
  49. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  50. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  51. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  52. package/dist/features/magic-context/storage.d.ts +1 -1
  53. package/dist/features/magic-context/storage.d.ts.map +1 -1
  54. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/boundary-execution.d.ts +1 -1
  56. package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/cache-busting-signals.d.ts +1 -0
  58. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/child-session-spawn.d.ts +26 -0
  60. package/dist/hooks/magic-context/child-session-spawn.d.ts.map +1 -0
  61. package/dist/hooks/magic-context/command-handler.d.ts +2 -0
  62. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/compaction-off-transition.d.ts +96 -0
  64. package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -0
  65. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -0
  66. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  67. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/compartment-trigger.d.ts +12 -5
  72. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +47 -1
  74. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/emergency-drop.d.ts +2 -2
  76. package/dist/hooks/magic-context/event-handler.d.ts +7 -0
  77. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  78. package/dist/hooks/magic-context/event-resolvers.d.ts +13 -21
  79. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  80. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  81. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  82. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -1
  83. package/dist/hooks/magic-context/hook-handlers.d.ts +1 -0
  84. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/hook.d.ts +11 -1
  86. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  87. package/dist/hooks/magic-context/inject-compartments.d.ts +9 -0
  88. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  89. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  92. package/dist/hooks/magic-context/rust-mode-transform.d.ts +3 -0
  93. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  94. package/dist/hooks/magic-context/send-session-notification.d.ts +20 -1
  95. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -0
  97. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +30 -0
  99. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/transform.d.ts +27 -0
  101. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  102. package/dist/{index-ey1bc14z.js → index-b563sga5.js} +7 -264
  103. package/dist/{index-y1c9tcm3.js → index-cn960ffd.js} +101 -20
  104. package/dist/{index-xyaekybw.js → index-dtas91s5.js} +6 -4
  105. package/dist/index-fcyvznch.js +279 -0
  106. package/dist/{index-63dq2g4d.js → index-ry8rh8cv.js} +138 -24
  107. package/dist/{index-c2gahrks.js → index-s0mdn690.js} +1599 -892
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +1292 -623
  110. package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
  111. package/dist/plugin/hooks/create-session-hooks.d.ts +11 -0
  112. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  113. package/dist/plugin/messages-transform.d.ts +10 -0
  114. package/dist/plugin/messages-transform.d.ts.map +1 -1
  115. package/dist/plugin/rpc-handlers.d.ts +3 -7
  116. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  117. package/dist/plugin/tool-registry.d.ts +8 -0
  118. package/dist/plugin/tool-registry.d.ts.map +1 -1
  119. package/dist/{recomp-orchestrator-2yb2hfh2.js → recomp-orchestrator-8e82vmvb.js} +6 -4
  120. package/dist/send-session-notification-6njqewzk.js +20 -0
  121. package/dist/shared/conflict-detector.d.ts +38 -1
  122. package/dist/shared/conflict-detector.d.ts.map +1 -1
  123. package/dist/shared/conflict-fixer.d.ts +18 -1
  124. package/dist/shared/conflict-fixer.d.ts.map +1 -1
  125. package/dist/shared/context-limit-provenance.d.ts +3 -0
  126. package/dist/shared/context-limit-provenance.d.ts.map +1 -0
  127. package/dist/shared/escalation-bands.d.ts +9 -0
  128. package/dist/shared/escalation-bands.d.ts.map +1 -0
  129. package/dist/shared/format-threshold.d.ts +1 -1
  130. package/dist/shared/models-dev-cache.d.ts +29 -3
  131. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  132. package/dist/shared/rpc-server.d.ts.map +1 -1
  133. package/dist/shared/rpc-types.d.ts +46 -1
  134. package/dist/shared/rpc-types.d.ts.map +1 -1
  135. package/dist/shared/storage-permissions.d.ts +5 -0
  136. package/dist/shared/storage-permissions.d.ts.map +1 -0
  137. package/dist/{storage-meta-session-td6bwtky.js → storage-meta-session-xt41zbw4.js} +2 -1
  138. package/dist/tui/compaction-off.d.ts +13 -0
  139. package/dist/tui/compaction-off.d.ts.map +1 -0
  140. package/dist/tui/data/context-db.d.ts.map +1 -1
  141. package/dist/tui-compiled/compaction-off.d.ts +13 -0
  142. package/dist/tui-compiled/compaction-off.d.ts.map +1 -0
  143. package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
  144. package/package.json +7 -2
  145. package/src/agents/dreamer.ts +68 -0
  146. package/src/agents/hidden-agent-registrations.ts +331 -0
  147. package/src/agents/historian.ts +3 -0
  148. package/src/agents/language-directive.test.ts +138 -0
  149. package/src/agents/language-directive.ts +121 -0
  150. package/src/agents/magic-context-prompt.language.test.ts +35 -0
  151. package/src/agents/magic-context-prompt.test.ts +292 -0
  152. package/src/agents/magic-context-prompt.ts +182 -0
  153. package/src/agents/permissions.test.ts +252 -0
  154. package/src/agents/permissions.ts +186 -0
  155. package/src/agents/sidekick.ts +1 -0
  156. package/src/agents/smart-note-compiler.ts +1 -0
  157. package/src/config/agent-disable.ts +106 -0
  158. package/src/config/compaction-accessor-guard.test.ts +82 -0
  159. package/src/config/compaction-backcompat.test.ts +52 -0
  160. package/src/config/index.test.ts +904 -0
  161. package/src/config/index.ts +601 -0
  162. package/src/config/migrate-config-location.test.ts +332 -0
  163. package/src/config/migrate-config-location.ts +635 -0
  164. package/src/config/migrate-dreamer-v2.test.ts +287 -0
  165. package/src/config/migrate-dreamer-v2.ts +336 -0
  166. package/src/config/migrate-experimental.ts +159 -0
  167. package/src/config/project-security.test.ts +366 -0
  168. package/src/config/project-security.ts +588 -0
  169. package/src/config/prune-config-leaf.test.ts +78 -0
  170. package/src/config/prune-config-leaf.ts +68 -0
  171. package/src/config/removed-config-keys.test.ts +116 -0
  172. package/src/config/schema/agent-overrides.ts +45 -0
  173. package/src/config/schema/magic-context.test.ts +358 -0
  174. package/src/config/schema/magic-context.ts +1002 -0
  175. package/src/config/transform-mode.test.ts +62 -0
  176. package/src/config/transform-mode.ts +34 -0
  177. package/src/config/variable.test.ts +323 -0
  178. package/src/config/variable.ts +192 -0
  179. package/src/features/magic-context/defaults.ts +1 -0
  180. package/src/features/magic-context/dreamer/cron.ts +243 -0
  181. package/src/features/magic-context/dreamer/task-registry.ts +104 -0
  182. package/src/shared/conflict-detector.test.ts +121 -0
  183. package/src/shared/conflict-detector.ts +50 -3
  184. package/src/shared/conflict-fixer.test.ts +309 -0
  185. package/src/shared/conflict-fixer.ts +82 -5
  186. package/src/shared/context-limit-provenance.ts +6 -0
  187. package/src/shared/escalation-bands.test.ts +23 -0
  188. package/src/shared/escalation-bands.ts +18 -0
  189. package/src/shared/format-threshold.ts +1 -1
  190. package/src/shared/models-dev-cache.test.ts +171 -0
  191. package/src/shared/models-dev-cache.ts +195 -38
  192. package/src/shared/rpc-server.ts +27 -18
  193. package/src/shared/rpc-types.ts +46 -1
  194. package/src/shared/storage-permissions.ts +20 -0
  195. package/src/tui/compaction-off.test.ts +56 -0
  196. package/src/tui/compaction-off.ts +29 -0
  197. package/src/tui/data/context-db.ts +6 -0
  198. package/src/tui/index.tsx +113 -65
  199. package/src/tui/slots/sidebar-content.tsx +139 -83
  200. package/src/tui-compiled/compaction-off.ts +29 -0
  201. package/src/tui-compiled/data/context-db.ts +6 -0
  202. package/src/tui-compiled/index.tsx +558 -455
  203. package/src/tui-compiled/slots/sidebar-content.tsx +237 -161
  204. package/dist/send-session-notification-tghq9gnh.js +0 -11
  205. /package/dist/{index-v7fmc1s2.js → index-ygcng9t8.js} +0 -0
@@ -0,0 +1,332 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ rmSync,
8
+ utimesSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import {
14
+ type LegacyConfigSource,
15
+ migrateConfigFile,
16
+ resolveLegacyConfigSources,
17
+ } from "./migrate-config-location";
18
+
19
+ function tmp(): string {
20
+ return mkdtempSync(join(tmpdir(), "mc-cfgloc-"));
21
+ }
22
+
23
+ function src(path: string, label = "legacy"): LegacyConfigSource {
24
+ return { path, label };
25
+ }
26
+
27
+ describe("migrateConfigFile (location migration)", () => {
28
+ it("no-ops when no legacy source exists", () => {
29
+ const dir = tmp();
30
+ try {
31
+ const target = join(dir, ".cortexkit", "magic-context.jsonc");
32
+ const r = migrateConfigFile({
33
+ scope: "project",
34
+ targetPath: target,
35
+ legacySources: [src(join(dir, "magic-context.jsonc"))],
36
+ });
37
+ expect(r.migrated).toBe(false);
38
+ expect(r.conflict).toBe(false);
39
+ expect(existsSync(target)).toBe(false);
40
+ } finally {
41
+ rmSync(dir, { recursive: true, force: true });
42
+ }
43
+ });
44
+
45
+ it("reclaims a stale (crashed-holder) lock in one shot without blocking init", () => {
46
+ const dir = tmp();
47
+ try {
48
+ const target = join(dir, ".cortexkit", "magic-context.jsonc");
49
+ const legacy = join(dir, "magic-context.jsonc");
50
+ writeFileSync(legacy, '{"enabled":false}');
51
+ // A leftover lock dir from a crashed holder, aged past the stale
52
+ // threshold. The non-blocking lock removes it and re-attempts mkdir
53
+ // once (no busy-wait, no synchronous sleep), so migration proceeds
54
+ // and init is never frozen.
55
+ const lockDir = `${target}.lock`;
56
+ mkdirSync(lockDir, { recursive: true });
57
+ // Backdate the lock dir's mtime well past CONFIG_LOCK_STALE_MS (4s).
58
+ const old = Date.now() - 60_000;
59
+ utimesSync(lockDir, new Date(old), new Date(old));
60
+ const start = Date.now();
61
+ const r = migrateConfigFile({
62
+ scope: "project",
63
+ targetPath: target,
64
+ legacySources: [src(legacy)],
65
+ });
66
+ const elapsed = Date.now() - start;
67
+ // Effectively instant — no timeout budget is ever waited.
68
+ expect(elapsed).toBeLessThan(1_000);
69
+ expect(r.migrated).toBe(true);
70
+ expect(existsSync(target)).toBe(true);
71
+ } finally {
72
+ rmSync(dir, { recursive: true, force: true });
73
+ }
74
+ });
75
+
76
+ it("skips migration this run (no block) when a LIVE holder owns the lock", () => {
77
+ const dir = tmp();
78
+ try {
79
+ const target = join(dir, ".cortexkit", "magic-context.jsonc");
80
+ const legacy = join(dir, "magic-context.jsonc");
81
+ writeFileSync(legacy, '{"enabled":false}');
82
+ // A FRESH lock dir (live holder, not stale): we must not block waiting
83
+ // for it — return immediately with migrated:false and leave the
84
+ // target absent (the loader read-legacy-on-conflict covers this run;
85
+ // the next init retries once the holder releases).
86
+ mkdirSync(`${target}.lock`, { recursive: true });
87
+ const start = Date.now();
88
+ const r = migrateConfigFile({
89
+ scope: "project",
90
+ targetPath: target,
91
+ legacySources: [src(legacy)],
92
+ });
93
+ const elapsed = Date.now() - start;
94
+ expect(elapsed).toBeLessThan(1_000);
95
+ expect(r.migrated).toBe(false);
96
+ expect(r.conflict).toBe(false);
97
+ expect(existsSync(target)).toBe(false);
98
+ } finally {
99
+ rmSync(dir, { recursive: true, force: true });
100
+ }
101
+ });
102
+
103
+ it("moves a single legacy source to the target and leaves a .MOVED_READPLEASE marker", () => {
104
+ const dir = tmp();
105
+ try {
106
+ const legacy = join(dir, ".opencode", "magic-context.jsonc");
107
+ mkdirSync(join(dir, ".opencode"), { recursive: true });
108
+ writeFileSync(legacy, '{ "enabled": true }');
109
+ const target = join(dir, ".cortexkit", "magic-context.jsonc");
110
+
111
+ const r = migrateConfigFile({
112
+ scope: "project",
113
+ targetPath: target,
114
+ legacySources: [src(legacy)],
115
+ });
116
+
117
+ expect(r.migrated).toBe(true);
118
+ expect(r.conflict).toBe(false);
119
+ // Content is at the target...
120
+ expect(readFileSync(target, "utf8")).toContain("enabled");
121
+ // ...the legacy file is gone (idempotency comes from this)...
122
+ expect(existsSync(legacy)).toBe(false);
123
+ // ...and a human breadcrumb preserves the original below a header.
124
+ const marker = `${legacy}.MOVED_READPLEASE`;
125
+ expect(existsSync(marker)).toBe(true);
126
+ const markerText = readFileSync(marker, "utf8");
127
+ expect(markerText).toContain("configuration moved");
128
+ expect(markerText).toContain(target);
129
+ expect(markerText).toContain("enabled");
130
+ } finally {
131
+ rmSync(dir, { recursive: true, force: true });
132
+ }
133
+ });
134
+
135
+ it("is idempotent: a second run finds no legacy source and no-ops", () => {
136
+ const dir = tmp();
137
+ try {
138
+ const legacy = join(dir, "magic-context.jsonc");
139
+ writeFileSync(legacy, '{ "protected_tags": 5 }');
140
+ const target = join(dir, ".cortexkit", "magic-context.jsonc");
141
+ const opts = {
142
+ scope: "project" as const,
143
+ targetPath: target,
144
+ legacySources: [src(legacy)],
145
+ };
146
+ const first = migrateConfigFile(opts);
147
+ expect(first.migrated).toBe(true);
148
+ const second = migrateConfigFile(opts);
149
+ expect(second.migrated).toBe(false);
150
+ expect(second.conflict).toBe(false);
151
+ } finally {
152
+ rmSync(dir, { recursive: true, force: true });
153
+ }
154
+ });
155
+
156
+ it("moves legacy aside (no overwrite) when target already exists and matches semantically", () => {
157
+ const dir = tmp();
158
+ try {
159
+ const target = join(dir, ".cortexkit", "magic-context.jsonc");
160
+ mkdirSync(join(dir, ".cortexkit"), { recursive: true });
161
+ // Target = pretty-printed; legacy = compact + comment + reordered keys.
162
+ writeFileSync(target, '{\n "protected_tags": 5,\n "cache_ttl": "1h"\n}\n');
163
+ const legacy = join(dir, ".opencode", "magic-context.jsonc");
164
+ mkdirSync(join(dir, ".opencode"), { recursive: true });
165
+ writeFileSync(legacy, '// mine\n{ "cache_ttl": "1h", "protected_tags": 5, }');
166
+
167
+ const r = migrateConfigFile({
168
+ scope: "project",
169
+ targetPath: target,
170
+ legacySources: [src(legacy)],
171
+ });
172
+
173
+ expect(r.migrated).toBe(false);
174
+ expect(r.conflict).toBe(false);
175
+ // Target untouched (not overwritten), legacy moved aside.
176
+ expect(readFileSync(target, "utf8")).toContain('"protected_tags": 5');
177
+ expect(existsSync(legacy)).toBe(false);
178
+ expect(existsSync(`${legacy}.MOVED_READPLEASE`)).toBe(true);
179
+ } finally {
180
+ rmSync(dir, { recursive: true, force: true });
181
+ }
182
+ });
183
+
184
+ it("REFUSES (conflict) when target exists with different settings — leaves both untouched", () => {
185
+ const dir = tmp();
186
+ try {
187
+ const target = join(dir, ".cortexkit", "magic-context.jsonc");
188
+ mkdirSync(join(dir, ".cortexkit"), { recursive: true });
189
+ writeFileSync(target, '{ "protected_tags": 5 }');
190
+ const legacy = join(dir, ".opencode", "magic-context.jsonc");
191
+ mkdirSync(join(dir, ".opencode"), { recursive: true });
192
+ writeFileSync(legacy, '{ "protected_tags": 9 }');
193
+
194
+ const r = migrateConfigFile({
195
+ scope: "project",
196
+ targetPath: target,
197
+ legacySources: [src(legacy)],
198
+ });
199
+
200
+ expect(r.conflict).toBe(true);
201
+ expect(r.migrated).toBe(false);
202
+ expect(r.warnings.join("\n")).toContain("already exists with different settings");
203
+ // BOTH left as-is for manual reconciliation — never auto-clobbered.
204
+ expect(readFileSync(target, "utf8")).toContain('"protected_tags": 5');
205
+ expect(readFileSync(legacy, "utf8")).toContain('"protected_tags": 9');
206
+ expect(existsSync(`${legacy}.MOVED_READPLEASE`)).toBe(false);
207
+ } finally {
208
+ rmSync(dir, { recursive: true, force: true });
209
+ }
210
+ });
211
+
212
+ it("REFUSES (conflict) when multiple legacy sources disagree and no target exists", () => {
213
+ const dir = tmp();
214
+ try {
215
+ const a = join(dir, ".opencode", "magic-context.jsonc");
216
+ const b = join(dir, ".pi", "magic-context.jsonc");
217
+ mkdirSync(join(dir, ".opencode"), { recursive: true });
218
+ mkdirSync(join(dir, ".pi"), { recursive: true });
219
+ writeFileSync(a, '{ "protected_tags": 5 }');
220
+ writeFileSync(b, '{ "protected_tags": 9 }');
221
+ const target = join(dir, ".cortexkit", "magic-context.jsonc");
222
+
223
+ const r = migrateConfigFile({
224
+ scope: "project",
225
+ targetPath: target,
226
+ legacySources: [src(a), src(b)],
227
+ });
228
+
229
+ expect(r.conflict).toBe(true);
230
+ expect(r.migrated).toBe(false);
231
+ expect(existsSync(target)).toBe(false);
232
+ // Neither moved aside — user reconciles by hand.
233
+ expect(existsSync(a)).toBe(true);
234
+ expect(existsSync(b)).toBe(true);
235
+ } finally {
236
+ rmSync(dir, { recursive: true, force: true });
237
+ }
238
+ });
239
+
240
+ it("migrates once and moves ALL matching legacy sources aside when they agree", () => {
241
+ const dir = tmp();
242
+ try {
243
+ const a = join(dir, ".opencode", "magic-context.jsonc");
244
+ const b = join(dir, ".pi", "magic-context.jsonc");
245
+ mkdirSync(join(dir, ".opencode"), { recursive: true });
246
+ mkdirSync(join(dir, ".pi"), { recursive: true });
247
+ writeFileSync(a, '{ "protected_tags": 5 }');
248
+ writeFileSync(b, '{ "protected_tags": 5 }'); // same
249
+ const target = join(dir, ".cortexkit", "magic-context.jsonc");
250
+
251
+ const r = migrateConfigFile({
252
+ scope: "project",
253
+ targetPath: target,
254
+ legacySources: [src(a), src(b)],
255
+ });
256
+
257
+ expect(r.migrated).toBe(true);
258
+ expect(existsSync(target)).toBe(true);
259
+ expect(existsSync(`${a}.MOVED_READPLEASE`)).toBe(true);
260
+ expect(existsSync(`${b}.MOVED_READPLEASE`)).toBe(true);
261
+ } finally {
262
+ rmSync(dir, { recursive: true, force: true });
263
+ }
264
+ });
265
+ });
266
+
267
+ describe("resolveLegacyConfigSources", () => {
268
+ it("includes the bare-root project source unique to Magic Context", () => {
269
+ const sources = resolveLegacyConfigSources("/proj");
270
+ const projectPaths = sources.project.map((s) => s.path);
271
+ // bare root + .opencode + .pi, each in {.jsonc,.json}
272
+ expect(projectPaths).toContain("/proj/magic-context.jsonc");
273
+ expect(projectPaths).toContain("/proj/magic-context.json");
274
+ expect(projectPaths).toContain("/proj/.opencode/magic-context.jsonc");
275
+ expect(projectPaths).toContain("/proj/.pi/magic-context.jsonc");
276
+ });
277
+
278
+ it("includes both OpenCode and Pi user sources", () => {
279
+ const sources = resolveLegacyConfigSources("/proj");
280
+ const userPaths = sources.user.map((s) => s.path);
281
+ expect(userPaths.some((p) => p.includes(join("opencode", "magic-context.jsonc")))).toBe(
282
+ true,
283
+ );
284
+ expect(userPaths.some((p) => p.includes(join(".pi", "agent", "magic-context.jsonc")))).toBe(
285
+ true,
286
+ );
287
+ });
288
+
289
+ it("never lists a user-scope config as a project source when the project dir is the CortexKit config home", () => {
290
+ // Regression: opencode opened with cwd = ~/.config/cortexkit made the
291
+ // bare-root project source `<root>/magic-context.jsonc` collide with the
292
+ // USER config, so the project migration ate the user config into
293
+ // `<root>/.cortexkit/` and left the user on schema defaults.
294
+ const prev = process.env.XDG_CONFIG_HOME;
295
+ const home = tmp();
296
+ try {
297
+ process.env.XDG_CONFIG_HOME = home;
298
+ const cortexkitHome = join(home, "cortexkit");
299
+ const sources = resolveLegacyConfigSources(cortexkitHome);
300
+ const projectPaths = sources.project.map((s) => s.path);
301
+ // The user config path must NOT be a project migration source.
302
+ expect(projectPaths).not.toContain(join(cortexkitHome, "magic-context.jsonc"));
303
+ expect(projectPaths).not.toContain(join(cortexkitHome, "magic-context.json"));
304
+ // The genuine project subdir sources are still present.
305
+ expect(projectPaths).toContain(join(cortexkitHome, ".opencode", "magic-context.jsonc"));
306
+ expect(projectPaths).toContain(join(cortexkitHome, ".pi", "magic-context.jsonc"));
307
+ } finally {
308
+ if (prev === undefined) delete process.env.XDG_CONFIG_HOME;
309
+ else process.env.XDG_CONFIG_HOME = prev;
310
+ rmSync(home, { recursive: true, force: true });
311
+ }
312
+ });
313
+
314
+ it("never lists the OpenCode/Pi user legacy paths as project sources when the project dir is the user config dir", () => {
315
+ // Same collision class for a project opened directly in ~/.config/opencode
316
+ // (bare-root would resolve to the OpenCode user legacy config).
317
+ const prev = process.env.XDG_CONFIG_HOME;
318
+ const home = tmp();
319
+ try {
320
+ process.env.XDG_CONFIG_HOME = home;
321
+ const opencodeHome = join(home, "opencode");
322
+ const sources = resolveLegacyConfigSources(opencodeHome);
323
+ const projectPaths = sources.project.map((s) => s.path);
324
+ expect(projectPaths).not.toContain(join(opencodeHome, "magic-context.jsonc"));
325
+ expect(projectPaths).not.toContain(join(opencodeHome, "magic-context.json"));
326
+ } finally {
327
+ if (prev === undefined) delete process.env.XDG_CONFIG_HOME;
328
+ else process.env.XDG_CONFIG_HOME = prev;
329
+ rmSync(home, { recursive: true, force: true });
330
+ }
331
+ });
332
+ });