@cortexkit/opencode-magic-context 0.38.1 → 0.39.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 (143) hide show
  1. package/README.md +37 -4
  2. package/dist/config/index.d.ts.map +1 -1
  3. package/dist/config/project-security.d.ts +11 -6
  4. package/dist/config/project-security.d.ts.map +1 -1
  5. package/dist/config/raw-loader.d.ts +42 -0
  6. package/dist/config/raw-loader.d.ts.map +1 -0
  7. package/dist/config/schema/magic-context.d.ts +585 -550
  8. package/dist/config/schema/magic-context.d.ts.map +1 -1
  9. package/dist/features/magic-context/dreamer/classify.d.ts +3 -2
  10. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  11. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts +9 -2
  12. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  13. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +4 -2
  14. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
  15. package/dist/features/magic-context/dreamer/map-memories.d.ts +21 -5
  16. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/refresh-primers.d.ts +3 -2
  18. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/task-config.d.ts +2 -1
  20. package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
  21. package/dist/features/magic-context/dreamer/task-executor.d.ts +0 -1
  22. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  23. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -2
  24. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  25. package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -2
  26. package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
  27. package/dist/features/magic-context/dreamer/verify.d.ts +6 -3
  28. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  29. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  30. package/dist/features/magic-context/mural/compress-cues.d.ts +3 -2
  31. package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
  32. package/dist/features/magic-context/smart-notes/compiler.d.ts +3 -2
  33. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  34. package/dist/features/magic-context/smart-notes/condition-compiler.d.ts +2 -0
  35. package/dist/features/magic-context/smart-notes/condition-compiler.d.ts.map +1 -1
  36. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
  37. package/dist/features/magic-context/storage-db.d.ts +1 -1
  38. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  39. package/dist/features/magic-context/storage-meta-persisted.d.ts +7 -2
  40. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  41. package/dist/features/magic-context/storage-meta-shared.d.ts +1 -0
  42. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  43. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  44. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  45. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  46. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  47. package/dist/features/magic-context/storage.d.ts +1 -1
  48. package/dist/features/magic-context/storage.d.ts.map +1 -1
  49. package/dist/features/magic-context/tool-definition-tokens.d.ts +10 -0
  50. package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
  51. package/dist/features/magic-context/transform-decision-log.d.ts +11 -0
  52. package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
  53. package/dist/features/magic-context/user-memory/review-user-memories.d.ts +3 -2
  54. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/channel2-delivery.d.ts +3 -1
  56. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/command-handler.d.ts +4 -0
  58. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +4 -1
  60. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  62. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/compartment-runner-types.d.ts +4 -1
  64. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +14 -2
  66. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
  67. package/dist/hooks/magic-context/emergency-fail-closed.d.ts +7 -0
  68. package/dist/hooks/magic-context/emergency-fail-closed.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/inject-compartments.d.ts +12 -1
  72. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/lkg-persist.d.ts +14 -0
  74. package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -0
  75. package/dist/hooks/magic-context/lkg-slot.d.ts +13 -0
  76. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/module-state-sync.d.ts +1 -0
  78. package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/module-transport.d.ts +12 -0
  80. package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/raw-fallback-context-limit.d.ts.map +1 -1
  82. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +4 -1
  83. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  84. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +12 -0
  86. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
  87. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -1
  88. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  89. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +4 -0
  90. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/transform.d.ts +9 -1
  92. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
  94. package/dist/{index-2y0ybv5x.js → index-3dkzzh0t.js} +15 -17
  95. package/dist/index-ehpzdy0v.js +126 -0
  96. package/dist/{index-5znjkjws.js → index-k2ts0wk1.js} +486 -106
  97. package/dist/{index-22mw8km4.js → index-rw0qrkvq.js} +4 -2
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +17186 -14853
  100. package/dist/model-resolution-r9jbcz86.js +15 -0
  101. package/dist/plugin/dream-timer.d.ts +3 -0
  102. package/dist/plugin/dream-timer.d.ts.map +1 -1
  103. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  104. package/dist/plugin/messages-transform.d.ts +14 -6
  105. package/dist/plugin/messages-transform.d.ts.map +1 -1
  106. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  107. package/dist/{recomp-orchestrator-p030kxtc.js → recomp-orchestrator-2pncxee7.js} +2 -3
  108. package/dist/shared/jsonc-edit.d.ts +6 -1
  109. package/dist/shared/jsonc-edit.d.ts.map +1 -1
  110. package/dist/shared/model-resolution.d.ts +53 -0
  111. package/dist/shared/model-resolution.d.ts.map +1 -0
  112. package/dist/shared/model-suggestion-retry.d.ts +4 -1
  113. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  114. package/dist/shared/resolve-fallbacks.d.ts +5 -1
  115. package/dist/shared/resolve-fallbacks.d.ts.map +1 -1
  116. package/dist/shared/status-detail-text.d.ts +4 -0
  117. package/dist/shared/status-detail-text.d.ts.map +1 -0
  118. package/dist/shared/subagent-runner.d.ts +2 -1
  119. package/dist/shared/subagent-runner.d.ts.map +1 -1
  120. package/dist/{storage-meta-session-2306t3j7.js → storage-meta-session-yvtbaw7c.js} +1 -1
  121. package/dist/{task-config-at3adc59.js → task-config-a1qmj9bx.js} +2 -2
  122. package/package.json +4 -2
  123. package/src/config/index.test.ts +73 -17
  124. package/src/config/index.ts +38 -23
  125. package/src/config/project-security.hostile-matrix.test.ts +471 -0
  126. package/src/config/project-security.test.ts +26 -5
  127. package/src/config/project-security.ts +141 -22
  128. package/src/config/raw-loader.test.ts +424 -0
  129. package/src/config/raw-loader.ts +490 -0
  130. package/src/config/schema/magic-context.test.ts +144 -2
  131. package/src/config/schema/magic-context.ts +253 -49
  132. package/src/shared/jsonc-edit.ts +74 -1
  133. package/src/shared/model-resolution.test.ts +170 -0
  134. package/src/shared/model-resolution.ts +227 -0
  135. package/src/shared/model-suggestion-retry.test.ts +27 -0
  136. package/src/shared/model-suggestion-retry.ts +19 -10
  137. package/src/shared/resolve-fallbacks.test.ts +13 -1
  138. package/src/shared/resolve-fallbacks.ts +19 -4
  139. package/src/shared/status-detail-text.test.ts +80 -0
  140. package/src/shared/status-detail-text.ts +56 -0
  141. package/src/shared/subagent-runner.ts +3 -1
  142. package/dist/index-td9depx5.js +0 -46
  143. package/dist/resolve-fallbacks-qf0ekmz9.js +0 -11
