@cortexkit/opencode-magic-context 0.33.1 → 0.34.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 (240) hide show
  1. package/README.md +20 -3
  2. package/dist/config/agent-disable.d.ts +22 -0
  3. package/dist/config/agent-disable.d.ts.map +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/migrate-experimental.d.ts +2 -1
  6. package/dist/config/migrate-experimental.d.ts.map +1 -1
  7. package/dist/config/project-security.d.ts +9 -0
  8. package/dist/config/project-security.d.ts.map +1 -1
  9. package/dist/config/schema/magic-context.d.ts +90 -17
  10. package/dist/config/schema/magic-context.d.ts.map +1 -1
  11. package/dist/config/transform-mode.d.ts +2 -0
  12. package/dist/config/transform-mode.d.ts.map +1 -1
  13. package/dist/{conflict-warning-hook-n48c783v.js → conflict-warning-hook-fvpfvt0w.js} +1 -1
  14. package/dist/{derive-budgets-e9j09sjr.js → derive-budgets-9crp0cvd.js} +1 -1
  15. package/dist/features/builtin-commands/commands.d.ts +1 -1
  16. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  17. package/dist/features/magic-context/compaction-marker.d.ts +57 -0
  18. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  19. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/classify.d.ts +1 -0
  21. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  23. package/dist/features/magic-context/dreamer/map-memories.d.ts +1 -0
  24. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  25. package/dist/features/magic-context/dreamer/refresh-primers.d.ts +1 -0
  26. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  27. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +2 -0
  28. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  29. package/dist/features/magic-context/dreamer/task-executor.d.ts +4 -1
  30. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  31. package/dist/features/magic-context/dreamer/task-gates.d.ts +19 -1
  32. package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
  33. package/dist/features/magic-context/dreamer/task-registry.d.ts +28 -0
  34. package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
  35. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +7 -1
  36. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  37. package/dist/features/magic-context/dreamer/verify.d.ts +1 -0
  38. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  39. package/dist/features/magic-context/fail-closed-block.d.ts +10 -0
  40. package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
  41. package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts.map +1 -1
  42. package/dist/features/magic-context/git-commits/storage-git-commits.d.ts.map +1 -1
  43. package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
  44. package/dist/features/magic-context/memory/embedding-local.d.ts +7 -1
  45. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  46. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  47. package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -1
  48. package/dist/features/magic-context/memory/project-identity.d.ts +2 -6
  49. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  50. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
  51. package/dist/features/magic-context/migrations.d.ts +23 -1
  52. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  53. package/dist/features/magic-context/mural/compress-cues.d.ts +1 -0
  54. package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
  55. package/dist/features/magic-context/mural/render-mural.d.ts.map +1 -1
  56. package/dist/features/magic-context/overflow-detection.d.ts +8 -1
  57. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  58. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  59. package/dist/features/magic-context/scheduler.d.ts.map +1 -1
  60. package/dist/features/magic-context/schema-fence-probe.d.ts +28 -0
  61. package/dist/features/magic-context/schema-fence-probe.d.ts.map +1 -0
  62. package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
  63. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  64. package/dist/features/magic-context/storage-db.d.ts +37 -2
  65. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  66. package/dist/features/magic-context/storage-meta-persisted.d.ts +38 -4
  67. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  68. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  69. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  70. package/dist/features/magic-context/storage.d.ts +2 -2
  71. package/dist/features/magic-context/storage.d.ts.map +1 -1
  72. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/boundary-execution.d.ts +1 -1
  74. package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/cache-busting-signals.d.ts +1 -0
  76. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/child-session-spawn.d.ts +26 -0
  78. package/dist/hooks/magic-context/child-session-spawn.d.ts.map +1 -0
  79. package/dist/hooks/magic-context/command-handler.d.ts +3 -0
  80. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/compaction-off-transition.d.ts +96 -0
  82. package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -0
  83. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -0
  84. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  86. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
  87. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  88. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  89. package/dist/hooks/magic-context/compartment-trigger.d.ts +12 -5
  90. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +47 -1
  92. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/emergency-drop.d.ts +2 -2
  94. package/dist/hooks/magic-context/event-handler.d.ts +7 -0
  95. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/event-resolvers.d.ts +13 -21
  97. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/execute-status.d.ts +5 -1
  99. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  101. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -1
  103. package/dist/hooks/magic-context/hook-handlers.d.ts +1 -0
  104. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/hook.d.ts +14 -6
  106. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/inject-compartments.d.ts +10 -1
  108. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  109. package/dist/hooks/magic-context/live-session-state.d.ts +3 -0
  110. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  111. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  113. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/rust-mode-transform.d.ts +3 -0
  115. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  116. package/dist/hooks/magic-context/send-session-notification.d.ts +20 -1
  117. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  118. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -0
  119. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  120. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +31 -1
  121. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/transform.d.ts +29 -2
  123. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  124. package/dist/{index-56yjaeb2.js → index-2y0ybv5x.js} +11 -1
  125. package/dist/{index-ey1bc14z.js → index-b563sga5.js} +7 -264
  126. package/dist/{index-y1c9tcm3.js → index-cn960ffd.js} +101 -20
  127. package/dist/{index-xyaekybw.js → index-dtas91s5.js} +6 -4
  128. package/dist/index-fcyvznch.js +279 -0
  129. package/dist/{index-63dq2g4d.js → index-ry8rh8cv.js} +138 -24
  130. package/dist/{index-c2gahrks.js → index-x8spt5wy.js} +1682 -893
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +1677 -723
  133. package/dist/plugin/dream-timer.d.ts +4 -2
  134. package/dist/plugin/dream-timer.d.ts.map +1 -1
  135. package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
  136. package/dist/plugin/hooks/create-session-hooks.d.ts +12 -1
  137. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  138. package/dist/plugin/messages-transform.d.ts +10 -0
  139. package/dist/plugin/messages-transform.d.ts.map +1 -1
  140. package/dist/plugin/rpc-handlers.d.ts +3 -7
  141. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  142. package/dist/plugin/tool-registry.d.ts +8 -0
  143. package/dist/plugin/tool-registry.d.ts.map +1 -1
  144. package/dist/{recomp-orchestrator-2yb2hfh2.js → recomp-orchestrator-k72ejkkh.js} +6 -4
  145. package/dist/send-session-notification-6njqewzk.js +20 -0
  146. package/dist/shared/conflict-detector.d.ts +38 -1
  147. package/dist/shared/conflict-detector.d.ts.map +1 -1
  148. package/dist/shared/conflict-fixer.d.ts +18 -1
  149. package/dist/shared/conflict-fixer.d.ts.map +1 -1
  150. package/dist/shared/context-limit-provenance.d.ts +3 -0
  151. package/dist/shared/context-limit-provenance.d.ts.map +1 -0
  152. package/dist/shared/escalation-bands.d.ts +9 -0
  153. package/dist/shared/escalation-bands.d.ts.map +1 -0
  154. package/dist/shared/format-threshold.d.ts +1 -1
  155. package/dist/shared/models-dev-cache.d.ts +29 -3
  156. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  157. package/dist/shared/rpc-server.d.ts.map +1 -1
  158. package/dist/shared/rpc-types.d.ts +51 -1
  159. package/dist/shared/rpc-types.d.ts.map +1 -1
  160. package/dist/shared/rpc-utils.d.ts +19 -0
  161. package/dist/shared/rpc-utils.d.ts.map +1 -1
  162. package/dist/shared/storage-permissions.d.ts +5 -0
  163. package/dist/shared/storage-permissions.d.ts.map +1 -0
  164. package/dist/shared/tui-runtime-specifiers.d.ts +20 -0
  165. package/dist/shared/tui-runtime-specifiers.d.ts.map +1 -0
  166. package/dist/{storage-meta-session-td6bwtky.js → storage-meta-session-xt41zbw4.js} +2 -1
  167. package/dist/{task-config-5nx3qfz8.js → task-config-gbp7r23g.js} +1 -1
  168. package/dist/tui/compaction-off.d.ts +13 -0
  169. package/dist/tui/compaction-off.d.ts.map +1 -0
  170. package/dist/tui/data/context-db.d.ts.map +1 -1
  171. package/dist/tui-compiled/compaction-off.d.ts +13 -0
  172. package/dist/tui-compiled/compaction-off.d.ts.map +1 -0
  173. package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
  174. package/package.json +6 -1
  175. package/src/agents/dreamer.ts +68 -0
  176. package/src/agents/hidden-agent-registrations.ts +331 -0
  177. package/src/agents/historian.ts +3 -0
  178. package/src/agents/language-directive.test.ts +138 -0
  179. package/src/agents/language-directive.ts +121 -0
  180. package/src/agents/magic-context-prompt.language.test.ts +35 -0
  181. package/src/agents/magic-context-prompt.test.ts +292 -0
  182. package/src/agents/magic-context-prompt.ts +182 -0
  183. package/src/agents/permissions.test.ts +252 -0
  184. package/src/agents/permissions.ts +186 -0
  185. package/src/agents/sidekick.ts +1 -0
  186. package/src/agents/smart-note-compiler.ts +1 -0
  187. package/src/config/agent-disable.ts +106 -0
  188. package/src/config/compaction-accessor-guard.test.ts +82 -0
  189. package/src/config/compaction-backcompat.test.ts +52 -0
  190. package/src/config/index.test.ts +936 -0
  191. package/src/config/index.ts +601 -0
  192. package/src/config/migrate-config-location.test.ts +332 -0
  193. package/src/config/migrate-config-location.ts +635 -0
  194. package/src/config/migrate-dreamer-v2.test.ts +287 -0
  195. package/src/config/migrate-dreamer-v2.ts +336 -0
  196. package/src/config/migrate-experimental.test.ts +41 -0
  197. package/src/config/migrate-experimental.ts +185 -0
  198. package/src/config/project-security.test.ts +388 -0
  199. package/src/config/project-security.ts +611 -0
  200. package/src/config/prune-config-leaf.test.ts +78 -0
  201. package/src/config/prune-config-leaf.ts +68 -0
  202. package/src/config/removed-config-keys.test.ts +116 -0
  203. package/src/config/schema/agent-overrides.ts +45 -0
  204. package/src/config/schema/magic-context.test.ts +359 -0
  205. package/src/config/schema/magic-context.ts +991 -0
  206. package/src/config/transform-mode.test.ts +62 -0
  207. package/src/config/transform-mode.ts +34 -0
  208. package/src/config/variable.test.ts +323 -0
  209. package/src/config/variable.ts +192 -0
  210. package/src/features/magic-context/defaults.ts +1 -0
  211. package/src/features/magic-context/dreamer/cron.ts +243 -0
  212. package/src/features/magic-context/dreamer/task-registry.ts +151 -0
  213. package/src/shared/conflict-detector.test.ts +121 -0
  214. package/src/shared/conflict-detector.ts +50 -3
  215. package/src/shared/conflict-fixer.test.ts +309 -0
  216. package/src/shared/conflict-fixer.ts +82 -5
  217. package/src/shared/context-limit-provenance.ts +6 -0
  218. package/src/shared/escalation-bands.test.ts +23 -0
  219. package/src/shared/escalation-bands.ts +18 -0
  220. package/src/shared/format-threshold.ts +1 -1
  221. package/src/shared/models-dev-cache.test.ts +171 -0
  222. package/src/shared/models-dev-cache.ts +195 -38
  223. package/src/shared/rpc-server.ts +27 -18
  224. package/src/shared/rpc-types.ts +55 -1
  225. package/src/shared/rpc-utils.test.ts +157 -0
  226. package/src/shared/rpc-utils.ts +137 -0
  227. package/src/shared/storage-permissions.ts +20 -0
  228. package/src/shared/tui-runtime-specifiers.ts +32 -0
  229. package/src/tui/compaction-off.test.ts +56 -0
  230. package/src/tui/compaction-off.ts +29 -0
  231. package/src/tui/data/context-db.ts +6 -0
  232. package/src/tui/index.tsx +113 -65
  233. package/src/tui/slots/sidebar-content.tsx +189 -91
  234. package/src/tui/tui-compiled-runtime-imports.test.ts +151 -0
  235. package/src/tui-compiled/compaction-off.ts +29 -0
  236. package/src/tui-compiled/data/context-db.ts +6 -0
  237. package/src/tui-compiled/index.tsx +558 -455
  238. package/src/tui-compiled/slots/sidebar-content.tsx +334 -169
  239. package/dist/send-session-notification-tghq9gnh.js +0 -11
  240. /package/dist/{index-v7fmc1s2.js → index-ygcng9t8.js} +0 -0
