@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
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Minimal 5-field cron evaluator for per-task dreamer scheduling.
3
+ *
4
+ * Why vendored (no dependency): we only need "next occurrence after time T", the
5
+ * 3-day package-release-age floor makes pulling a dep awkward, and the surface is
6
+ * small. Shared core — both OpenCode and Pi import this via @magic-context/core.
7
+ *
8
+ * Fields: `minute hour day-of-month month day-of-week`
9
+ * minute 0-59 · hour 0-23 · dom 1-31 · month 1-12 · dow 0-6 (0 or 7 = Sunday)
10
+ * Each field supports: a star wildcard, a star-with-step (every-N), a single
11
+ * value `a`, a range `a-b`, a range-with-step `a-b/step`, an open step `a/step`
12
+ * (a..max), and comma-lists of any of those. Numeric only — month and weekday
13
+ * NAMES are intentionally unsupported (smaller surface; add later if asked).
14
+ * Empty string `""` means "never" and is handled by the caller, not here.
15
+ *
16
+ * Day matching uses Vixie OR-semantics: when BOTH dom and dow are restricted
17
+ * (not `*`), a day matches if EITHER matches; when only one is restricted, only
18
+ * that one is consulted; when neither is restricted, every day matches.
19
+ *
20
+ * Timezone: all matching is in the machine's LOCAL time — the dreamer's whole
21
+ * purpose is "run while the user is asleep", which is a wall-clock concept.
22
+ * nextOccurrence steps by real (epoch) minutes and reads LOCAL civil fields off
23
+ * each candidate, so DST transitions are handled correctly by construction (no
24
+ * setHours/local-constructor normalization, which is DST-ambiguous).
25
+ */
26
+
27
+ export interface ParsedCron {
28
+ minute: Set<number>;
29
+ hour: Set<number>;
30
+ dom: Set<number>;
31
+ month: Set<number>;
32
+ dow: Set<number>;
33
+ /** Original field token was not `*` — drives Vixie dom/dow OR-semantics. */
34
+ domRestricted: boolean;
35
+ dowRestricted: boolean;
36
+ }
37
+
38
+ export type ParseCronResult = { ok: true; cron: ParsedCron } | { ok: false; error: string };
39
+
40
+ interface FieldSpec {
41
+ name: string;
42
+ min: number;
43
+ max: number;
44
+ }
45
+
46
+ const FIELDS: FieldSpec[] = [
47
+ { name: "minute", min: 0, max: 59 },
48
+ { name: "hour", min: 0, max: 23 },
49
+ { name: "day-of-month", min: 1, max: 31 },
50
+ { name: "month", min: 1, max: 12 },
51
+ { name: "day-of-week", min: 0, max: 7 }, // 7 normalized to 0 (Sunday)
52
+ ];
53
+
54
+ const MINUTE_MS = 60_000;
55
+ /**
56
+ * Forward search bound for nextOccurrence. ~4 years covers Feb-29-only crons
57
+ * (leap years are ≤4 apart) and the longest legitimate gaps; anything past it is
58
+ * treated as "never" (e.g. an impossible `0 0 31 2 *` — Feb 31). Generous so a
59
+ * valid-but-rare schedule is never wrongly reported as never.
60
+ */
61
+ const MAX_SEARCH_MS = 4 * 366 * 24 * 60 * MINUTE_MS;
62
+
63
+ /** Parse one field into the set of matching values, or null on error. */
64
+ function parseField(token: string, spec: FieldSpec): Set<number> | null {
65
+ const values = new Set<number>();
66
+ const normalize = (n: number): number => (spec.name === "day-of-week" && n === 7 ? 0 : n);
67
+
68
+ for (const part of token.split(",")) {
69
+ const piece = part.trim();
70
+ if (piece.length === 0) return null;
71
+
72
+ // Split off an optional /step.
73
+ const [rangePart, stepPart, ...extra] = piece.split("/");
74
+ if (extra.length > 0) return null;
75
+ let step = 1;
76
+ if (stepPart !== undefined) {
77
+ if (!/^\d+$/.test(stepPart)) return null;
78
+ step = Number(stepPart);
79
+ if (step < 1) return null;
80
+ }
81
+
82
+ let lo: number;
83
+ let hi: number;
84
+ if (rangePart === "*") {
85
+ lo = spec.min;
86
+ hi = spec.max;
87
+ } else if (rangePart.includes("-")) {
88
+ const [loStr, hiStr, ...rest] = rangePart.split("-");
89
+ if (rest.length > 0) return null;
90
+ if (!/^\d+$/.test(loStr) || !/^\d+$/.test(hiStr)) return null;
91
+ lo = Number(loStr);
92
+ hi = Number(hiStr);
93
+ } else {
94
+ if (!/^\d+$/.test(rangePart)) return null;
95
+ lo = Number(rangePart);
96
+ // `a/step` (no upper bound) means a..max by step.
97
+ hi = stepPart !== undefined ? spec.max : lo;
98
+ }
99
+
100
+ // Validate against the field range BEFORE dow-7 normalization so `7` is
101
+ // accepted (it's a valid Sunday alias) but `8` is rejected.
102
+ if (lo < spec.min || lo > spec.max || hi < spec.min || hi > spec.max) return null;
103
+ if (lo > hi) return null;
104
+
105
+ for (let v = lo; v <= hi; v += step) {
106
+ values.add(normalize(v));
107
+ }
108
+ }
109
+
110
+ return values.size > 0 ? values : null;
111
+ }
112
+
113
+ /** Parse a 5-field cron expression. Empty/whitespace is rejected here — callers
114
+ * treat `""` as "never" before calling. */
115
+ export function parseCron(expression: string): ParseCronResult {
116
+ const trimmed = expression.trim();
117
+ if (trimmed.length === 0) {
118
+ return { ok: false, error: "empty cron expression" };
119
+ }
120
+ const tokens = trimmed.split(/\s+/);
121
+ if (tokens.length !== 5) {
122
+ return {
123
+ ok: false,
124
+ error: `expected 5 fields (minute hour day-of-month month day-of-week), got ${tokens.length}`,
125
+ };
126
+ }
127
+
128
+ const sets: Set<number>[] = [];
129
+ for (let i = 0; i < FIELDS.length; i++) {
130
+ const parsed = parseField(tokens[i], FIELDS[i]);
131
+ if (!parsed) {
132
+ return {
133
+ ok: false,
134
+ error: `invalid ${FIELDS[i].name} field "${tokens[i]}" (allowed ${FIELDS[i].min}-${FIELDS[i].max})`,
135
+ };
136
+ }
137
+ sets.push(parsed);
138
+ }
139
+
140
+ return {
141
+ ok: true,
142
+ cron: {
143
+ minute: sets[0],
144
+ hour: sets[1],
145
+ dom: sets[2],
146
+ month: sets[3],
147
+ dow: sets[4],
148
+ domRestricted: tokens[2] !== "*",
149
+ dowRestricted: tokens[4] !== "*",
150
+ },
151
+ };
152
+ }
153
+
154
+ /** True if the expression parses; thin wrapper for config validation. */
155
+ export function isValidCron(expression: string): boolean {
156
+ return parseCron(expression).ok;
157
+ }
158
+
159
+ function matchesDay(cron: ParsedCron, date: Date): boolean {
160
+ const dom = cron.dom.has(date.getDate());
161
+ const dow = cron.dow.has(date.getDay());
162
+ if (cron.domRestricted && cron.dowRestricted) return dom || dow;
163
+ if (cron.domRestricted) return dom;
164
+ if (cron.dowRestricted) return dow;
165
+ return true;
166
+ }
167
+
168
+ /** True if `date`'s local civil fields match the cron. */
169
+ export function matchesCron(cron: ParsedCron, date: Date): boolean {
170
+ return (
171
+ cron.minute.has(date.getMinutes()) &&
172
+ cron.hour.has(date.getHours()) &&
173
+ cron.month.has(date.getMonth() + 1) &&
174
+ matchesDay(cron, date)
175
+ );
176
+ }
177
+
178
+ /** Local civil-minute key `YYYY-MM-DD HH:mm` — used to dedupe the repeated wall
179
+ * minute on a DST fall-back so a run isn't fired twice for the same slot. */
180
+ function civilMinuteKey(date: Date): string {
181
+ const p = (n: number, w = 2) => String(n).padStart(w, "0");
182
+ return `${p(date.getFullYear(), 4)}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}`;
183
+ }
184
+
185
+ /**
186
+ * First instant strictly after `after` whose LOCAL civil time matches `cron`.
187
+ * Returns null if none within the ~4-year cap (effectively-never schedules).
188
+ *
189
+ * @param excludeCivilMinute Skip any candidate sharing this `YYYY-MM-DD HH:mm`
190
+ * key. Pass the just-consumed run's scheduled civil minute when advancing
191
+ * `next_due_at` so a DST fall-back's repeated wall minute doesn't double-fire
192
+ * the same daily slot. (For sub-hourly every-N schedules, only the exact
193
+ * consumed minute is skipped; the other repeated-hour minutes still fire, which
194
+ * is correct — more real time elapsed.)
195
+ */
196
+ export function nextOccurrence(
197
+ cron: ParsedCron,
198
+ after: Date,
199
+ excludeCivilMinute?: string,
200
+ maxSearchMs = MAX_SEARCH_MS,
201
+ ): Date | null {
202
+ const afterMs = after.getTime();
203
+ // Align to the next whole minute strictly after `after` (never returns `after`).
204
+ let cursorMs = Math.floor(afterMs / MINUTE_MS) * MINUTE_MS + MINUTE_MS;
205
+ const capMs = afterMs + Math.max(0, Math.min(MAX_SEARCH_MS, maxSearchMs));
206
+
207
+ while (cursorMs <= capMs) {
208
+ const candidate = new Date(cursorMs);
209
+ if (matchesCron(cron, candidate)) {
210
+ if (!excludeCivilMinute || civilMinuteKey(candidate) !== excludeCivilMinute) {
211
+ return candidate;
212
+ }
213
+ }
214
+ cursorMs += MINUTE_MS;
215
+ }
216
+ return null;
217
+ }
218
+
219
+ /**
220
+ * Convenience: parse + compute next-due epoch (ms) for a schedule string.
221
+ * Returns null for `""` / invalid / effectively-never schedules — the caller
222
+ * persists `next_due_at = NULL` in all of those cases.
223
+ *
224
+ * @param consumedScheduledAtMs When advancing after a run, pass the epoch of the
225
+ * slot just satisfied (the prior `next_due_at`); its civil minute is excluded
226
+ * to prevent the DST repeated-minute double-fire.
227
+ */
228
+ export function nextDueAtMs(
229
+ expression: string,
230
+ afterMs: number,
231
+ consumedScheduledAtMs?: number,
232
+ maxSearchMs = MAX_SEARCH_MS,
233
+ ): number | null {
234
+ if (expression.trim().length === 0) return null;
235
+ const parsed = parseCron(expression);
236
+ if (!parsed.ok) return null;
237
+ const exclude =
238
+ consumedScheduledAtMs !== undefined
239
+ ? civilMinuteKey(new Date(consumedScheduledAtMs))
240
+ : undefined;
241
+ const next = nextOccurrence(parsed.cron, new Date(afterMs), exclude, maxSearchMs);
242
+ return next ? next.getTime() : null;
243
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Canonical Dreamer v2 task registry (pure — no DB imports, so the config schema
3
+ * can import the task names without pulling runtime code).
4
+ *
5
+ * v2 promotes the former post-phases (review-user-memories, key-files,
6
+ * evaluate-smart-notes) to first-class scheduled tasks alongside the agentic
7
+ * maintenance tasks, and assigns each a LEASE DOMAIN so disjoint-state tasks run
8
+ * concurrently while memory-mutating tasks serialize. See lease.ts + the A+B spec.
9
+ */
10
+
11
+ export const CANONICAL_DREAM_TASKS = [
12
+ // map-memories runs BEFORE verify (it records the file mappings verify gates
13
+ // on) and shares the memory lease, so it leads the canonical order.
14
+ "map-memories",
15
+ "verify",
16
+ "verify-broad",
17
+ "curate",
18
+ "compress-cues",
19
+ "classify-memories",
20
+ "retrospective",
21
+ "maintain-docs",
22
+ "evaluate-smart-notes",
23
+ "review-user-memories",
24
+ "promote-primers",
25
+ "refresh-primers",
26
+ ] as const;
27
+
28
+ export type DreamTaskName = (typeof CANONICAL_DREAM_TASKS)[number];
29
+
30
+ /** Cheap, read-only work counts for one Dreamer task. */
31
+ export interface DreamTaskBacklog {
32
+ /** Items selected by the task's current backlog predicate. */
33
+ pending: number;
34
+ /** Total items in the task's candidate pool. */
35
+ total: number;
36
+ }
37
+
38
+ /** Backlog counts keyed by canonical task name. */
39
+ export type DreamTaskBacklogMap = Partial<Record<DreamTaskName, DreamTaskBacklog>>;
40
+
41
+ /** Stable human-readable rendering shared by /ctx-dream and status surfaces. */
42
+ export function formatDreamTaskBacklogs(
43
+ backlogs: DreamTaskBacklogMap,
44
+ tasks: readonly DreamTaskName[] = CANONICAL_DREAM_TASKS,
45
+ ): string {
46
+ return tasks
47
+ .filter((task) => backlogs[task] !== undefined)
48
+ .map((task) => {
49
+ const backlog = backlogs[task];
50
+ return `- ${task}: ${backlog?.pending ?? 0} pending / ${backlog?.total ?? 0} total`;
51
+ })
52
+ .join("\n");
53
+ }
54
+
55
+ /** Process-local progress for the task currently applying a run chunk. */
56
+ export interface DreamTaskProgress {
57
+ task: DreamTaskName;
58
+ processed: number;
59
+ total: number;
60
+ startedAt: number;
61
+ }
62
+
63
+ /** Persisted per-task run counts used by dream-run history and summaries. */
64
+ export interface DreamTaskRunBacklog {
65
+ pendingAtStart: number;
66
+ totalAtStart: number;
67
+ pendingAtEnd: number;
68
+ totalAtEnd: number;
69
+ processed: number;
70
+ }
71
+
72
+ /** Use the decrease in the persisted backlog between the start and end snapshots as the per-run progress count, clamped to zero when the backlog does not decrease. */
73
+ export function processedDreamTaskItems(startPending: number, endPending: number): number {
74
+ return Math.max(0, startPending - endPending);
75
+ }
76
+
77
+ /**
78
+ * The agentic tasks — those run as a generic dreamer agent session driven by
79
+ * `buildDreamTaskPrompt`. The other canonical tasks (map-memories, verify,
80
+ * verify-broad, classify-memories, review-user-memories, evaluate-smart-notes,
81
+ * primers, retrospective) have their own specialized runners and do NOT go
82
+ * through the prompt builder.
83
+ */
84
+ export const AGENTIC_DREAM_TASKS = ["curate", "maintain-docs"] as const;
85
+
86
+ /**
87
+ * Tasks that read-modify-write the project `memories` table (+ epoch +
88
+ * supersede-delta rows). They SHARE one per-project "memory" lease so they
89
+ * serialize with each other — concurrent runs race semantically (stale-view
90
+ * merges/splits). Canonical run order when several are due in one drain.
91
+ */
92
+ export const MEMORY_DOMAIN_TASKS: readonly DreamTaskName[] = [
93
+ "map-memories",
94
+ "verify",
95
+ "verify-broad",
96
+ "curate",
97
+ "compress-cues",
98
+ "classify-memories",
99
+ "retrospective",
100
+ "promote-primers",
101
+ "refresh-primers",
102
+ ];
103
+
104
+ const MEMORY_DOMAIN_SET = new Set<DreamTaskName>(MEMORY_DOMAIN_TASKS);
105
+
106
+ /**
107
+ * Lease KIND per task. `memory` + the three independent kinds are per-project;
108
+ * `user-memories` is GLOBAL (mutates the cross-project user-profile pool, so two
109
+ * different projects' dreamers must not review concurrently).
110
+ */
111
+ export type LeaseKind = "memory" | "maintain-docs" | "evaluate-smart-notes" | "user-memories";
112
+
113
+ export function leaseKindFor(task: DreamTaskName): LeaseKind {
114
+ if (MEMORY_DOMAIN_SET.has(task)) return "memory";
115
+ switch (task) {
116
+ case "review-user-memories":
117
+ return "user-memories";
118
+ case "promote-primers":
119
+ case "refresh-primers":
120
+ return "memory";
121
+ case "maintain-docs":
122
+ return "maintain-docs";
123
+ case "evaluate-smart-notes":
124
+ return "evaluate-smart-notes";
125
+ default:
126
+ // Memory-domain tasks already returned above; this is unreachable.
127
+ return "memory";
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Resolve the concrete lease key for a task in a project. The global
133
+ * `user-memories` lease is NOT project-scoped (one reviewer across all projects);
134
+ * every other domain is keyed by project so different projects never block.
135
+ */
136
+ export function leaseKeyFor(task: DreamTaskName, projectIdentity: string): string {
137
+ const kind = leaseKindFor(task);
138
+ return kind === "user-memories" ? "user-memories" : `${kind}:${projectIdentity}`;
139
+ }
140
+
141
+ export function isCanonicalDreamTask(value: string): value is DreamTaskName {
142
+ return (CANONICAL_DREAM_TASKS as readonly string[]).includes(value);
143
+ }
144
+
145
+ /**
146
+ * Stable canonical ordering used when multiple due tasks share a lease domain
147
+ * (preserves the suite order for the memory domain).
148
+ */
149
+ export function compareTaskOrder(a: DreamTaskName, b: DreamTaskName): number {
150
+ return CANONICAL_DREAM_TASKS.indexOf(a) - CANONICAL_DREAM_TASKS.indexOf(b);
151
+ }
@@ -371,4 +371,125 @@ describe("detectConflicts", () => {
371
371
  expect(result.hasConflict).toBe(false);
372
372
  });
373
373
  });
374
+
375
+ // --- Compaction-off mode matrix (issue #266 S2) ---
376
+ // The detector must NOT flag OpenCode compaction.auto=true / prune=true as
377
+ // a plugin-disabling conflict when MC compaction is OFF (compaction-off
378
+ // mode), or compaction-off users get a DISABLED plugin — the exact inverse
379
+ // of intent. With MC compaction ON, today's conflict behavior is unchanged.
380
+ //
381
+ // The 2x2 matrix: MC mode (on/off) x native compaction.auto (true/false).
382
+ // Each case asserts BOTH the conflict verdict AND the plugin-enabled
383
+ // outcome (the boot path disables the plugin when hasConflict).
384
+ describe("compaction-off mode matrix (issue #266)", () => {
385
+ // The suite beforeEach sets OPENCODE_DISABLE_AUTOCOMPACT=1 to isolate
386
+ // plugin detection from compaction detection. These matrix tests
387
+ // exercise compaction detection directly, so they clear that env var
388
+ // and write an explicit compaction block to control the native state.
389
+ function writeCompactionConfig(auto: boolean, prune = false): void {
390
+ const prev = process.env.OPENCODE_DISABLE_AUTOCOMPACT;
391
+ delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
392
+ writeFileSync(
393
+ join(projectDir, "opencode.json"),
394
+ JSON.stringify({ compaction: { auto, prune } }),
395
+ );
396
+ if (prev !== undefined) process.env.OPENCODE_DISABLE_AUTOCOMPACT = prev;
397
+ }
398
+
399
+ function detectWithMode(compactionEnabled: boolean) {
400
+ const prev = process.env.OPENCODE_DISABLE_AUTOCOMPACT;
401
+ delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
402
+ try {
403
+ return detectConflicts(projectDir, { compactionEnabled });
404
+ } finally {
405
+ if (prev !== undefined) process.env.OPENCODE_DISABLE_AUTOCOMPACT = prev;
406
+ }
407
+ }
408
+
409
+ it("MC ON + auto=true → conflict fires, plugin would be disabled", () => {
410
+ writeCompactionConfig(true);
411
+ const result = detectWithMode(true);
412
+ expect(result.hasConflict).toBe(true);
413
+ expect(result.conflicts.compactionAuto).toBe(true);
414
+ });
415
+
416
+ it("MC ON + auto=false → no compaction conflict, plugin stays enabled", () => {
417
+ writeCompactionConfig(false);
418
+ const result = detectWithMode(true);
419
+ expect(result.conflicts.compactionAuto).toBe(false);
420
+ expect(result.hasConflict).toBe(false);
421
+ });
422
+
423
+ it("MC OFF + auto=true → NO conflict, plugin stays enabled (native compaction active)", () => {
424
+ writeCompactionConfig(true);
425
+ const result = detectWithMode(false);
426
+ expect(result.conflicts.compactionAuto).toBe(false);
427
+ expect(result.conflicts.compactionPrune).toBe(false);
428
+ expect(result.hasConflict).toBe(false);
429
+ // Native compaction state is still reported honestly.
430
+ expect(result.nativeCompaction.auto).toBe(true);
431
+ });
432
+
433
+ it("MC OFF + auto=false → NO conflict, no-manager configuration reported honestly", () => {
434
+ writeCompactionConfig(false);
435
+ const result = detectWithMode(false);
436
+ expect(result.conflicts.compactionAuto).toBe(false);
437
+ expect(result.hasConflict).toBe(false);
438
+ // No-manager: neither MC nor native compaction owns the window.
439
+ expect(result.nativeCompaction.auto).toBe(false);
440
+ expect(result.nativeCompaction.prune).toBe(false);
441
+ });
442
+
443
+ // Mutation direction: force mode-on in the detector with auto=true →
444
+ // conflict fires. This proves the off-gate isn't just always-pass —
445
+ // the same native config that was NOT a conflict in the off case
446
+ // becomes a conflict when the mode is forced on.
447
+ it("mutation direction: same auto=true config DOES conflict when mode forced on", () => {
448
+ writeCompactionConfig(true);
449
+ const offResult = detectWithMode(false);
450
+ const onResult = detectWithMode(true);
451
+ expect(offResult.hasConflict).toBe(false);
452
+ expect(onResult.hasConflict).toBe(true);
453
+ expect(onResult.conflicts.compactionAuto).toBe(true);
454
+ });
455
+
456
+ // prune=true follows the same gate as auto=true.
457
+ it("MC OFF + prune=true → NO conflict (prune is not a conflict in compaction-off mode)", () => {
458
+ writeCompactionConfig(false, true);
459
+ const result = detectWithMode(false);
460
+ expect(result.conflicts.compactionPrune).toBe(false);
461
+ expect(result.hasConflict).toBe(false);
462
+ expect(result.nativeCompaction.prune).toBe(true);
463
+ });
464
+
465
+ // DCP and OMO conflicts keep their existing policy in BOTH modes.
466
+ it("MC OFF + DCP plugin → DCP conflict still fires (compaction-off does not broaden compatibility)", () => {
467
+ writeProjectConfig(["@tarquinen/opencode-dcp"]);
468
+ const result = detectWithMode(false);
469
+ expect(result.conflicts.dcpPlugin).toBe(true);
470
+ expect(result.hasConflict).toBe(true);
471
+ });
472
+
473
+ it("MC OFF + OMO hooks → OMO conflicts still fire in both modes", () => {
474
+ writeProjectConfig(["oh-my-opencode"]);
475
+ const result = detectWithMode(false);
476
+ expect(result.conflicts.omoPreemptiveCompaction).toBe(true);
477
+ expect(result.hasConflict).toBe(true);
478
+ });
479
+
480
+ // Default (no options) preserves today's mode-on behavior — a call
481
+ // site that cannot supply the resolved mode fails toward mode-on.
482
+ it("default (no options) treats compaction.auto=true as a conflict (fail toward mode-on)", () => {
483
+ writeCompactionConfig(true);
484
+ const prev = process.env.OPENCODE_DISABLE_AUTOCOMPACT;
485
+ delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
486
+ try {
487
+ const result = detectConflicts(projectDir);
488
+ expect(result.conflicts.compactionAuto).toBe(true);
489
+ expect(result.hasConflict).toBe(true);
490
+ } finally {
491
+ if (prev !== undefined) process.env.OPENCODE_DISABLE_AUTOCOMPACT = prev;
492
+ }
493
+ });
494
+ });
374
495
  });