@@ -104,6 +104,212 @@ export const PromptSurfaceConfigSchema = z
104
104
  );
105
105
  export type PromptSurfaceConfig = z.infer<typeof PromptSurfaceConfigSchema>;
106
106
 
107
+ /**
108
+ * The flat model-resolution fields are the only fields that move into harness
109
+ * blocks. Keeping this inventory next to the schema gives the loader migration
110
+ * an exhaustive contract instead of a catch-all rule.
111
+ */
112
+ export const PER_HARNESS_MIGRATION_INVENTORY = {
113
+ historian: {
114
+ retained: [
115
+ "temperature",
116
+ "top_p",
117
+ "prompt",
118
+ "tools",
119
+ "disable",
120
+ "description",
121
+ "mode",
122
+ "color",
123
+ "maxSteps",
124
+ "permission",
125
+ "maxTokens",
126
+ "two_pass",
127
+ "disallowed_tools",
128
+ ],
129
+ migrated_execution: ["model", "fallback_models", "variant", "thinking_level"],
130
+ },
131
+ dreamer: {
132
+ retained: [
133
+ "temperature",
134
+ "top_p",
135
+ "prompt",
136
+ "tools",
137
+ "disable",
138
+ "description",
139
+ "mode",
140
+ "color",
141
+ "maxSteps",
142
+ "permission",
143
+ "maxTokens",
144
+ "inject_docs",
145
+ ],
146
+ migrated_execution: ["model", "fallback_models", "variant", "thinking_level"],
147
+ },
148
+ task: {
149
+ retained: ["schedule", "promotion_threshold"],
150
+ migrated_execution: [
151
+ "model",
152
+ "fallback_models",
153
+ "variant",
154
+ "thinking_level",
155
+ "timeout_minutes",
156
+ ],
157
+ },
158
+ } as const;
159
+
160
+ /** OpenCode entry objects permit `variant` and reject Pi-only `thinking_level`. */
161
+ const OcEntryObjectSchema = z
162
+ .object({
163
+ model: z.string().describe("OpenCode model ID (for example, provider/model)."),
164
+ variant: z.string().optional().describe("OpenCode reasoning variant for this entry."),
165
+ })
166
+ .strict();
167
+ export const OcEntrySchema = z.union([z.string(), OcEntryObjectSchema]);
168
+ export type OcEntry = z.infer<typeof OcEntrySchema>;
169
+
170
+ /** Pi entry objects permit `thinking_level` and reject OpenCode-only `variant`. */
171
+ const PiEntryObjectSchema = z
172
+ .object({
173
+ model: z.string().describe("Pi model ID (for example, provider/model)."),
174
+ thinking_level: PiThinkingLevelSchema.describe("Pi thinking level for this entry."),
175
+ })
176
+ .strict();
177
+ export const PiEntrySchema = z.union([z.string(), PiEntryObjectSchema]);
178
+ export type PiEntry = z.infer<typeof PiEntrySchema>;
179
+
180
+ /** Strict model-resolution block used by historian.opencode. */
181
+ export const OpenCodeHarnessBlockSchema = z
182
+ .object({
183
+ model: OcEntrySchema.optional().describe("Primary OpenCode model entry."),
184
+ fallback_models: z
185
+ .array(OcEntrySchema)
186
+ .optional()
187
+ .describe(
188
+ "Ordered fallback OpenCode entries. New-shape configuration requires an array; legacy singleton values migrate to a one-element array.",
189
+ ),
190
+ variant: z
191
+ .string()
192
+ .optional()
193
+ .describe(
194
+ "OpenCode reasoning variant for the primary entry when it declares none. Fallback entries declare variants per-entry.",
195
+ ),
196
+ })
197
+ .strict()
198
+ .describe("Strict OpenCode model-resolution block. It accepts no Pi vocabulary.");
199
+ export type OpenCodeHarnessBlock = z.infer<typeof OpenCodeHarnessBlockSchema>;
200
+
201
+ /** Strict model-resolution block used by historian.pi. */
202
+ export const PiHarnessBlockSchema = z
203
+ .object({
204
+ model: PiEntrySchema.optional().describe("Primary Pi model entry."),
205
+ fallback_models: z
206
+ .array(PiEntrySchema)
207
+ .optional()
208
+ .describe(
209
+ "Ordered fallback Pi entries. New-shape configuration requires an array; legacy singleton values migrate to a one-element array.",
210
+ ),
211
+ thinking_level: PiThinkingLevelSchema.describe(
212
+ "Pi thinking level for the primary entry when it declares none. Fallback entries declare thinking levels per-entry.",
213
+ ),
214
+ })
215
+ .strict()
216
+ .describe("Strict Pi model-resolution block. It accepts no OpenCode vocabulary.");
217
+ export type PiHarnessBlock = z.infer<typeof PiHarnessBlockSchema>;
218
+
219
+ /** Strict OpenCode-only execution override for one dreamer task. */
220
+ export const OpenCodeTaskExecutionSchema = z
221
+ .object({
222
+ model: OcEntrySchema.optional().describe("OpenCode model entry for this task."),
223
+ fallback_models: z
224
+ .array(OcEntrySchema)
225
+ .optional()
226
+ .describe("Ordered OpenCode fallback entries for this task."),
227
+ variant: z
228
+ .string()
229
+ .optional()
230
+ .describe(
231
+ "OpenCode reasoning variant for this task's primary entry when it declares none. Fallback entries declare variants per-entry.",
232
+ ),
233
+ timeout_minutes: z
234
+ .number()
235
+ .min(5)
236
+ .optional()
237
+ .describe("Minutes allowed for this task before it is aborted."),
238
+ })
239
+ .strict();
240
+ export type OpenCodeTaskExecution = z.infer<typeof OpenCodeTaskExecutionSchema>;
241
+
242
+ /** Strict Pi-only execution override for one dreamer task. */
243
+ export const PiTaskExecutionSchema = z
244
+ .object({
245
+ model: PiEntrySchema.optional().describe("Pi model entry for this task."),
246
+ fallback_models: z
247
+ .array(PiEntrySchema)
248
+ .optional()
249
+ .describe("Ordered Pi fallback entries for this task."),
250
+ thinking_level: PiThinkingLevelSchema.describe(
251
+ "Pi thinking level for this task's primary entry when it declares none. Fallback entries declare thinking levels per-entry.",
252
+ ),
253
+ timeout_minutes: z
254
+ .number()
255
+ .min(5)
256
+ .optional()
257
+ .describe("Minutes allowed for this task before it is aborted."),
258
+ })
259
+ .strict();
260
+ export type PiTaskExecution = z.infer<typeof PiTaskExecutionSchema>;
261
+
262
+ /** Strict OpenCode harness block for dreamer execution and task overrides. */
263
+ export const DreamerOpenCodeHarnessBlockSchema = z
264
+ .object({
265
+ model: OcEntrySchema.optional().describe("Primary OpenCode model entry."),
266
+ fallback_models: z
267
+ .array(OcEntrySchema)
268
+ .optional()
269
+ .describe(
270
+ "Ordered fallback OpenCode entries. New-shape configuration requires an array; legacy singleton values migrate to a one-element array.",
271
+ ),
272
+ variant: z
273
+ .string()
274
+ .optional()
275
+ .describe(
276
+ "OpenCode reasoning variant for the primary entry when it declares none. Fallback entries declare variants per-entry.",
277
+ ),
278
+ tasks: z
279
+ .record(z.string(), OpenCodeTaskExecutionSchema)
280
+ .optional()
281
+ .describe(
282
+ "OpenCode task execution overrides. Each named task accepts only model, fallback_models, variant, and timeout_minutes.",
283
+ ),
284
+ })
285
+ .strict()
286
+ .describe("Strict OpenCode dreamer model-resolution block. It accepts no Pi vocabulary.");
287
+ export type DreamerOpenCodeHarnessBlock = z.infer<typeof DreamerOpenCodeHarnessBlockSchema>;
288
+
289
+ /** Strict Pi harness block for dreamer execution and task overrides. */
290
+ export const DreamerPiHarnessBlockSchema = z
291
+ .object({
292
+ model: PiEntrySchema.optional().describe("Primary Pi model entry."),
293
+ fallback_models: z
294
+ .array(PiEntrySchema)
295
+ .optional()
296
+ .describe(
297
+ "Ordered fallback Pi entries. New-shape configuration requires an array; legacy singleton values migrate to a one-element array.",
298
+ ),
299
+ thinking_level: PiThinkingLevelSchema.describe(
300
+ "Pi thinking level for the primary entry when it declares none. Fallback entries declare thinking levels per-entry.",
301
+ ),
302
+ tasks: z
303
+ .record(z.string(), PiTaskExecutionSchema)
304
+ .optional()
305
+ .describe(
306
+ "Pi task execution overrides. Each named task accepts only model, fallback_models, thinking_level, and timeout_minutes.",
307
+ ),
308
+ })
309
+ .strict()
310
+ .describe("Strict Pi dreamer model-resolution block. It accepts no OpenCode vocabulary.");
311
+ export type DreamerPiHarnessBlock = z.infer<typeof DreamerPiHarnessBlockSchema>;
312
+
107
313
  /** A 5-field cron expression, or "" to disable the task. */