@@ -5,6 +5,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
7
  import { parse as parseJsonc } from "comment-json";
8
+ import { detectConflicts } from "./conflict-detector";
8
9
  import { fixConflicts } from "./conflict-fixer";
9
10
 
10
11
  const noOmoConflicts = {
@@ -17,19 +18,24 @@ describe("fixConflicts", () => {
17
18
  let root: string;
18
19
  let projectDir: string;
19
20
  let userConfigDir: string;
21
+ let homeDir: string;
20
22
  let originalEnv: Record<string, string | undefined>;
21
23
 
22
24
  beforeEach(() => {
23
25
  root = mkdtempSync(join(tmpdir(), "mc-conflict-fixer-"));
24
26
  projectDir = join(root, "project");
25
27
  userConfigDir = join(root, "user-config", "opencode");
28
+ homeDir = join(root, "home");
26
29
  mkdirSync(projectDir, { recursive: true });
27
30
  mkdirSync(userConfigDir, { recursive: true });
31
+ mkdirSync(homeDir, { recursive: true });
28
32
  originalEnv = {
29
33
  OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
30
34
  XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
35
+ HOME: process.env.HOME,
31
36
  };
32
37
  process.env.OPENCODE_CONFIG_DIR = userConfigDir;
38
+ process.env.HOME = homeDir;
33
39
  delete process.env.XDG_CONFIG_HOME;
34
40
  });
35
41
 
@@ -125,4 +131,307 @@ describe("fixConflicts", () => {
125
131
  ["@other/opencode-dcp-slim@latest", { enabled: true }],
126
132
  ]);
127
133
  });
134
+
135
+ // --- Unified OMO config (oh-my-openagent >= 4.19.0) ---
136
+
137
+ describe("unified OMO config paths", () => {
138
+ const omoConflicts = {
139
+ compactionAuto: false,
140
+ compactionPrune: false,
141
+ dcpPlugin: false,
142
+ omoPreemptiveCompaction: true,
143
+ omoContextWindowMonitor: true,
144
+ omoAnthropicRecovery: true,
145
+ };
146
+
147
+ it("disables hooks inside [opencode] block in ~/.omo/omo.jsonc, comments survive, detector confirms", () => {
148
+ const omoDir = join(homeDir, ".omo");
149
+ mkdirSync(omoDir, { recursive: true });
150
+ const configPath = join(omoDir, "omo.jsonc");
151
+ writeFileSync(
152
+ configPath,
153
+ `{
154
+ // top-level comment
155
+ "some-omo-setting": true,
156
+ "[opencode]": {
157
+ // opencode block comment
158
+ "other_setting": "value"
159
+ }
160
+ }
161
+ `,
162
+ );
163
+
164
+ const actions = fixConflicts(projectDir, omoConflicts);
165
+
166
+ expect(actions).toEqual(["Disabled conflicting oh-my-opencode hooks"]);
167
+
168
+ const updatedText = readFileSync(configPath, "utf-8");
169
+ expect(updatedText).toContain("top-level comment");
170
+ expect(updatedText).toContain("opencode block comment");
171
+ expect(updatedText).toContain("some-omo-setting");
172
+
173
+ const updated = parseJsonc(updatedText) as Record<string, unknown>;
174
+ const opencodeBlock = updated["[opencode]"] as Record<string, unknown>;
175
+ expect(opencodeBlock.disabled_hooks).toEqual([
176
+ "context-window-monitor",
177
+ "preemptive-compaction",
178
+ "anthropic-context-window-limit-recovery",
179
+ ]);
180
+ expect(opencodeBlock.other_setting).toBe("value");
181
+
182
+ // Round-trip: detector should now report no OMO conflicts
183
+ // Need a project-level opencode.json with OMO plugin for detection
184
+ writeFileSync(
185
+ join(projectDir, "opencode.json"),
186
+ JSON.stringify({ plugin: ["oh-my-opencode"] }),
187
+ );
188
+ const result = detectConflicts(projectDir);
189
+ expect(result.conflicts.omoPreemptiveCompaction).toBe(false);
190
+ expect(result.conflicts.omoContextWindowMonitor).toBe(false);
191
+ expect(result.conflicts.omoAnthropicRecovery).toBe(false);
192
+ });
193
+
194
+ it("creates [opencode] block when missing in unified omo.jsonc", () => {
195
+ const omoDir = join(homeDir, ".omo");
196
+ mkdirSync(omoDir, { recursive: true });
197
+ const configPath = join(omoDir, "omo.jsonc");
198
+ writeFileSync(
199
+ configPath,
200
+ `{
201
+ // just a top-level setting, no [opencode] block
202
+ "some-omo-setting": true
203
+ }
204
+ `,
205
+ );
206
+
207
+ const actions = fixConflicts(projectDir, omoConflicts);
208
+
209
+ expect(actions).toEqual(["Disabled conflicting oh-my-opencode hooks"]);
210
+
211
+ const updatedText = readFileSync(configPath, "utf-8");
212
+ expect(updatedText).toContain("top-level setting");
213
+
214
+ const updated = parseJsonc(updatedText) as Record<string, unknown>;
215
+ const opencodeBlock = updated["[opencode]"] as Record<string, unknown>;
216
+ expect(opencodeBlock.disabled_hooks).toEqual([
217
+ "context-window-monitor",
218
+ "preemptive-compaction",
219
+ "anthropic-context-window-limit-recovery",
220
+ ]);
221
+ });
222
+
223
+ it("writes to project-level .omo/omo.jsonc", () => {
224
+ const omoDir = join(projectDir, ".omo");
225
+ mkdirSync(omoDir, { recursive: true });
226
+ const configPath = join(omoDir, "omo.jsonc");
227
+ writeFileSync(
228
+ configPath,
229
+ JSON.stringify({
230
+ "[opencode]": {},
231
+ }),
232
+ );
233
+
234
+ const actions = fixConflicts(projectDir, omoConflicts);
235
+
236
+ expect(actions).toEqual(["Disabled conflicting oh-my-opencode hooks"]);
237
+
238
+ const updated = parseJsonc(readFileSync(configPath, "utf-8")) as Record<
239
+ string,
240
+ unknown
241
+ >;
242
+ const opencodeBlock = updated["[opencode]"] as Record<string, unknown>;
243
+ expect(opencodeBlock.disabled_hooks).toEqual([
244
+ "context-window-monitor",
245
+ "preemptive-compaction",
246
+ "anthropic-context-window-limit-recovery",
247
+ ]);
248
+ });
249
+
250
+ it("reads omo.json (fallback) when omo.jsonc does not exist", () => {
251
+ const omoDir = join(homeDir, ".omo");
252
+ mkdirSync(omoDir, { recursive: true });
253
+ const configPath = join(omoDir, "omo.json");
254
+ writeFileSync(
255
+ configPath,
256
+ JSON.stringify({
257
+ "[opencode]": {},
258
+ }),
259
+ );
260
+
261
+ const actions = fixConflicts(projectDir, omoConflicts);
262
+
263
+ expect(actions).toEqual(["Disabled conflicting oh-my-opencode hooks"]);
264
+
265
+ const updated = JSON.parse(readFileSync(configPath, "utf-8"));
266
+ expect(updated["[opencode]"].disabled_hooks).toEqual([
267
+ "context-window-monitor",
268
+ "preemptive-compaction",
269
+ "anthropic-context-window-limit-recovery",
270
+ ]);
271
+ });
272
+
273
+ it("updates both legacy and unified config when both exist", () => {
274
+ // Legacy: project-level oh-my-opencode.json
275
+ const legacyPath = join(projectDir, "oh-my-opencode.json");
276
+ writeFileSync(legacyPath, JSON.stringify({ disabled_hooks: [] }));
277
+
278
+ // Unified: ~/.omo/omo.jsonc
279
+ const omoDir = join(homeDir, ".omo");
280
+ mkdirSync(omoDir, { recursive: true });
281
+ const unifiedPath = join(omoDir, "omo.jsonc");
282
+ writeFileSync(
283
+ unifiedPath,
284
+ JSON.stringify({
285
+ "[opencode]": {},
286
+ }),
287
+ );
288
+
289
+ const actions = fixConflicts(projectDir, omoConflicts);
290
+
291
+ expect(actions).toEqual(["Disabled conflicting oh-my-opencode hooks"]);
292
+
293
+ // Legacy: top-level disabled_hooks
294
+ const legacy = JSON.parse(readFileSync(legacyPath, "utf-8"));
295
+ expect(legacy.disabled_hooks).toEqual([
296
+ "context-window-monitor",
297
+ "preemptive-compaction",
298
+ "anthropic-context-window-limit-recovery",
299
+ ]);
300
+
301
+ // Unified: inside [opencode] block
302
+ const unified = JSON.parse(readFileSync(unifiedPath, "utf-8"));
303
+ expect(unified["[opencode]"].disabled_hooks).toEqual([
304
+ "context-window-monitor",
305
+ "preemptive-compaction",
306
+ "anthropic-context-window-limit-recovery",
307
+ ]);
308
+ });
309
+
310
+ it("skips non-existent unified paths (no create)", () => {
311
+ // No .omo directory at all — fixer should find no targets
312
+ const actions = fixConflicts(projectDir, omoConflicts);
313
+ expect(actions).toEqual([]);
314
+ });
315
+ });
316
+
317
+ // --- Compaction-off mode parity (issue #266 S2) ---
318
+ // In compaction-off mode the fixer MUST NOT flip compaction.auto/prune to
319
+ // false — native compaction fields are left byte-for-byte as found.
320
+ // DCP and OMO hook fixes keep their existing policy in BOTH modes.
321
+ describe("compaction-off mode parity (issue #266)", () => {
322
+ it("does NOT flip compaction.auto to false when compaction-off", () => {
323
+ const configPath = join(projectDir, "opencode.jsonc");
324
+ const original = `{
325
+ // keep comment
326
+ "compaction": {
327
+ "auto": true,
328
+ "prune": true
329
+ }
330
+ }
331
+ `;
332
+ writeFileSync(configPath, original);
333
+
334
+ const actions = fixConflicts(
335
+ projectDir,
336
+ {
337
+ compactionAuto: true,
338
+ compactionPrune: true,
339
+ dcpPlugin: false,
340
+ ...noOmoConflicts,
341
+ },
342
+ { compactionEnabled: false },
343
+ );
344
+
345
+ // No compaction repair action in compaction-off mode.
346
+ expect(actions).toEqual([]);
347
+ // File is byte-for-byte unchanged (native fields preserved).
348
+ expect(readFileSync(configPath, "utf-8")).toBe(original);
349
+ });
350
+
351
+ it("still removes DCP plugin in compaction-off mode (DCP policy is mode-independent)", () => {
352
+ const configPath = join(projectDir, "opencode.jsonc");
353
+ writeFileSync(
354
+ configPath,
355
+ JSON.stringify({
356
+ plugin: ["@tarquinen/opencode-dcp@latest"],
357
+ compaction: { auto: true, prune: true },
358
+ }),
359
+ );
360
+
361
+ const actions = fixConflicts(
362
+ projectDir,
363
+ { compactionAuto: true, compactionPrune: true, dcpPlugin: true, ...noOmoConflicts },
364
+ { compactionEnabled: false },
365
+ );
366
+
367
+ // DCP removed, compaction NOT flipped.
368
+ expect(actions).toEqual(["Removed opencode-dcp plugin"]);
369
+ const updated = parseJsonc(readFileSync(configPath, "utf-8")) as Record<
370
+ string,
371
+ unknown
372
+ >;
373
+ expect(updated.compaction).toEqual({ auto: true, prune: true });
374
+ expect(updated.plugin).toEqual([]);
375
+ });
376
+
377
+ it("still disables OMO hooks in compaction-off mode (OMO policy is mode-independent)", () => {
378
+ const configPath = join(projectDir, "opencode.json");
379
+ writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-opencode"] }));
380
+ const omoDir = join(homeDir, ".omo");
381
+ mkdirSync(omoDir, { recursive: true });
382
+ const omoPath = join(omoDir, "omo.jsonc");
383
+ writeFileSync(omoPath, JSON.stringify({ "[opencode]": {} }));
384
+
385
+ const actions = fixConflicts(
386
+ projectDir,
387
+ {
388
+ compactionAuto: false,
389
+ compactionPrune: false,
390
+ dcpPlugin: false,
391
+ omoPreemptiveCompaction: true,
392
+ omoContextWindowMonitor: true,
393
+ omoAnthropicRecovery: true,
394
+ },
395
+ { compactionEnabled: false },
396
+ );
397
+
398
+ expect(actions).toEqual(["Disabled conflicting oh-my-opencode hooks"]);
399
+ });
400
+
401
+ // Mutation direction: with mode ON, the same compaction conflict IS
402
+ // repaired. Proves the off-gate isn't just always-skip.
403
+ it("mutation direction: same conflict IS repaired when mode forced on", () => {
404
+ const configPath = join(projectDir, "opencode.jsonc");
405
+ writeFileSync(configPath, JSON.stringify({ compaction: { auto: true, prune: true } }));
406
+
407
+ const offActions = fixConflicts(
408
+ projectDir,
409
+ {
410
+ compactionAuto: true,
411
+ compactionPrune: true,
412
+ dcpPlugin: false,
413
+ ...noOmoConflicts,
414
+ },
415
+ { compactionEnabled: false },
416
+ );
417
+ const onActions = fixConflicts(
418
+ projectDir,
419
+ {
420
+ compactionAuto: true,
421
+ compactionPrune: true,
422
+ dcpPlugin: false,
423
+ ...noOmoConflicts,
424
+ },
425
+ { compactionEnabled: true },
426
+ );
427
+
428
+ expect(offActions).toEqual([]);
429
+ expect(onActions).toEqual(["Disabled auto-compaction"]);
430
+ const updated = parseJsonc(readFileSync(configPath, "utf-8")) as Record<
431
+ string,
432
+ unknown
433
+ >;
434
+ expect(updated.compaction).toEqual({ auto: false, prune: false });
435
+ });
436
+ });
128
437
  });
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import { join } from "node:path";
3
4
  import { parse, stringify } from "comment-json";
