@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
@@ -1,3 +1,4 @@
1
+ import { homedir } from "node:os";
1
2
  import { join } from "node:path";
2
3
  import { readJsoncFile } from "./jsonc-parser";
3
4
  import { getOpenCodeConfigPaths } from "./opencode-config-dir";
@@ -15,6 +16,14 @@ interface OmoConfig {
15
16
  disabled_hooks?: string[];
16
17
  }
17
18
 
19
+ /** Shape of the new unified omo.jsonc (oh-my-openagent >= 4.19.0).
20
+ * Hook config lives inside the `[opencode]` harness block. */
21
+ interface OmoV2Config {
22
+ "[opencode]"?: {
23
+ disabled_hooks?: string[];
24
+ };
25
+ }
26
+
18
27
  export interface ConflictResult {
19
28
  /** Whether any blocking conflict was found */
20
29
  hasConflict: boolean;
@@ -29,13 +38,55 @@ export interface ConflictResult {
29
38
  omoContextWindowMonitor: boolean;
30
39
  omoAnthropicRecovery: boolean;
31
40
  };
41
+ /**
42
+ * Resolved native compaction state observed during detection, for honest
43
+ * reporting in both MC modes. `auto`/`prune` reflect the OpenCode
44
+ * `compaction` block as resolved by the detector (env override, project
45
+ * then user, default-on). They are populated even when MC compaction is
46
+ * OFF (in which case they are NOT flagged as conflicts).
47
+ */
48
+ nativeCompaction: {
49
+ auto: boolean;
50
+ prune: boolean;
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Options for {@link detectConflicts}.
56
+ *
57
+ * `compactionEnabled` is the boot-resolved Magic Context compaction mode
58
+ * (the result of {@link isCompactionEnabled} on the resolved user-tier
59
+ * config). It MUST be threaded through every production call site — plugin
60
+ * boot, setup, doctor, conflict-fixer — so the MC-mode decision is never
61
+ * re-derived at a call site. A call site that genuinely cannot supply it
62
+ * (e.g. a low-level native-config reader with no MC config handle) MUST
63
+ * omit it and accept the default `true` (mode-on) behavior, which preserves
64
+ * today's conflict semantics; it must never silently skip the check.
65
+ *
66
+ * When `compactionEnabled` is `false` (compaction-off mode), OpenCode
67
+ * `compaction.auto=true` / `compaction.prune=true` are NOT plugin-disabling
68
+ * conflicts — native compaction is the user's chosen window manager. DCP
69
+ * and the three OMO conflict classes keep their existing policy in BOTH
70
+ * modes.
71
+ */
72
+ export interface DetectConflictsOptions {
73
+ compactionEnabled?: boolean;
32
74
  }
33
75
 
34
76
  /**
35
77
  * Detect all conflicts that would prevent magic-context from working correctly.
36
78
  * Checks: OpenCode compaction, DCP plugin, OMO conflicting hooks.
79
+ *
80
+ * `compactionEnabled` (default `true`) is the resolved MC compaction mode.
81
+ * When `false` (compaction-off mode), native `compaction.auto`/`prune` are
82
+ * reported in {@link ConflictResult.nativeCompaction} but are NOT flagged as
83
+ * conflicts — native compaction is the intended window manager in that mode.
37
84
  */
38
- export function detectConflicts(directory: string): ConflictResult {
85
+ export function detectConflicts(
86
+ directory: string,
87
+ options?: DetectConflictsOptions,
88
+ ): ConflictResult {
89
+ const compactionEnabled = options?.compactionEnabled ?? true;
39
90
  const conflicts: ConflictResult["conflicts"] = {
40
91
  compactionAuto: false,
41
92
  compactionPrune: false,
@@ -48,11 +99,15 @@ export function detectConflicts(directory: string): ConflictResult {
48
99
 
49
100
  // --- Check OpenCode compaction config ---
50
101
  const compactionResult = checkCompaction(directory);
51
- if (compactionResult.auto) {
102
+ // Native compaction is a conflict ONLY when MC compaction is ON. In
103
+ // compaction-off mode the user has explicitly handed the window to native
104
+ // compaction (or nothing), so compaction.auto=true / prune=true are the
105
+ // intended state, not a plugin-disabling conflict.
106
+ if (compactionEnabled && compactionResult.auto) {
52
107
  conflicts.compactionAuto = true;
53
108
  reasons.push("OpenCode auto-compaction is enabled (compaction.auto=true)");
54
109
  }
55
- if (compactionResult.prune) {
110
+ if (compactionEnabled && compactionResult.prune) {
56
111
  conflicts.compactionPrune = true;
57
112
  reasons.push("OpenCode prune is enabled (compaction.prune=true)");
58
113
  }
@@ -91,6 +146,7 @@ export function detectConflicts(directory: string): ConflictResult {
91
146
  hasConflict: reasons.length > 0,
92
147
  reasons,
93
148
  conflicts,
149
+ nativeCompaction: { auto: compactionResult.auto, prune: compactionResult.prune },
94
150
  };
95
151
  }
96
152
 
@@ -318,7 +374,7 @@ function checkOmoHooks(directory: string): {
318
374
  function readOmoDisabledHooks(directory: string): Set<string> {
319
375
  const disabled = new Set<string>();
320
376
 
321
- // Check both old and new OMO config names
377
+ // Check both old and new OMO config names in the OpenCode config dir
322
378
  const configNames = [
323
379
  "oh-my-opencode.jsonc",
324
380
  "oh-my-opencode.json",
@@ -341,7 +397,7 @@ function readOmoDisabledHooks(directory: string): Set<string> {
341
397
  // best-effort
342
398
  }
343
399
 
344
- // Also check project-level OMO configs
400
+ // Also check project-level OMO configs (old format)
345
401
  for (const name of configNames) {
346
402
  const config = readJsoncFile<OmoConfig>(join(directory, name));
347
403
  if (config?.disabled_hooks) {
@@ -351,6 +407,29 @@ function readOmoDisabledHooks(directory: string): Set<string> {
351
407
  }
352
408
  }
353
409
 
410
+ // --- New unified omo.jsonc (oh-my-openagent >= 4.19.0) ---
411
+ // User-level: ~/.omo/omo.jsonc (fallback ~/.omo/omo.json)
412
+ const homeDir = process.env.HOME || homedir();
413
+ const omoHomeDir = join(homeDir, ".omo");
414
+ for (const name of ["omo.jsonc", "omo.json"]) {
415
+ const config = readJsoncFile<OmoV2Config>(join(omoHomeDir, name));
416
+ if (config?.["[opencode]"]?.disabled_hooks) {
417
+ for (const hook of config["[opencode]"].disabled_hooks) {
418
+ disabled.add(hook);
419
+ }
420
+ }
421
+ }
422
+
423
+ // Project-level: .omo/omo.jsonc (fallback .omo/omo.json)
424
+ for (const name of ["omo.jsonc", "omo.json"]) {
425
+ const config = readJsoncFile<OmoV2Config>(join(directory, ".omo", name));
426
+ if (config?.["[opencode]"]?.disabled_hooks) {
427
+ for (const hook of config["[opencode]"].disabled_hooks) {
428
+ disabled.add(hook);
429
+ }
430
+ }
431
+ }
432
+
354
433
  return disabled;
355
434
  }
356
435
 
@@ -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
+ }