108
314
  const CronScheduleSchema = z
109
315
  .string()
@@ -113,23 +319,15 @@ const CronScheduleSchema = z
113
319
  })
114
320
  .describe('5-field cron schedule (e.g. "0 3 * * *"), or "" to disable this task.');
115
321
 
116
- /** Per-task scheduling + model config. Model/fallback/thinking inherit the
117
- * dreamer-level defaults when omitted. Task-specific params (promotion_threshold)
118
- * are optional and only meaningful for their owning task. */
119
- const DreamTaskBaseConfigSchema = z.object({
120
- schedule: CronScheduleSchema.default(""),
121
- model: z.string().optional().describe("Per-task model override (inherits dreamer.model)"),
122
- fallback_models: z
123
- .union([z.string(), z.array(z.string())])
124
- .optional()
125
- .describe("Per-task fallback chain (inherits dreamer.fallback_models)"),
126
- thinking_level: PiThinkingLevelSchema.describe("Pi only: per-task thinking level"),
127
- timeout_minutes: z
128
- .number()
129
- .min(5)
130
- .default(20)
131
- .describe("Minutes allowed for this task before it is aborted"),
132
- });
322
+ /**
323
+ * Harness-independent task metadata. These fields stay at dreamer.tasks.<task>
324
+ * during per-harness migration; model execution fields are deliberately absent.
325
+ */
326
+ const DreamTaskBaseConfigSchema = z
327
+ .object({
328
+ schedule: CronScheduleSchema.default(""),
329
+ })
330
+ .strict();
133
331
 