4
5
 
@@ -25,6 +26,9 @@ const OMO_CONFIG_NAMES = [
25
26
  "oh-my-opencode.json",
26
27
  ] as const;
27
28
 
29
+ /** Unified OMO config names (oh-my-openagent >= 4.19.0) */
30
+ const OMO_UNIFIED_NAMES = ["omo.jsonc", "omo.json"] as const;
31
+
28
32
  function isRecord(value: unknown): value is JsonObject {
29
33
  return typeof value === "object" && value !== null && !Array.isArray(value);
30
34
  }
@@ -84,6 +88,7 @@ function collectOmoConfigPaths(directory: string): string[] {
84
88
  const paths = new Set<string>();
85
89
  const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
86
90
 
91
+ // Legacy OMO config names in the OpenCode config dir and project dir
87
92
  for (const fileName of OMO_CONFIG_NAMES) {
88
93
  const userPath = join(configDir, fileName);
89
94
  const projectPath = join(directory, fileName);
@@ -97,9 +102,48 @@ function collectOmoConfigPaths(directory: string): string[] {
97
102
  }
98
103
  }
99
104
 
105
+ // New unified omo.jsonc (oh-my-openagent >= 4.19.0)
106
+ // User-level: ~/.omo/omo.jsonc (fallback ~/.omo/omo.json)
107
+ const homeDir = process.env.HOME || homedir();
108
+ const omoHomeDir = join(homeDir, ".omo");
109
+ for (const name of OMO_UNIFIED_NAMES) {
110
+ const userPath = join(omoHomeDir, name);
111
+ if (existsSync(userPath)) {
112
+ paths.add(userPath);
113
+ }
114
+ }
115
+
116
+ // Project-level: .omo/omo.jsonc (fallback .omo/omo.json)
117
+ for (const name of OMO_UNIFIED_NAMES) {
118
+ const projectPath = join(directory, ".omo", name);
119
+ if (existsSync(projectPath)) {
120
+ paths.add(projectPath);
121
+ }
122
+ }
123
+
100
124
  return [...paths];
101
125
  }
