@cortexkit/opencode-magic-context 0.33.1 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/dist/config/agent-disable.d.ts +22 -0
  2. package/dist/config/agent-disable.d.ts.map +1 -1
  3. package/dist/config/index.d.ts.map +1 -1
  4. package/dist/config/project-security.d.ts +7 -0
  5. package/dist/config/project-security.d.ts.map +1 -1
  6. package/dist/config/schema/magic-context.d.ts +82 -2
  7. package/dist/config/schema/magic-context.d.ts.map +1 -1
  8. package/dist/config/transform-mode.d.ts +2 -0
  9. package/dist/config/transform-mode.d.ts.map +1 -1
  10. package/dist/{conflict-warning-hook-n48c783v.js → conflict-warning-hook-fvpfvt0w.js} +1 -1
  11. package/dist/{derive-budgets-e9j09sjr.js → derive-budgets-9crp0cvd.js} +1 -1
  12. package/dist/features/builtin-commands/commands.d.ts +1 -1
  13. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  14. package/dist/features/magic-context/compaction-marker.d.ts +57 -0
  15. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  16. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  21. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  23. package/dist/features/magic-context/fail-closed-block.d.ts +10 -0
  24. package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
  25. package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts.map +1 -1
  26. package/dist/features/magic-context/git-commits/storage-git-commits.d.ts.map +1 -1
  27. package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
  28. package/dist/features/magic-context/memory/embedding-local.d.ts +7 -1
  29. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  30. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  31. package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -1
  32. package/dist/features/magic-context/memory/project-identity.d.ts +2 -6
  33. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  34. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
  35. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  36. package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
  37. package/dist/features/magic-context/mural/render-mural.d.ts.map +1 -1
  38. package/dist/features/magic-context/overflow-detection.d.ts +8 -1
  39. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  40. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  41. package/dist/features/magic-context/scheduler.d.ts.map +1 -1
  42. package/dist/features/magic-context/schema-fence-probe.d.ts +28 -0
  43. package/dist/features/magic-context/schema-fence-probe.d.ts.map +1 -0
  44. package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
  45. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  46. package/dist/features/magic-context/storage-db.d.ts +35 -1
  47. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  48. package/dist/features/magic-context/storage-meta-persisted.d.ts +38 -4
  49. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  50. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  51. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  52. package/dist/features/magic-context/storage.d.ts +1 -1
  53. package/dist/features/magic-context/storage.d.ts.map +1 -1
  54. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/boundary-execution.d.ts +1 -1
  56. package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/cache-busting-signals.d.ts +1 -0
  58. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/child-session-spawn.d.ts +26 -0
  60. package/dist/hooks/magic-context/child-session-spawn.d.ts.map +1 -0
  61. package/dist/hooks/magic-context/command-handler.d.ts +2 -0
  62. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/compaction-off-transition.d.ts +96 -0
  64. package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -0
  65. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -0
  66. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  67. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/compartment-trigger.d.ts +12 -5
  72. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +47 -1
  74. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/emergency-drop.d.ts +2 -2
  76. package/dist/hooks/magic-context/event-handler.d.ts +7 -0
  77. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  78. package/dist/hooks/magic-context/event-resolvers.d.ts +13 -21
  79. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  80. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  81. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  82. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -1
  83. package/dist/hooks/magic-context/hook-handlers.d.ts +1 -0
  84. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/hook.d.ts +11 -1
  86. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  87. package/dist/hooks/magic-context/inject-compartments.d.ts +9 -0
  88. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  89. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  92. package/dist/hooks/magic-context/rust-mode-transform.d.ts +3 -0
  93. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  94. package/dist/hooks/magic-context/send-session-notification.d.ts +20 -1
  95. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -0
  97. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +30 -0
  99. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/transform.d.ts +27 -0
  101. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  102. package/dist/{index-ey1bc14z.js → index-b563sga5.js} +7 -264
  103. package/dist/{index-y1c9tcm3.js → index-cn960ffd.js} +101 -20
  104. package/dist/{index-xyaekybw.js → index-dtas91s5.js} +6 -4
  105. package/dist/index-fcyvznch.js +279 -0
  106. package/dist/{index-63dq2g4d.js → index-ry8rh8cv.js} +138 -24
  107. package/dist/{index-c2gahrks.js → index-s0mdn690.js} +1599 -892
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +1292 -623
  110. package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
  111. package/dist/plugin/hooks/create-session-hooks.d.ts +11 -0
  112. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  113. package/dist/plugin/messages-transform.d.ts +10 -0
  114. package/dist/plugin/messages-transform.d.ts.map +1 -1
  115. package/dist/plugin/rpc-handlers.d.ts +3 -7
  116. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  117. package/dist/plugin/tool-registry.d.ts +8 -0
  118. package/dist/plugin/tool-registry.d.ts.map +1 -1
  119. package/dist/{recomp-orchestrator-2yb2hfh2.js → recomp-orchestrator-8e82vmvb.js} +6 -4
  120. package/dist/send-session-notification-6njqewzk.js +20 -0
  121. package/dist/shared/conflict-detector.d.ts +38 -1
  122. package/dist/shared/conflict-detector.d.ts.map +1 -1
  123. package/dist/shared/conflict-fixer.d.ts +18 -1
  124. package/dist/shared/conflict-fixer.d.ts.map +1 -1
  125. package/dist/shared/context-limit-provenance.d.ts +3 -0
  126. package/dist/shared/context-limit-provenance.d.ts.map +1 -0
  127. package/dist/shared/escalation-bands.d.ts +9 -0
  128. package/dist/shared/escalation-bands.d.ts.map +1 -0
  129. package/dist/shared/format-threshold.d.ts +1 -1
  130. package/dist/shared/models-dev-cache.d.ts +29 -3
  131. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  132. package/dist/shared/rpc-server.d.ts.map +1 -1
  133. package/dist/shared/rpc-types.d.ts +46 -1
  134. package/dist/shared/rpc-types.d.ts.map +1 -1
  135. package/dist/shared/storage-permissions.d.ts +5 -0
  136. package/dist/shared/storage-permissions.d.ts.map +1 -0
  137. package/dist/{storage-meta-session-td6bwtky.js → storage-meta-session-xt41zbw4.js} +2 -1
  138. package/dist/tui/compaction-off.d.ts +13 -0
  139. package/dist/tui/compaction-off.d.ts.map +1 -0
  140. package/dist/tui/data/context-db.d.ts.map +1 -1
  141. package/dist/tui-compiled/compaction-off.d.ts +13 -0
  142. package/dist/tui-compiled/compaction-off.d.ts.map +1 -0
  143. package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
  144. package/package.json +7 -2
  145. package/src/agents/dreamer.ts +68 -0
  146. package/src/agents/hidden-agent-registrations.ts +331 -0
  147. package/src/agents/historian.ts +3 -0
  148. package/src/agents/language-directive.test.ts +138 -0
  149. package/src/agents/language-directive.ts +121 -0
  150. package/src/agents/magic-context-prompt.language.test.ts +35 -0
  151. package/src/agents/magic-context-prompt.test.ts +292 -0
  152. package/src/agents/magic-context-prompt.ts +182 -0
  153. package/src/agents/permissions.test.ts +252 -0
  154. package/src/agents/permissions.ts +186 -0
  155. package/src/agents/sidekick.ts +1 -0
  156. package/src/agents/smart-note-compiler.ts +1 -0
  157. package/src/config/agent-disable.ts +106 -0
  158. package/src/config/compaction-accessor-guard.test.ts +82 -0
  159. package/src/config/compaction-backcompat.test.ts +52 -0
  160. package/src/config/index.test.ts +904 -0
  161. package/src/config/index.ts +601 -0
  162. package/src/config/migrate-config-location.test.ts +332 -0
  163. package/src/config/migrate-config-location.ts +635 -0
  164. package/src/config/migrate-dreamer-v2.test.ts +287 -0
  165. package/src/config/migrate-dreamer-v2.ts +336 -0
  166. package/src/config/migrate-experimental.ts +159 -0
  167. package/src/config/project-security.test.ts +366 -0
  168. package/src/config/project-security.ts +588 -0
  169. package/src/config/prune-config-leaf.test.ts +78 -0
  170. package/src/config/prune-config-leaf.ts +68 -0
  171. package/src/config/removed-config-keys.test.ts +116 -0
  172. package/src/config/schema/agent-overrides.ts +45 -0
  173. package/src/config/schema/magic-context.test.ts +358 -0
  174. package/src/config/schema/magic-context.ts +1002 -0
  175. package/src/config/transform-mode.test.ts +62 -0
  176. package/src/config/transform-mode.ts +34 -0
  177. package/src/config/variable.test.ts +323 -0
  178. package/src/config/variable.ts +192 -0
  179. package/src/features/magic-context/defaults.ts +1 -0
  180. package/src/features/magic-context/dreamer/cron.ts +243 -0
  181. package/src/features/magic-context/dreamer/task-registry.ts +104 -0
  182. package/src/shared/conflict-detector.test.ts +121 -0
  183. package/src/shared/conflict-detector.ts +50 -3
  184. package/src/shared/conflict-fixer.test.ts +309 -0
  185. package/src/shared/conflict-fixer.ts +82 -5
  186. package/src/shared/context-limit-provenance.ts +6 -0
  187. package/src/shared/escalation-bands.test.ts +23 -0
  188. package/src/shared/escalation-bands.ts +18 -0
  189. package/src/shared/format-threshold.ts +1 -1
  190. package/src/shared/models-dev-cache.test.ts +171 -0
  191. package/src/shared/models-dev-cache.ts +195 -38
  192. package/src/shared/rpc-server.ts +27 -18
  193. package/src/shared/rpc-types.ts +46 -1
  194. package/src/shared/storage-permissions.ts +20 -0
  195. package/src/tui/compaction-off.test.ts +56 -0
  196. package/src/tui/compaction-off.ts +29 -0
  197. package/src/tui/data/context-db.ts +6 -0
  198. package/src/tui/index.tsx +113 -65
  199. package/src/tui/slots/sidebar-content.tsx +139 -83
  200. package/src/tui-compiled/compaction-off.ts +29 -0
  201. package/src/tui-compiled/data/context-db.ts +6 -0
  202. package/src/tui-compiled/index.tsx +558 -455
  203. package/src/tui-compiled/slots/sidebar-content.tsx +237 -161
  204. package/dist/send-session-notification-tghq9gnh.js +0 -11
  205. /package/dist/{index-v7fmc1s2.js → index-ygcng9t8.js} +0 -0
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Immutable nested-config-leaf pruning for config recovery.
3
+ *
4
+ * When Zod validation flags an invalid nested field, recovery prunes just that
5
+ * field and keeps valid siblings (so one bad value doesn't reset a whole block
6
+ * and silently restore disabled behavior via schema defaults). The naive version
7
+ * deleted the FIRST child under the top-level key (`path[1]`), which is wrong for
8
+ * paths deeper than two levels: an invalid `memory.git_commit_indexing.since_days`
9
+ * has `path = ["memory","git_commit_indexing","since_days"]`, and deleting
10
+ * `path[1]` ("git_commit_indexing") drops the entire sub-block — losing a
11
+ * user's `enabled: false` so the default `enabled: true` is restored.
12
+ *
13
+ * `pruneNestedConfigLeaf` walks the full path and removes only the deepest leaf,
14
+ * deep-cloning each object along the way so sibling values (and the caller's
15
+ * original object) are never mutated.
16
+ */
17
+
18
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
19
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20
+ }
21
+
22
+ /**
23
+ * Remove a single nested leaf from `block`, given a path RELATIVE to it
24
+ * (i.e. the Zod issue path with its top-level key sliced off). Returns the new
25
+ * block plus the dotted label of the removed leaf, or null when the path can't
26
+ * be fully navigated to an object that contains the leaf.
27
+ */
28
+ export function pruneNestedConfigLeaf(
29
+ block: Record<string, unknown>,
30
+ relativePath: readonly PropertyKey[],
31
+ ): { block: Record<string, unknown>; removed: string } | null {
32
+ if (relativePath.length === 0) return null;
33
+
34
+ const result: Record<string, unknown> = { ...block };
35
+ let cursor = result;
36
+
37
+ // Navigate to the parent of the leaf, cloning each intermediate object so
38
+ // the original (and untouched siblings) are preserved.
39
+ for (let i = 0; i < relativePath.length - 1; i++) {
40
+ const seg = String(relativePath[i]);
41
+ const child = cursor[seg];
42
+ if (!isPlainObject(child)) {
43
+ // The path descends into a non-object (e.g. an ARRAY element, like an
44
+ // invalid `system_prompt_injection.skip_signatures[0]`). We can't reach
45
+ // the deeper leaf — but we CAN prune the nearest OWNING field (the
46
+ // array/primitive at `seg`) so it falls back to its schema default,
47
+ // preserving valid siblings. Without this we'd return null → the whole
48
+ // config collapses to all-defaults, dropping unrelated user settings.
49
+ if (!(seg in cursor)) return null;
50
+ delete cursor[seg];
51
+ return {
52
+ block: result,
53
+ removed: relativePath
54
+ .slice(0, i + 1)
55
+ .map(String)
56
+ .join("."),
57
+ };
58
+ }
59
+ const clonedChild: Record<string, unknown> = { ...child };
60
+ cursor[seg] = clonedChild;
61
+ cursor = clonedChild;
62
+ }
63
+
64
+ const leaf = String(relativePath[relativePath.length - 1]);
65
+ if (!(leaf in cursor)) return null;
66
+ delete cursor[leaf];
67
+ return { block: result, removed: relativePath.map(String).join(".") };
68
+ }
@@ -0,0 +1,116 @@
1
+ /// <reference types="bun-types" />
2
+
3
+ // Regression guard: the `auto_drop_tool_age` and `drop_tool_structure` config
4
+ // keys (and their camelCase threads `autoDropToolAge` / `dropToolStructure`)
5
+ // were REMOVED in Phase 2 — routine age-based tool drops were replaced by the
6
+ // tiered target-headroom emergency drop, which always full-drops. No production
7
+ // source in the plugin or Pi packages may reference them; a stray reference
8
+ // would either be dead code or a config the schema now rejects with a warning.
9
+ //
10
+ // This is a static source-text scan so it fails at test-time without runtime
11
+ // mocking. Tests, generated files, and the doctor's deletion routine (which
12
+ // must name the dead keys to strip them) are intentionally excluded.
13
+
14
+ import { describe, expect, it } from "bun:test";
15
+ import { readdirSync, readFileSync, statSync } from "node:fs";
16
+ import { join } from "node:path";
17
+
18
+ const PLUGIN_SRC = join(__dirname, "..");
19
+ const PI_SRC = join(__dirname, "..", "..", "..", "pi-plugin", "src");
20
+
21
+ const FORBIDDEN = [
22
+ "auto_drop_tool_age",
23
+ "drop_tool_structure",
24
+ "autoDropToolAge",
25
+ "dropToolStructure",
26
+ // Nudge redesign removed the rolling/iteration nudge config entirely
27
+ // (Channel 1/2 replaced them). No schema key, no camelCase thread.
28
+ "nudge_interval_tokens",
29
+ "iteration_nudge_threshold",
30
+ "nudgeIntervalTokens",
31
+ "iterationNudgeThreshold",
32
+ ];
33
+
34
+ // Files allowed to mention the dead keys: the CLI doctor names them to strip
35
+ // them from existing user configs. (The doctor lives in packages/cli, not
36
+ // scanned here, but keep this list as the documented exception surface.)
37
+ const ALLOWED_BASENAMES = new Set<string>([]);
38
+
39
+ function walkSourceFiles(dir: string, out: string[] = []): string[] {
40
+ let entries: string[];
41
+ try {
42
+ entries = readdirSync(dir);
43
+ } catch {
44
+ return out; // package not present in this checkout
45
+ }
46
+ for (const entry of entries) {
47
+ if (entry === "node_modules" || entry === "dist") continue;
48
+ const full = join(dir, entry);
49
+ const s = statSync(full);
50
+ if (s.isDirectory()) {
51
+ walkSourceFiles(full, out);
52
+ } else if (
53
+ s.isFile() &&
54
+ entry.endsWith(".ts") &&
55
+ !entry.endsWith(".test.ts") &&
56
+ !entry.endsWith(".gen.ts") &&
57
+ !ALLOWED_BASENAMES.has(entry)
58
+ ) {
59
+ out.push(full);
60
+ }
61
+ }
62
+ return out;
63
+ }
64
+
65
+ function scanForForbidden(files: string[]): string[] {
66
+ const offenders: string[] = [];
67
+ for (const file of files) {
68
+ let text: string;
69
+ try {
70
+ text = readFileSync(file, "utf-8");
71
+ } catch {
72
+ continue; // file absent in this checkout
73
+ }
74
+ for (const key of FORBIDDEN) {
75
+ if (text.includes(key)) offenders.push(`${file}: ${key}`);
76
+ }
77
+ }
78
+ return offenders;
79
+ }
80
+
81
+ const REPO_ROOT = join(__dirname, "..", "..", "..", "..");
82
+
83
+ describe("removed Phase 2 config keys", () => {
84
+ it("no production source references auto_drop_tool_age / drop_tool_structure", () => {
85
+ const files = [...walkSourceFiles(PLUGIN_SRC), ...walkSourceFiles(PI_SRC)];
86
+ expect(scanForForbidden(files)).toEqual([]);
87
+ });
88
+
89
+ // The keys also leaked into docs, the generated schema, and e2e test
90
+ // configs (where they're silently ignored — the worst kind, since the test
91
+ // reads as if the knob still works). Guard those surfaces too so doc/config
92
+ // drift fails CI alongside source drift.
93
+ it("no docs / generated schema / e2e config references the removed keys", () => {
94
+ const docFiles = [
95
+ join(REPO_ROOT, "CONFIGURATION.md"),
96
+ join(REPO_ROOT, "README.md"),
97
+ join(REPO_ROOT, "assets", "magic-context.schema.json"),
98
+ ];
99
+ const e2eFiles = walkSourceFiles(join(REPO_ROOT, "packages", "e2e-tests", "tests")).filter(
100
+ // e2e tests are the consumer surface here, not unit tests of the
101
+ // keys — scan them despite the .test.ts skip in walkSourceFiles.
102
+ () => true,
103
+ );
104
+ // walkSourceFiles skips .test.ts, so collect e2e .test.ts explicitly.
105
+ const e2eDir = join(REPO_ROOT, "packages", "e2e-tests", "tests");
106
+ let e2eEntries: string[] = [];
107
+ try {
108
+ e2eEntries = readdirSync(e2eDir)
109
+ .filter((f) => f.endsWith(".ts"))
110
+ .map((f) => join(e2eDir, f));
111
+ } catch {
112
+ e2eEntries = [];
113
+ }
114
+ expect(scanForForbidden([...docFiles, ...e2eFiles, ...e2eEntries])).toEqual([]);
115
+ });
116
+ });
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+
3
+ const PermissionValueSchema = z.enum(["ask", "allow", "deny"]);
4
+
5
+ const PermissionSchema = z
6
+ .object({
7
+ edit: PermissionValueSchema.optional(),
8
+ bash: z
9
+ .union([PermissionValueSchema, z.record(z.string(), PermissionValueSchema)])
10
+ .optional(),
11
+ webfetch: PermissionValueSchema.optional(),
12
+ doom_loop: PermissionValueSchema.optional(),
13
+ external_directory: PermissionValueSchema.optional(),
14
+ })
15
+ .optional();
16
+
17
+ export const AgentOverrideConfigSchema = z.object({
18
+ model: z.string().optional().describe("Primary model ID (e.g. 'claude-sonnet-4-6')"),
19
+ temperature: z.number().min(0).max(2).optional().describe("Sampling temperature (0-2)"),
20
+ top_p: z.number().min(0).max(1).optional().describe("Nucleus sampling top_p (0-1)"),
21
+ prompt: z.string().optional().describe("Additional system prompt text"),
22
+ tools: z.record(z.string(), z.boolean()).optional().describe("Tool enable/disable overrides"),
23
+ disable: z.boolean().optional().describe("Disable this agent"),
24
+ description: z.string().optional().describe("Agent description"),
25
+ mode: z
26
+ .enum(["subagent", "primary", "all"])
27
+ .optional()
28
+ .describe("Agent mode (subagent, primary, or all)"),
29
+ color: z
30
+ .string()
31
+ .regex(/^#[0-9A-Fa-f]{6}$/)
32
+ .optional()
33
+ .describe("Hex color for the agent (e.g. '#a1b2c3')"),
34
+ maxSteps: z.number().optional().describe("Maximum tool-call steps per invocation"),
35
+ permission: PermissionSchema.describe("Per-tool permission overrides"),
36
+ maxTokens: z.number().optional().describe("Maximum output tokens"),
37
+ variant: z
38
+ .string()
39
+ .optional()
40
+ .describe("OpenCode reasoning variant (e.g. for extended thinking)"),
41
+ fallback_models: z
42
+ .union([z.string(), z.array(z.string())])
43
+ .optional()
44
+ .describe("Fallback model IDs if primary is unavailable"),
45
+ });
@@ -0,0 +1,358 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ DEFAULT_HISTORIAN_TIMEOUT_MS,
4
+ DEFAULT_HISTORY_BUDGET_PERCENTAGE,
5
+ DEFAULT_LOCAL_EMBEDDING_MODEL,
6
+ type MagicContextConfig,
7
+ MagicContextConfigSchema,
8
+ } from "./magic-context";
9
+
10
+ describe("MagicContextConfigSchema", () => {
11
+ describe("defaults", () => {
12
+ it("applies defaults for an empty config", () => {
13
+ const result = MagicContextConfigSchema.parse({});
14
+
15
+ expect(result).toMatchObject({
16
+ enabled: true,
17
+ allow_home_project: false,
18
+ fail_closed_blocking: true,
19
+ transform_mode: "ts",
20
+ storage: { enforce_private_permissions: true },
21
+ cache_ttl: "5m",
22
+ execute_threshold_percentage: 65,
23
+ protected_tags: 20,
24
+ clear_reasoning_age: 50,
25
+ history_budget_percentage: DEFAULT_HISTORY_BUDGET_PERCENTAGE,
26
+ historian_timeout_ms: DEFAULT_HISTORIAN_TIMEOUT_MS,
27
+ embedding: {
28
+ provider: "local",
29
+ model: DEFAULT_LOCAL_EMBEDDING_MODEL,
30
+ },
31
+ memory: {
32
+ enabled: true,
33
+ injection_budget_tokens: 4000,
34
+ auto_promote: true,
35
+ retrieval_count_promotion_threshold: 3,
36
+ },
37
+ todowrite: {
38
+ enabled: true,
39
+ overlay: true,
40
+ },
41
+ });
42
+ expect(result.historian).toBeUndefined();
43
+ expect(result.dreamer).toBeUndefined();
44
+ expect(result.sidekick).toBeUndefined();
45
+ expect(result.pi).toBeUndefined();
46
+ });
47
+ });
48
+
49
+ describe("budget configuration", () => {
50
+ it("accepts 90% execute thresholds and rejects 91%", () => {
51
+ expect(
52
+ MagicContextConfigSchema.safeParse({ execute_threshold_percentage: 90 }).success,
53
+ ).toBe(true);
54
+ expect(
55
+ MagicContextConfigSchema.safeParse({ execute_threshold_percentage: 91 }).success,
56
+ ).toBe(false);
57
+ });
58
+
59
+ it("accepts numeric and per-model output reserves including zero", () => {
60
+ expect(MagicContextConfigSchema.parse({ output_reserve: 0 }).output_reserve).toBe(0);
61
+ expect(
62
+ MagicContextConfigSchema.parse({
63
+ output_reserve: { default: 16_384, "google/gemini": 0 },
64
+ }).output_reserve,
65
+ ).toEqual({ default: 16_384, "google/gemini": 0 });
66
+ expect(MagicContextConfigSchema.safeParse({ output_reserve: -1 }).success).toBe(false);
67
+ });
68
+ });
69
+
70
+ describe("valid config", () => {
71
+ it("parses an enabled config without stale reduction-specific keys", () => {
72
+ const input = {
73
+ enabled: true,
74
+ allow_home_project: false,
75
+ fail_closed_blocking: true,
76
+ experimental: { mural: { enabled: false } },
77
+ transform_mode: "ts",
78
+ auto_update: false,
79
+ toast_duration_ms: 5000,
80
+ cache_ttl: "10m",
81
+ protected_tags: 3,
82
+ execute_threshold_percentage: 75,
83
+ clear_reasoning_age: 60,
84
+ history_budget_percentage: 0.2,
85
+ historian_timeout_ms: 360_000,
86
+ commit_cluster_trigger: {
87
+ enabled: true,
88
+ min_clusters: 3,
89
+ },
90
+ sqlite: {
91
+ cache_size_mb: 64,
92
+ mmap_size_mb: 0,
93
+ },
94
+ storage: {
95
+ enforce_private_permissions: false,
96
+ },
97
+ system_prompt_injection: {
98
+ enabled: true,
99
+ skip_signatures: ["<!-- magic-context: skip -->"],
100
+ },
101
+ temporal_awareness: false,
102
+ keep_subagents: false,
103
+ todowrite: {
104
+ enabled: false,
105
+ overlay: false,
106
+ },
107
+ smart_drops: false,
108
+ shadow_embedding: {
109
+ enabled: false,
110
+ },
111
+ caveman_text_compression: {
112
+ enabled: false,
113
+ min_chars: 500,
114
+ },
115
+ embedding: {
116
+ provider: "openai-compatible",
117
+ endpoint: "http://localhost:1234/v1",
118
+ model: "text-embedding-3-small",
119
+ api_key: "secret-embedding",
120
+ },
121
+ memory: {
122
+ enabled: true,
123
+ injection_budget_tokens: 4000,
124
+ auto_promote: true,
125
+ retrieval_count_promotion_threshold: 3,
126
+ auto_search: {
127
+ enabled: false,
128
+ score_threshold: 0.6,
129
+ min_prompt_chars: 20,
130
+ },
131
+ git_commit_indexing: {
132
+ enabled: false,
133
+ since_days: 365,
134
+ max_commits: 2000,
135
+ },
136
+ },
137
+ pi: {
138
+ subagent_extensions: ["@example/provider", "./extensions/local.ts"],
139
+ },
140
+ sidekick: {
141
+ disable: false,
142
+ model: "qwen-test",
143
+ fallback_models: ["qwen-fallback"],
144
+ temperature: 0.1,
145
+ variant: "fast",
146
+ timeout_ms: 12_000,
147
+ system_prompt: "Custom prompt",
148
+ },
149
+ compaction: {
150
+ enabled: true,
151
+ },
152
+ } satisfies MagicContextConfig;
153
+
154
+ const result = MagicContextConfigSchema.parse(input);
155
+
156
+ expect(result).toEqual(input);
157
+ });
158
+
159
+ it("accepts a boolean storage permission policy and rejects non-booleans", () => {
160
+ expect(
161
+ MagicContextConfigSchema.parse({
162
+ storage: { enforce_private_permissions: false },
163
+ }).storage.enforce_private_permissions,
164
+ ).toBe(false);
165
+ expect(
166
+ MagicContextConfigSchema.safeParse({
167
+ storage: { enforce_private_permissions: "false" },
168
+ }).success,
169
+ ).toBe(false);
170
+ });
171
+
172
+ it("applies sidekick defaults when the object is present", () => {
173
+ const result = MagicContextConfigSchema.parse({
174
+ sidekick: {
175
+ model: "github-copilot/gpt-5.4",
176
+ },
177
+ });
178
+
179
+ expect(result.sidekick).toEqual({
180
+ model: "github-copilot/gpt-5.4",
181
+ timeout_ms: 30000,
182
+ });
183
+ });
184
+
185
+ it("accepts disable on hidden agents and strips deprecated top-level enabled", () => {
186
+ const result = MagicContextConfigSchema.parse({
187
+ historian: { disable: true },
188
+ dreamer: {
189
+ disable: true,
190
+ enabled: true,
191
+ // Dreamer v2: per-task config. review-user-memories disabled,
192
+ // maintain-docs scheduled.
193
+ tasks: {
194
+ "review-user-memories": { schedule: "" },
195
+ "maintain-docs": { schedule: "0 * * * *" },
196
+ },
197
+ },
198
+ sidekick: { disable: true, enabled: true },
199
+ });
200
+
201
+ expect(result.historian?.disable).toBe(true);
202
+ expect(result.dreamer?.disable).toBe(true);
203
+ expect(result.sidekick?.disable).toBe(true);
204
+ expect("enabled" in (result.dreamer as Record<string, unknown>)).toBe(false);
205
+ expect("enabled" in (result.sidekick as Record<string, unknown>)).toBe(false);
206
+ expect(result.dreamer?.tasks["review-user-memories"].schedule).toBe("");
207
+ expect(result.dreamer?.tasks["maintain-docs"].schedule).toBe("0 * * * *");
208
+ expect(result.dreamer?.tasks["classify-memories"].schedule).toBe("0 6 * * *");
209
+ expect(result.dreamer?.tasks.retrospective.schedule).toBe("0 5 * * *");
210
+ });
211
+
212
+ it("defaults classify-memories and retrospective on daily in dreamer task schema", () => {
213
+ const result = MagicContextConfigSchema.parse({ dreamer: { model: "x/y" } });
214
+ expect(result.dreamer?.tasks["classify-memories"].schedule).toBe("0 6 * * *");
215
+ expect(result.dreamer?.tasks.retrospective.schedule).toBe("0 5 * * *");
216
+ });
217
+
218
+ it("parses both transform modes", () => {
219
+ expect(MagicContextConfigSchema.parse({ transform_mode: "ts" }).transform_mode).toBe(
220
+ "ts",
221
+ );
222
+ expect(MagicContextConfigSchema.parse({ transform_mode: "rust" }).transform_mode).toBe(
223
+ "rust",
224
+ );
225
+ });
226
+
227
+ it("accepts optional auto_update user preference", () => {
228
+ expect(MagicContextConfigSchema.parse({ auto_update: false }).auto_update).toBe(false);
229
+ expect(MagicContextConfigSchema.parse({ auto_update: true }).auto_update).toBe(true);
230
+ });
231
+
232
+ it("accepts an explicitly configured Pi subagent extension allowlist", () => {
233
+ expect(
234
+ MagicContextConfigSchema.parse({
235
+ pi: { subagent_extensions: ["provider-package", "./local.ts"] },
236
+ }).pi,
237
+ ).toEqual({ subagent_extensions: ["provider-package", "./local.ts"] });
238
+ });
239
+
240
+ it("accepts and normalizes 2-letter ISO 639-1 language codes", () => {
241
+ expect(MagicContextConfigSchema.parse({ language: "tr" }).language).toBe("tr");
242
+ expect(MagicContextConfigSchema.parse({ language: " ES " }).language).toBe("es");
243
+ expect(MagicContextConfigSchema.parse({ language: "ja" }).language).toBe("ja");
244
+ });
245
+
246
+ it("parses per-model cache_ttl objects", () => {
247
+ const input = {
248
+ cache_ttl: {
249
+ default: "5m",
250
+ "claude-3-haiku": "10m",
251
+ "gpt-4": "2m",
252
+ },
253
+ };
254
+
255
+ const result = MagicContextConfigSchema.parse(input);
256
+
257
+ expect(result.cache_ttl).toEqual(input.cache_ttl);
258
+ });
259
+ });
260
+
261
+ describe("validation", () => {
262
+ it("rejects an unknown transform mode", () => {
263
+ expect(() => MagicContextConfigSchema.parse({ transform_mode: "wasm" })).toThrow();
264
+ });
265
+
266
+ it("rejects empty Pi subagent extension entries", () => {
267
+ expect(() =>
268
+ MagicContextConfigSchema.parse({ pi: { subagent_extensions: [" "] } }),
269
+ ).toThrow();
270
+ });
271
+
272
+ it("rejects protected_tags greater than 100", () => {
273
+ expect(() => MagicContextConfigSchema.parse({ protected_tags: 101 })).toThrow();
274
+ });
275
+
276
+ it("rejects protected_tags less than 1", () => {
277
+ expect(() => MagicContextConfigSchema.parse({ protected_tags: 0 })).toThrow();
278
+ });
279
+
280
+ it("accepts protected_tags boundary values", () => {
281
+ expect(MagicContextConfigSchema.parse({ protected_tags: 1 }).protected_tags).toBe(1);
282
+ expect(MagicContextConfigSchema.parse({ protected_tags: 20 }).protected_tags).toBe(20);
283
+ });
284
+
285
+ it("rejects clear_reasoning_age below minimum", () => {
286
+ expect(() => MagicContextConfigSchema.parse({ clear_reasoning_age: 9 })).toThrow();
287
+ });
288
+
289
+ it("rejects historian_timeout_ms below minimum", () => {
290
+ expect(() =>
291
+ MagicContextConfigSchema.parse({ historian_timeout_ms: 59_999 }),
292
+ ).toThrow();
293
+ });
294
+
295
+ it("rejects non-code output language values", () => {
296
+ expect(() => MagicContextConfigSchema.parse({ language: "Turkish" })).toThrow(); // full name
297
+ expect(() => MagicContextConfigSchema.parse({ language: "tur" })).toThrow(); // 3-letter
298
+ expect(() => MagicContextConfigSchema.parse({ language: "zz" })).toThrow(); // unknown code
299
+ expect(() => MagicContextConfigSchema.parse({ language: "<x>" })).toThrow();
300
+ });
301
+
302
+ it("rejects openai-compatible embedding config without endpoint", () => {
303
+ expect(() =>
304
+ MagicContextConfigSchema.parse({
305
+ embedding: {
306
+ provider: "openai-compatible",
307
+ model: "text-embedding-3-small",
308
+ },
309
+ }),
310
+ ).toThrow();
311
+ });
312
+
313
+ it("rejects openai-compatible embedding config without model", () => {
314
+ expect(() =>
315
+ MagicContextConfigSchema.parse({
316
+ embedding: {
317
+ provider: "openai-compatible",
318
+ endpoint: "http://localhost:1234/v1",
319
+ },
320
+ }),
321
+ ).toThrow();
322
+ });
323
+
324
+ it("accepts a configured local embedding dtype", () => {
325
+ const result = MagicContextConfigSchema.parse({
326
+ embedding: {
327
+ provider: "local",
328
+ model: "Xenova/paraphrase-multilingual-MiniLM-L12-v2",
329
+ local_dtype: "q8",
330
+ },
331
+ });
332
+ expect(result.embedding).toEqual({
333
+ provider: "local",
334
+ model: "Xenova/paraphrase-multilingual-MiniLM-L12-v2",
335
+ local_dtype: "q8",
336
+ });
337
+ });
338
+
339
+ it("omits local_dtype from the resolved config when unset (preserves default identity)", () => {
340
+ const result = MagicContextConfigSchema.parse({
341
+ embedding: { provider: "local" },
342
+ });
343
+ expect(result.embedding).toEqual({
344
+ provider: "local",
345
+ model: DEFAULT_LOCAL_EMBEDDING_MODEL,
346
+ });
347
+ expect("local_dtype" in result.embedding).toBe(false);
348
+ });
349
+
350
+ it("rejects an unsupported local embedding dtype", () => {
351
+ expect(() =>
352
+ MagicContextConfigSchema.parse({
353
+ embedding: { provider: "local", local_dtype: "fp64" },
354
+ }),
355
+ ).toThrow();
356
+ });
357
+ });
358
+ });