134
332
  const PromotionThresholdSchema = z
135
333
  .number()
@@ -188,11 +386,8 @@ function defaultTaskConfig(task: DreamTaskName): z.input<typeof DreamTaskConfigS
188
386
  return base;
189
387
  }
190
388
 
191
- // `.default()` in Zod 4 wants the OUTPUT shape; the function form lets us derive
192
- // it by parsing the (partial) input default so timeout_minutes etc. fill in.
193
- /** The `tasks` record: one entry per canonical task, each defaulting to its
194
- * v1-behavior-preserving schedule. Written explicitly (not via fromEntries) so
195
- * the inferred type stays a precise per-key object. */
389
+ /** The harness-independent task metadata record. Schedule remains the only
390
+ * disable control; runtime gates are not configuration fields. */
196
391
  export const DreamTasksSchema = z
197
392
  .object({
198
393
  "map-memories": DreamTaskBaseConfigSchema.default(() =>
@@ -233,50 +428,59 @@ export const DreamTasksSchema = z
233
428
  ),
234
429
  })
235
430
  .describe(
236
- "Per-task scheduling + model config. Each task has its own cron schedule and may override the dreamer-level model.",
431
+ "Harness-independent task metadata. schedule, promotion_threshold, and other task metadata remain here; execution settings live under dreamer.opencode.tasks or dreamer.pi.tasks.",
237
432
  );