102
126
 
127
+ /** Check if a config path is a unified omo.json(c) path (not legacy). */
128
+ function isUnifiedOmoPath(configPath: string): boolean {
129
+ const basename = configPath.split("/").pop() ?? "";
130
+ return basename === "omo.jsonc" || basename === "omo.json";
131
+ }
132
+
133
+ /**
134
+ * Get or create the `[opencode]` block inside a unified omo.json(c) config.
135
+ * Returns the block itself so callers can read/write `disabled_hooks` on it.
136
+ */
137
+ function getOrCreateOmoV2Block(config: JsonObject): JsonObject {
138
+ const block = config["[opencode]"];
139
+ if (isRecord(block)) {
140
+ return block;
141
+ }
142
+ const newBlock: JsonObject = {};
143
+ config["[opencode]"] = newBlock;
144
+ return newBlock;
145
+ }
146
+
103
147
  function filterDcpPluginEntries(entries: unknown[]): { plugins: unknown[]; removed: boolean } {
104
148
  const plugins = entries.filter((entry) => {
105
149
  const name = extractPluginName(entry);
@@ -108,13 +152,43 @@ function filterDcpPluginEntries(entries: unknown[]): { plugins: unknown[]; remov
108
152
  return { plugins, removed: plugins.length !== entries.length };
109
153
  }
110
154
 
111
- export function fixConflicts(directory: string, conflicts: ConflictResult["conflicts"]): string[] {
155
+ /**
156
+ * Options for {@link fixConflicts}.
157
+ *
158
+ * `compactionEnabled` is the boot-resolved MC compaction mode (the result of
159
+ * {@link isCompactionEnabled} on the resolved user-tier config). When `false`
160
+ * (compaction-off mode), the fixer MUST NOT flip `compaction.auto`/`prune` to
161
+ * `false` — native compaction fields are left byte-for-byte as found, because
162
+ * native compaction (or nothing) is the user's chosen window manager. DCP and
163
+ * OMO hook fixes keep their existing policy in BOTH modes.
164
+ *
165
+ * Default `true` (mode-on) preserves today's fix behavior for call sites that
166
+ * cannot supply the resolved mode; they fail toward mode-on, never silently
167
+ * skipping the fix.
168
+ */
169
+ export interface FixConflictsOptions {
170
+ compactionEnabled?: boolean;
171
+ }
172
+
173
+ export function fixConflicts(
174
+ directory: string,
175
+ conflicts: ConflictResult["conflicts"],
176
+ options?: FixConflictsOptions,
177
+ ): string[] {
178
+ const compactionEnabled = options?.compactionEnabled ?? true;
112
179
  const actions: string[] = [];
113
180
  let updatedCompaction = false;
114
181
  let removedDcpPlugin = false;
115
182
  let disabledOmoHooks = false;
116
183
 
117
- if (conflicts.compactionAuto || conflicts.compactionPrune || conflicts.dcpPlugin) {
184
+ // Native compaction fields are repaired ONLY when MC compaction is ON. In
185
+ // compaction-off mode the fixer must not rewrite compaction.auto/prune —
186
+ // native compaction is the intended manager and pre-existing values are
187
+ // left byte-for-byte as found.
188
+ const repairCompaction =
189
+ compactionEnabled && (conflicts.compactionAuto || conflicts.compactionPrune);
190
+
191
+ if (repairCompaction || conflicts.dcpPlugin) {
118
192
  for (const configPath of collectOpenCodeConfigPaths(directory)) {
119
193
  const config = readConfig(configPath);
120
194
  if (!config) {
@@ -123,7 +197,7 @@ export function fixConflicts(directory: string, conflicts: ConflictResult["confl
123
197
 
124
198
  let changed = false;
125
199
 
126
- if (conflicts.compactionAuto || conflicts.compactionPrune) {
200
+ if (repairCompaction) {
127
201
  const compaction = isRecord(config.compaction) ? config.compaction : {};
128
202
 
129
203
  if (compaction.auto !== false) {
@@ -180,7 +254,10 @@ export function fixConflicts(directory: string, conflicts: ConflictResult["confl
180
254
  continue;
181
255
  }
182
256
 
183
- const disabledHooks = new Set(asStringArray(config.disabled_hooks));
257
+ // Unified paths nest hooks under the [opencode] block; legacy paths use top-level
258
+ const target = isUnifiedOmoPath(configPath) ? getOrCreateOmoV2Block(config) : config;
259
+
260
+ const disabledHooks = new Set(asStringArray(target.disabled_hooks));
184
261
  let changed = false;
185
262
 
186
263
  for (const hook of hooksToDisable) {
@@ -192,7 +269,7 @@ export function fixConflicts(directory: string, conflicts: ConflictResult["confl
192
269
  }
193
270
 
194
271
  if (changed) {
195
- config.disabled_hooks = [
272
+ target.disabled_hooks = [
196
273
  ...CONFLICTING_OMO_HOOKS.filter((hook) => disabledHooks.has(hook)),
197
274
  ...[...disabledHooks].filter(
198
275
  (hook) =>
@@ -0,0 +1,6 @@
1
+ export type ContextLimitProvenance = "prompt_only" | "combined" | "unknown";
2
+
3
+ export function normalizeContextLimitProvenance(value: unknown): ContextLimitProvenance {
4
+ if (value === "prompt_only" || value === "combined") return value;
5
+ return "unknown";
6
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { escalationBands } from "./escalation-bands";
3
+
4
+ describe("escalationBands", () => {
5
+ test.each([
6
+ [65, 85],
7
+ [80, 85],
8
+ [88, 90],
9
+ [90, 92],
10
+ ])("keeps the force band ordered for threshold %i", (threshold, expectedForce) => {
11
+ const bands = escalationBands(threshold);
12
+ expect(bands.forceMaterializationPercentage).toBe(expectedForce);
13
+ expect(threshold).toBeLessThan(bands.forceMaterializationPercentage);
14
+ expect(bands.forceMaterializationPercentage).toBeGreaterThanOrEqual(85);
15
+ expect(bands.forceMaterializationPercentage).toBeLessThan(95);
16
+ expect(bands.emergencyPercentage).toBe(95);
17
+ });
18
+
19
+ test("preserves the exact pre-raise behavior through threshold 80", () => {
20
+ expect(escalationBands(65).forceMaterializationPercentage).toBe(85);
21
+ expect(escalationBands(80).forceMaterializationPercentage).toBe(85);
22
+ });
23
+ });
@@ -0,0 +1,18 @@
1
+ export const MAX_EXECUTE_THRESHOLD = 90;
2
+ export const ABSOLUTE_EMERGENCY_PERCENTAGE = 95;
3
+
4
+ export interface EscalationBands {
5
+ forceMaterializationPercentage: number;
6
+ emergencyPercentage: number;
7
+ }
8
+
9
+ /** Keep force cleanup above normal execution while preserving the absolute 95% wall. */
10
+ export function escalationBands(effectiveThresholdPercentage: number): EscalationBands {
11
+ const threshold = Number.isFinite(effectiveThresholdPercentage)
12
+ ? Math.min(effectiveThresholdPercentage, MAX_EXECUTE_THRESHOLD)
13
+ : 65;
14
+ return {
15
+ forceMaterializationPercentage: Math.max(85, threshold + 2),
16
+ emergencyPercentage: ABSOLUTE_EMERGENCY_PERCENTAGE,
17
+ };
18
+ }
@@ -31,7 +31,7 @@ export function formatThresholdPercent(value: number | undefined | null): string
31
31
  * Build a terse "clamped" annotation for execute-threshold display, or "" when the
32
32
  * configured value was not clamped. Status surfaces (/ctx-status text, pi status
33
33
  * dialog) append this so a user who configured e.g. 190000 tokens on a 128K model
34
- * sees that it was reduced to the 80% safety cap, instead of the value being
34
+ * sees that it was reduced to the 90% safety cap, instead of the value being
35
35
  * silently ignored (issue #241). The note includes the configured value and the cap
36
36
  * so the user can see the math.
37
37
  *