@@ -38,13 +38,55 @@ export interface ConflictResult {
38
38
  omoContextWindowMonitor: boolean;
39
39
  omoAnthropicRecovery: boolean;
40
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;
41
74
  }
42
75
 
43
76
  /**
44
77
  * Detect all conflicts that would prevent magic-context from working correctly.
45
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.
46
84
  */
47
- export function detectConflicts(directory: string): ConflictResult {
85
+ export function detectConflicts(
86
+ directory: string,
87
+ options?: DetectConflictsOptions,
88
+ ): ConflictResult {
89
+ const compactionEnabled = options?.compactionEnabled ?? true;
48
90
  const conflicts: ConflictResult["conflicts"] = {
49
91
  compactionAuto: false,
50
92
  compactionPrune: false,
@@ -57,11 +99,15 @@ export function detectConflicts(directory: string): ConflictResult {
57
99
 
58
100
  // --- Check OpenCode compaction config ---
59
101
  const compactionResult = checkCompaction(directory);
60
- 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) {
61
107
  conflicts.compactionAuto = true;
62
108
  reasons.push("OpenCode auto-compaction is enabled (compaction.auto=true)");
63
109
  }
64
- if (compactionResult.prune) {
110
+ if (compactionEnabled && compactionResult.prune) {
65
111
  conflicts.compactionPrune = true;
66
112
  reasons.push("OpenCode prune is enabled (compaction.prune=true)");
67
113
  }
@@ -100,6 +146,7 @@ export function detectConflicts(directory: string): ConflictResult {
100
146
  hasConflict: reasons.length > 0,
101
147
  reasons,
102
148
  conflicts,
149
+ nativeCompaction: { auto: compactionResult.auto, prune: compactionResult.prune },
103
150
  };
104
151
  }
105
152