238
433
 
239
- /** Combined dreamer agent + per-task scheduling configuration (Dreamer v2). */
240
- export const DreamerConfigSchema = AgentOverrideConfigSchema.merge(
241
- z.object({
242
- tasks: DreamTasksSchema.default(() => DreamTasksSchema.parse({})),
243
- inject_docs: z
244
- .boolean()
245
- .default(true)
246
- .describe(
247
- "Inject ARCHITECTURE.md and STRUCTURE.md into the m[0] `<project-docs>` block (default true)",
248
- ),
249
- thinking_level: PiThinkingLevelSchema.describe(
250
- "Pi only: default thinking level for dreamer subagent invocations. See historian.thinking_level.",
434
+ const AgentMetadataSchema = AgentOverrideConfigSchema.pick({
435
+ temperature: true,
436
+ top_p: true,
437
+ prompt: true,
438
+ tools: true,
439
+ disable: true,
440
+ description: true,
441
+ mode: true,
442
+ color: true,
443
+ maxSteps: true,
444
+ permission: true,
445
+ maxTokens: true,
446
+ });
447
+
448
+ /** Combined dreamer metadata plus two independent strict execution blocks. */
449
+ export const DreamerConfigSchema = AgentMetadataSchema.extend({
450
+ opencode: DreamerOpenCodeHarnessBlockSchema.optional(),
451
+ pi: DreamerPiHarnessBlockSchema.optional(),
452
+ tasks: DreamTasksSchema.default(() => DreamTasksSchema.parse({})),
453
+ inject_docs: z
454
+ .boolean()
455
+ .default(true)
456
+ .describe(
457
+ "Inject ARCHITECTURE.md and STRUCTURE.md into the m[0] `<project-docs>` block (default true)",
251
458
  ),
252
- }),
253
- );
459
+ });
254
460
  export type DreamerConfig = z.infer<typeof DreamerConfigSchema>;
255
461
 
256
462
  export const SidekickConfigSchema = AgentOverrideConfigSchema.extend({
257
463
  timeout_ms: z.number().default(30000).describe("Timeout for sidekick calls in milliseconds"),
258
464
  system_prompt: z.string().optional().describe("Custom system prompt for sidekick"),
259
465
  thinking_level: PiThinkingLevelSchema.describe(
260
- "Pi only: explicit thinking level for sidekick subagent invocations. See historian.thinking_level.",
466
+ "Pi only: explicit thinking level for sidekick subagent invocations. See historian.pi.thinking_level.",
261
467
  ),
262
468
  }).optional();
263
469
  export type SidekickConfig = NonNullable<z.infer<typeof SidekickConfigSchema>>;
264
470
 
265
- /** Historian agent configuration — includes all agent overrides plus two_pass mode.
266
- * Two-pass mode runs a second editor pass after the initial historian pass to clean
267
- * up low-signal U: lines and cross-compartment duplicates. Recommended for models
268
- * without extended thinking; not needed for Claude Sonnet/Opus when reasoning is
269
- * enabled via OpenCode variant config. */
270
- export const HistorianConfigSchema = AgentOverrideConfigSchema.extend({
471
+ /**
472
+ * Historian metadata remains harness-independent. Only model resolution moves to
473
+ * the strict opencode and pi blocks; two_pass and disallowed_tools stay here.
474
+ */
475
+ export const HistorianConfigSchema = AgentMetadataSchema.extend({
476
+ opencode: OpenCodeHarnessBlockSchema.optional(),
477
+ pi: PiHarnessBlockSchema.optional(),
271
478
  two_pass: z
272
479
  .boolean()
273
480
  .default(false)
274
481
  .describe(
275
482
  "Run a second editor pass over historian output to clean low-signal U: lines and cross-compartment duplicates. Adds ~1 extra API call and ~1.3x cost per historian run. Useful for models without extended thinking support. (default: false)",
276
483
  ),
277
- thinking_level: PiThinkingLevelSchema.describe(
278
- "Pi only: explicit thinking level passed as --thinking <level> to Pi historian subagent invocations. Required when using reasoning models (e.g. github-copilot/gpt-5.4) because Pi's default thinking-level resolution can pick a value the provider rejects. OpenCode users set variant instead. Valid: off | minimal | low | medium | high | xhigh | max",
279
- ),
280
484
  disallowed_tools: z
281
485
  .array(z.enum(["*", "read", "aft_outline", "aft_zoom", "aft_search"]))
282
486
  .default([])
@@ -689,10 +893,10 @@ export const MagicContextConfigSchema = z
689
893
  "original language until naturally rewritten.",
690
894
  ),
691
895
  historian: HistorianConfigSchema.describe(
692
- "Historian agent configuration (model, fallback_models, variant, temperature, maxTokens, permission, two_pass, etc.)",
896
+ "Historian metadata plus independent strict OpenCode and Pi execution blocks. Retained metadata stays at historian; model, fallback_models, variant, and thinking_level belong only in historian.opencode or historian.pi.",
693
897
  ),
694
898
  dreamer: DreamerConfigSchema.optional().describe(
695
- "Dreamer agent + scheduling configuration (model, fallback_models, disable, schedule, tasks, etc.)",
899
+ "Dreamer metadata and scheduling plus independent strict OpenCode and Pi execution blocks. schedule and promotion_threshold stay at dreamer.tasks; model, fallback_models, variant, thinking_level, and timeout_minutes belong only in the matching harness block.",
696
900
  ),
697
901
  smart_notes: z
698
902
  .object({
@@ -8,7 +8,14 @@ import {
8
8
  type Node,
9
9
  type ParseError,
10
10
  parseTree,
11
- } from "jsonc-parser";
11
+ } from "jsonc-parser/lib/esm/main.js";
12
+
13
+ // ^ Deep ESM import on purpose. jsonc-parser has no "exports" map, and its
14
+ // "main" points at a UMD build whose runtime-relative requires (./impl/format
15
+ // etc.) survive bundling verbatim and then fail to resolve from inside a
16
+ // bundled chunk. When this module became reachable from the plugin entry (the
17
+ // config raw-loader), that broken require made the ENTIRE plugin bundle fail
18
+ // to load at boot. The ESM build bundles statically and safely.
12
19
 
13
20
  interface TokenLocation {
14
21
  offset: number;
@@ -252,6 +259,72 @@ export function setJsoncValue(text: string, path: JSONPath, value: unknown): str
252
259
  return applyEdits(text, modify(text, path, value, {}));
253
260
  }
254
261
 
262
+ function removeObjectProperty(text: string, object: Node, key: string): string {
263
+ const properties = object.children ?? [];
264
+ const index = properties.findIndex((property) => {
265
+ const propertyKey = property.children?.[0];
266
+ return propertyKey !== undefined && getNodeValue(propertyKey) === key;
267
+ });
268
+ if (index === -1) return text;
269
+
270
+ const property = properties[index];
271
+ if (!property) return text;
272
+ const closingBrace = object.offset + object.length - 1;
273
+ const next = properties[index + 1];
274
+ if (next) {
275
+ const followingComma = findComma(text, property.offset + property.length, next.offset);
276
+ if (!followingComma) return text;
277
+ // Leading and trailing comments remain in their original positions. A
278
+ // comment previously adjacent to a migrated field therefore survives as
279
+ // standalone JSONC rather than being discarded with the old key.
280
+ return (
281
+ text.slice(0, property.offset) +
282
+ text.slice(followingComma.offset + followingComma.length)
283
+ );
284
+ }
285
+
286
+ const previous = properties[index - 1];
287
+ const trailingComma = findComma(text, property.offset + property.length, closingBrace);
288
+ if (!previous) {
289
+ const afterProperty = property.offset + property.length;
290
+ if (!trailingComma) return text.slice(0, property.offset) + text.slice(afterProperty);
291
+ return (
292
+ text.slice(0, property.offset) +
293
+ text.slice(afterProperty, trailingComma.offset) +
294
+ text.slice(trailingComma.offset + trailingComma.length)
295
+ );
296
+ }
297
+
298
+ const precedingComma = findComma(text, previous.offset + previous.length, property.offset);
299
+ if (!precedingComma) return text;
300
+ const afterProperty = property.offset + property.length;
301
+ const withoutProperty =
302
+ text.slice(0, precedingComma.offset) +
303
+ text.slice(precedingComma.offset + precedingComma.length, property.offset) +
304
+ text.slice(afterProperty);
305
+ if (!trailingComma) return withoutProperty;
306
+
307
+ // The trailing comma position is measured in the original string. Removing
308
+ // only one character before it (the preceding comma) shifts it left by one.
309
+ const shiftedTrailingComma = trailingComma.offset - 1;
310
+ return (
311
+ withoutProperty.slice(0, shiftedTrailingComma) +
312
+ withoutProperty.slice(shiftedTrailingComma + trailingComma.length)
313
+ );
314
+ }
315
+
316
+ /**
317
+ * Delete an object property without reserializing sibling fields. Comments are
318
+ * retained as JSONC trivia so key migrations do not erase a user's notes.
319
+ */
320
+ export function removeJsoncValue(text: string, path: JSONPath): string {
321
+ const key = path.at(-1);
322
+ if (typeof key !== "string") return text;
323
+ const parent = findNode(text, path.slice(0, -1));
324
+ if (parent?.type !== "object") return text;
325
+ return removeObjectProperty(text, parent, key);
326
+ }
327
+
255
328
  /**
256
329
  * Remove matching array entries while retaining the exact bytes for survivor
257
330
  * comments and surrounding JSONC regions.
@@ -0,0 +1,170 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ normalizeModelEntry,
5
+ resolveDreamerTaskModel,
6
+ resolveFallbackEntries,
7
+ resolveHistorianModel,
8
+ } from "./model-resolution";
9
+
10
+ describe("model-resolution", () => {
11
+ test("normalizes string and object entries to the same model identity", () => {
12
+ expect(normalizeModelEntry("anthropic/sonnet", "opencode")).toEqual({
13
+ model: "anthropic/sonnet",
14
+ });
15
+ expect(
16
+ normalizeModelEntry({ model: "anthropic/sonnet", variant: "high" }, "opencode"),
17
+ ).toEqual({ model: "anthropic/sonnet", qualifier: "high" });
18
+ });
19
+
20
+ test("keeps ordered qualifier-distinct fallbacks and removes exact duplicates", () => {
21
+ expect(
22
+ resolveFallbackEntries(
23
+ [
24
+ { model: "anthropic/sonnet", variant: "low" },
25
+ { model: "anthropic/sonnet", variant: "high" },
26
+ { model: "anthropic/sonnet", variant: "low" },
27
+ "google/flash",
28
+ "google/flash",
29
+ ],
30
+ "opencode",
31
+ ),
32
+ ).toEqual([
33
+ { model: "anthropic/sonnet", qualifier: "low" },
34
+ { model: "anthropic/sonnet", qualifier: "high" },
35
+ { model: "google/flash" },
36
+ ]);
37
+ });
38
+
39
+ test("does not inherit a block qualifier into unqualified fallbacks", () => {
40
+ const config = {
41
+ historian: {
42
+ opencode: {
43
+ model: "open/primary",
44
+ fallback_models: ["open/fallback"],
45
+ variant: "primary-only",
46
+ },
47
+ pi: {
48
+ model: "pi/primary",
49
+ fallback_models: ["pi/fallback"],
50
+ thinking_level: "high",
51
+ },
52
+ },
53
+ };
54
+
55
+ // Inheritance is deliberately absent because a fallback model may not support the
56
+ // qualifier selected for the primary model.
57
+ expect(resolveHistorianModel(config, "opencode")).toEqual({
58
+ primary: { model: "open/primary", qualifier: "primary-only" },
59
+ fallbacks: [{ model: "open/fallback" }],
60
+ });
61
+ expect(resolveHistorianModel(config, "pi")).toEqual({
62
+ primary: { model: "pi/primary", qualifier: "high" },
63
+ fallbacks: [{ model: "pi/fallback" }],
64
+ });
65
+ });
66
+
67
+ test("reads historian attempts only from the requested harness", () => {
68
+ const config = {
69
+ historian: {
70
+ model: "flat/ignored",
71
+ opencode: {
72
+ model: { model: "open/code", variant: "oc-primary" },
73
+ fallback_models: [{ model: "open/fallback", variant: "oc-fallback" }],
74
+ },
75
+ pi: {
76
+ model: { model: "pi/model", thinking_level: "high" },
77
+ fallback_models: [{ model: "pi/fallback", thinking_level: "max" }],
78
+ },
79
+ },
80
+ };
81
+
82
+ expect(resolveHistorianModel(config, "opencode")).toEqual({
83
+ primary: { model: "open/code", qualifier: "oc-primary" },
84
+ fallbacks: [{ model: "open/fallback", qualifier: "oc-fallback" }],
85
+ });
86
+ expect(resolveHistorianModel(config, "pi")).toEqual({
87
+ primary: { model: "pi/model", qualifier: "high" },
88
+ fallbacks: [{ model: "pi/fallback", qualifier: "max" }],
89
+ });
90
+ });
91
+
92
+ test("resolves ordinary task model and scheduling without crossing harnesses", () => {
93
+ const config = {
94
+ dreamer: {
95
+ tasks: {
96
+ curate: { schedule: "0 4 * * *", promotion_threshold: 3 },
97
+ },
98
+ opencode: {
99
+ model: { model: "open/default", variant: "default-variant" },
100
+ tasks: {
101
+ curate: {
102
+ model: { model: "open/task", variant: "task-variant" },
103
+ fallback_models: [
104
+ "open/bare",
105
+ { model: "open/qualified", variant: "fb" },
106
+ ],
107
+ timeout_minutes: 12,
108
+ },
109
+ },
110
+ },
111
+ pi: {
112
+ model: { model: "pi/default", thinking_level: "high" },
113
+ tasks: {
114
+ curate: {
115
+ model: { model: "pi/task", thinking_level: "max" },
116
+ fallback_models: [{ model: "pi/fallback", thinking_level: "minimal" }],
117
+ },
118
+ },
119
+ },
120
+ },
121
+ };
122
+
123
+ expect(resolveDreamerTaskModel({ config, harness: "opencode", task: "curate" })).toEqual({
124
+ primary: { model: "open/task", qualifier: "task-variant" },
125
+ fallbacks: [{ model: "open/bare" }, { model: "open/qualified", qualifier: "fb" }],
126
+ schedule: "0 4 * * *",
127
+ timeoutMinutes: 12,
128
+ promotionThreshold: 3,
129
+ });
130
+ });
131
+
132
+ test("uses mural model between compress-cues task and harness default", () => {
133
+ const config = {
134
+ dreamer: {
135
+ tasks: { "compress-cues": { schedule: "0 4 * * *" } },
136
+ opencode: {
137
+ model: { model: "open/default", variant: "default-variant" },
138
+ tasks: { "compress-cues": { variant: "task-local-must-not-leak" } },
139
+ },
140
+ pi: {
141
+ model: { model: "pi/default", thinking_level: "high" },
142
+ tasks: { "compress-cues": {} },
143
+ },
144
+ },
145
+ };
146
+
147
+ expect(
148
+ resolveDreamerTaskModel({
149
+ config,
150
+ harness: "opencode",
151
+ task: "compress-cues",
152
+ muralModel: "mural/model",
153
+ }),
154
+ ).toMatchObject({
155
+ primary: { model: "mural/model", qualifier: "default-variant" },
156
+ fallbacks: [],
157
+ });
158
+ expect(
159
+ resolveDreamerTaskModel({
160
+ config,
161
+ harness: "pi",
162
+ task: "compress-cues",
163
+ muralModel: "mural/model",
164
+ }),
165
+ ).toMatchObject({
166
+ primary: { model: "mural/model", qualifier: "high" },
167
+ fallbacks: [],
168
+ });
169
+ });
170
+ });