@adhdev/daemon-core 0.9.82-rc.436 → 0.9.82-rc.438

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.
@@ -72,6 +72,14 @@ export interface MeshWorkQueueEntry {
72
72
  * replicas. Absent on ordinary tasks. Rides in the payload JSON (no column).
73
73
  */
74
74
  consensusGroupId?: string;
75
+ /**
76
+ * MAGI-KIND-PANEL (model axis): model override for the session that executes this
77
+ * task. When the task auto-launches a session, this is passed to launch_cli as
78
+ * `initialModel` (ACP → setConfigOption; CLI → modelLaunchArgs template). Absent on
79
+ * ordinary tasks. Rides in the payload JSON (no column). Best-effort — a provider
80
+ * that cannot honor the model still runs the task (never a fatal launch error).
81
+ */
82
+ model?: string;
75
83
  /**
76
84
  * M1: why this task is held back (e.g. "dependency_failed:<taskId>").
77
85
  * Only set by the system on dependency failure under the 'block' policy;
@@ -186,6 +194,8 @@ export declare function enqueueTask(meshId: string, message: string, opts?: {
186
194
  missionId?: string;
187
195
  /** MAGI: consensus group id shared by every replica of a mesh_magi_review fan-out. */
188
196
  consensusGroupId?: string;
197
+ /** MAGI-KIND-PANEL: model override forwarded to the executing session's launch (initialModel). */
198
+ model?: string;
189
199
  /** Explicit task id for batch/template flows (M5). Random UUID when omitted. */
190
200
  id?: string;
191
201
  /** (3) Originating coordinator session id (for session-anchored completion routing). */
@@ -489,6 +489,16 @@ export interface ProviderModule {
489
489
  /** Auto-implement spawn config — controls how this provider is invoked for autonomous script generation */
490
490
  autoImpl?: ProviderAutoImplSpawnConfig;
491
491
  };
492
+ /**
493
+ * MAGI-KIND-PANEL (model axis): template for expanding an `initialModel` selection
494
+ * into launch args for a CLI provider. `{{model}}` is substituted with the model
495
+ * string; e.g. `['--model', '{{model}}']` for claude-cli → `--model opus`. Applied
496
+ * at session launch when `initialModel` is passed AND this provider is a plain CLI
497
+ * (ACP providers instead route the model through setConfigOption). A CLI provider
498
+ * with no template silently ignores `initialModel` at launch (best-effort; a model
499
+ * request never fails a launch). Absent → no launch-time model selection for CLI.
500
+ */
501
+ modelLaunchArgs?: string[];
492
502
  /** Delay before submitting typed CLI input (provider-specific TUI tuning) */
493
503
  sendDelayMs?: number;
494
504
  /** Submit key used after typing into CLI PTY (default: carriage return) */
@@ -13,7 +13,7 @@
13
13
  import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
14
14
  import type { MeshMissionSummary, MeshMissionSlimSummary } from './mesh/mesh-missions.js';
15
15
  import type { MeshMagiActivitySummary } from './mesh/mesh-magi-status.js';
16
- import type { MagiPanelMap } from '@adhdev/mesh-shared';
16
+ import type { MagiPanelMap, MagiKindPanelMap } from '@adhdev/mesh-shared';
17
17
  export interface RepoMesh {
18
18
  id: string;
19
19
  name: string;
@@ -536,6 +536,14 @@ export interface LocalMeshConfig {
536
536
  * Optional: absent on configs written before MAGI existed.
537
537
  */
538
538
  magiPanels?: MagiPanelMap;
539
+ /**
540
+ * MAGI-KIND-PANEL: per-task_kind panel bindings (machine-local). Keyed by
541
+ * task_kind (rca / design / claim_audit / freeform); each maps to ≥1
542
+ * `(node × provider × model?)` slot. A `mesh_magi_review` invoked with a bare
543
+ * `task_kind` resolves its panel from here — an unconfigured kind is a hard
544
+ * error, never a synthesized fallback. Optional; absent on pre-feature configs.
545
+ */
546
+ magiKindPanels?: MagiKindPanelMap;
539
547
  }
540
548
  export interface LocalMeshEntry {
541
549
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.436",
3
+ "version": "0.9.82-rc.438",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.436",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.438",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -369,6 +369,18 @@ function expandResumeArgs(template: string[] | undefined, sessionId: string): st
369
369
  return template.map((part) => part === '{{id}}' ? sessionId : part);
370
370
  }
371
371
 
372
+ /**
373
+ * Expand a provider's `modelLaunchArgs` template with the requested model, mirroring
374
+ * expandResumeArgs. `{{model}}` → the trimmed model string. Returns undefined when
375
+ * there is no template or no model (a model request without a template is a no-op —
376
+ * see startSession, where the caller logs the skip). MAGI kind-panel model axis.
377
+ */
378
+ function expandModelLaunchArgs(template: string[] | undefined, model: string | undefined): string[] | undefined {
379
+ const m = typeof model === 'string' ? model.trim() : '';
380
+ if (!m || !Array.isArray(template) || template.length === 0) return undefined;
381
+ return template.map((part) => part === '{{model}}' ? m : part);
382
+ }
383
+
372
384
  function readSubcommandSessionId(args: string[], subcommands: string[]): string | undefined {
373
385
  const resumeIndex = args.findIndex((arg) => subcommands.includes(arg));
374
386
  if (resumeIndex < 0) return undefined;
@@ -895,8 +907,23 @@ export class DaemonCliManager {
895
907
  console.log(colorize('cyan', ` 📦 Using provider: ${provider.name} (${provider.type})`));
896
908
  }
897
909
 
910
+ // ─── Model axis (MAGI kind-panel): expand initialModel → launch args ───
911
+ // For a plain CLI provider the model is selected at spawn time via the manifest's
912
+ // modelLaunchArgs template ('{{model}}' → the requested model). ACP providers took
913
+ // the setConfigOption path above and never reach here. A provider with no template,
914
+ // or no requested model, is a no-op — model selection is best-effort and must never
915
+ // fail a launch. The model args are prepended so a caller's explicit cliArgs (e.g. a
916
+ // resume flag) still win positionally where order matters.
917
+ const modelLaunchArgs = expandModelLaunchArgs(provider?.modelLaunchArgs, initialModel);
918
+ const cliArgsWithModel = modelLaunchArgs
919
+ ? [...modelLaunchArgs, ...(cliArgs || [])]
920
+ : cliArgs;
921
+ if (initialModel && !modelLaunchArgs) {
922
+ LOG.warn('CLI', `[${normalizedType}] initialModel='${initialModel}' requested but provider declares no modelLaunchArgs template — launching without model selection.`);
923
+ }
924
+
898
925
  // ─── Resolve launch options → provider session binding ───
899
- const sessionBinding = resolveCliSessionBinding(provider, normalizedType, cliArgs, options?.resumeSessionId);
926
+ const sessionBinding = resolveCliSessionBinding(provider, normalizedType, cliArgsWithModel, options?.resumeSessionId);
900
927
  const resolvedCliArgs = sessionBinding.cliArgs;
901
928
 
902
929
  // If InstanceManager exists, manage as CliProviderInstance unified
@@ -302,6 +302,48 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
302
302
  }
303
303
  },
304
304
 
305
+ // ─── MAGI kind → panel bindings (MAGI-KIND-PANEL, machine-local config) ───
306
+ // Per-task_kind slot lists in ~/.adhdev/meshes.json `magiKindPanels`. Same
307
+ // owner-only gating and structured-error precedent as the magi_panel_* handlers
308
+ // above (not listed in canPeerUsePrivilegedShareCommand → owner-only). set/remove
309
+ // are WRITE commands; list is read-only. normalizeMagiSlots (inside setMagiKindPanel)
310
+ // surfaces invalid_magi_kind_panel: … messages verbatim for the editor.
311
+ magi_kind_panel_list: async (_ctx: MedFamilyContext, _args: any) => {
312
+ try {
313
+ const { listMagiKindPanels } = await import('../../config/mesh-config.js');
314
+ return { success: true, kindPanels: listMagiKindPanels() };
315
+ } catch (e: any) {
316
+ return { success: false, error: e.message };
317
+ }
318
+ },
319
+
320
+ magi_kind_panel_set: async (_ctx: MedFamilyContext, args: any) => {
321
+ const kind = typeof args?.kind === 'string' ? args.kind.trim() : '';
322
+ if (!kind) return { success: false, error: 'invalid_magi_kind_panel: task_kind is required' };
323
+ try {
324
+ const { setMagiKindPanel } = await import('../../config/mesh-config.js');
325
+ // normalizeMagiTaskKindKey + normalizeMagiSlots (inside setMagiKindPanel)
326
+ // validate the kind and each slot (provider required; model/nodeId optional;
327
+ // replica counts clamped). Structured errors flow back as `error`.
328
+ const slots = setMagiKindPanel(kind, args?.slots);
329
+ return { success: true, kind, slots };
330
+ } catch (e: any) {
331
+ return { success: false, error: e.message };
332
+ }
333
+ },
334
+
335
+ magi_kind_panel_remove: async (_ctx: MedFamilyContext, args: any) => {
336
+ const kind = typeof args?.kind === 'string' ? args.kind.trim() : '';
337
+ if (!kind) return { success: false, error: 'invalid_magi_kind_panel: task_kind is required' };
338
+ try {
339
+ const { removeMagiKindPanel } = await import('../../config/mesh-config.js');
340
+ const removed = removeMagiKindPanel(kind);
341
+ return { success: true, removed };
342
+ } catch (e: any) {
343
+ return { success: false, error: e.message };
344
+ }
345
+ },
346
+
305
347
  add_mesh_node: async (ctx: MedFamilyContext, args: any) => {
306
348
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
307
349
  const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
@@ -22,7 +22,7 @@ import type {
22
22
  RepoMeshHostMetadata,
23
23
  RepoMeshDaemonRole,
24
24
  } from '../repo-mesh-types.js';
25
- import type { MagiPanel, MagiPanelMember, MagiPanelDefaultKind } from '@adhdev/mesh-shared';
25
+ import type { MagiPanel, MagiPanelMember, MagiPanelDefaultKind, MagiKindPanelMap, MagiSlot, MagiTaskKind } from '@adhdev/mesh-shared';
26
26
  import { mergeAndNormalizePolicy } from '../repo-mesh-types.js';
27
27
  import { createDefaultMeshHostMetadata } from '../mesh/mesh-host-ownership.js';
28
28
 
@@ -650,11 +650,13 @@ export function normalizeMagiPanel(config: unknown): MagiPanel {
650
650
  throw new Error(`invalid_magi_panel: member[${idx}].provider is required`);
651
651
  }
652
652
  const nodeId = typeof m.nodeId === 'string' && m.nodeId.trim() ? m.nodeId.trim() : undefined;
653
+ const model = typeof m.model === 'string' && m.model.trim() ? m.model.trim() : undefined;
653
654
  const capabilityTags = normalizeCapabilityTags(m.capabilityTags);
654
655
  const n = normalizeReplicaCount(m.n);
655
656
  return {
656
657
  provider,
657
658
  ...(nodeId ? { nodeId } : {}),
659
+ ...(model ? { model } : {}),
658
660
  ...(capabilityTags ? { capabilityTags } : {}),
659
661
  ...(n !== undefined ? { n } : {}),
660
662
  };
@@ -726,3 +728,99 @@ export function removeMagiPanel(name: string): boolean {
726
728
  saveMeshConfig(stored);
727
729
  return true;
728
730
  }
731
+
732
+ // ─── MAGI kind → panel bindings (MAGI-KIND-PANEL) ─────────
733
+ //
734
+ // Per-task_kind slot lists (machine-local, meshes.json `magiKindPanels`). A bare
735
+ // `mesh_magi_review({task_kind})` resolves its panel exclusively from here — an
736
+ // unconfigured kind is a hard error, never a synthesized fallback. Mirrors the named
737
+ // panel accessors above (normalize / list / get / set / remove).
738
+
739
+ /** The task kinds a kind-panel can be bound to. Unlike a named panel's defaultKind,
740
+ * 'freeform' IS a valid kind-panel key (this is a direct kind→slots binding). */
741
+ const MAGI_KIND_PANEL_KINDS: readonly MagiTaskKind[] = ['claim_audit', 'rca', 'design', 'freeform'];
742
+ const MAX_MAGI_KIND_SLOTS = 24;
743
+
744
+ function normalizeMagiTaskKindKey(raw: unknown): MagiTaskKind {
745
+ const s = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
746
+ if (!(MAGI_KIND_PANEL_KINDS as readonly string[]).includes(s)) {
747
+ throw new Error(`invalid_magi_kind_panel: task_kind must be one of ${MAGI_KIND_PANEL_KINDS.join(' / ')} (got '${s || '(empty)'}')`);
748
+ }
749
+ return s as MagiTaskKind;
750
+ }
751
+
752
+ /**
753
+ * Validate + normalize a kind-panel's slots. Mirrors normalizeMagiPanel's member
754
+ * normalization: provider required per slot, trims strings, drops empties, clamps
755
+ * replica counts, and additionally carries an optional per-slot `model`. Throws on
756
+ * structurally invalid input (empty list / no provider) so the write returns a clear
757
+ * error. Returns the normalized slot array.
758
+ */
759
+ export function normalizeMagiSlots(slots: unknown): MagiSlot[] {
760
+ if (!Array.isArray(slots) || slots.length === 0) {
761
+ throw new Error('invalid_magi_kind_panel: slots must be a non-empty array');
762
+ }
763
+ if (slots.length > MAX_MAGI_KIND_SLOTS) {
764
+ throw new Error(`invalid_magi_kind_panel: too many slots (max ${MAX_MAGI_KIND_SLOTS})`);
765
+ }
766
+ return slots.map((entry, idx) => {
767
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
768
+ throw new Error(`invalid_magi_kind_panel: slot[${idx}] must be an object`);
769
+ }
770
+ const s = entry as Record<string, unknown>;
771
+ const provider = typeof s.provider === 'string' ? s.provider.trim() : '';
772
+ if (!provider) {
773
+ throw new Error(`invalid_magi_kind_panel: slot[${idx}].provider is required`);
774
+ }
775
+ const nodeId = typeof s.nodeId === 'string' && s.nodeId.trim() ? s.nodeId.trim() : undefined;
776
+ const model = typeof s.model === 'string' && s.model.trim() ? s.model.trim() : undefined;
777
+ const capabilityTags = normalizeCapabilityTags(s.capabilityTags);
778
+ const n = normalizeReplicaCount(s.n);
779
+ return {
780
+ provider,
781
+ ...(nodeId ? { nodeId } : {}),
782
+ ...(model ? { model } : {}),
783
+ ...(capabilityTags ? { capabilityTags } : {}),
784
+ ...(n !== undefined ? { n } : {}),
785
+ };
786
+ });
787
+ }
788
+
789
+ /** All configured kind-panels (machine-local), keyed by task_kind. Empty when none. */
790
+ export function listMagiKindPanels(): MagiKindPanelMap {
791
+ return loadMeshConfig().magiKindPanels ?? {};
792
+ }
793
+
794
+ /** The slot list for one task_kind, or undefined when the kind is not configured. */
795
+ export function getMagiKindPanel(kind: string): MagiSlot[] | undefined {
796
+ let key: MagiTaskKind;
797
+ try { key = normalizeMagiTaskKindKey(kind); } catch { return undefined; }
798
+ return loadMeshConfig().magiKindPanels?.[key];
799
+ }
800
+
801
+ /**
802
+ * Upsert the slot list for one task_kind. Unlike named panels this ALWAYS overwrites
803
+ * (a kind has exactly one binding) — the editor pushes the full desired slot set.
804
+ * Returns the normalized, persisted slots.
805
+ */
806
+ export function setMagiKindPanel(kind: string, slots: unknown): MagiSlot[] {
807
+ const key = normalizeMagiTaskKindKey(kind);
808
+ const normalized = normalizeMagiSlots(slots);
809
+ const stored = loadMeshConfig();
810
+ const map = stored.magiKindPanels ?? {};
811
+ map[key] = normalized;
812
+ stored.magiKindPanels = map;
813
+ saveMeshConfig(stored);
814
+ return normalized;
815
+ }
816
+
817
+ /** Remove the binding for one task_kind. Returns true when a binding was removed. */
818
+ export function removeMagiKindPanel(kind: string): boolean {
819
+ let key: MagiTaskKind;
820
+ try { key = normalizeMagiTaskKindKey(kind); } catch { return false; }
821
+ const stored = loadMeshConfig();
822
+ if (!stored.magiKindPanels || !stored.magiKindPanels[key]) return false;
823
+ delete stored.magiKindPanels[key];
824
+ saveMeshConfig(stored);
825
+ return true;
826
+ }
package/src/index.ts CHANGED
@@ -196,6 +196,7 @@ export {
196
196
  listMeshes, getMesh, getMeshByRepo, createMesh, updateMesh, deleteMesh,
197
197
  addNode, removeNode, updateNode, normalizeRepoIdentity,
198
198
  listMagiPanels, getMagiPanel, upsertMagiPanel, removeMagiPanel, normalizeMagiPanel,
199
+ listMagiKindPanels, getMagiKindPanel, setMagiKindPanel, removeMagiKindPanel, normalizeMagiSlots,
199
200
  } from './config/mesh-config.js';
200
201
  export type { CreateMeshOptions, UpdateMeshOptions, AddNodeOptions } from './config/mesh-config.js';
201
202
  // MAGI panel / common-output / synthesis types (re-exported from the mesh-shared
@@ -1538,6 +1538,9 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
1538
1538
  cliType: resolved.providerType,
1539
1539
  dir: node.workspace,
1540
1540
  settings: remoteSettings,
1541
+ // MAGI-KIND-PANEL model axis: forward the task's model override so the
1542
+ // remote worker session launches with it (initialModel). Best-effort.
1543
+ ...(typeof task.model === 'string' && task.model.trim() ? { initialModel: task.model.trim() } : {}),
1541
1544
  });
1542
1545
  } catch (e: any) {
1543
1546
  markAutoLaunch(meshId, task.id, { status: 'failed', reason: `remote_launch_dispatch_failed: ${e?.message || String(e)}`, nodeId, providerType: resolved.providerType });
@@ -1567,6 +1570,9 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
1567
1570
  cliType: resolved.providerType,
1568
1571
  dir: node.workspace,
1569
1572
  settings: launchSettings,
1573
+ // MAGI-KIND-PANEL model axis: local launch forwards the task's model
1574
+ // override as initialModel (CLI → modelLaunchArgs; ACP → setConfigOption).
1575
+ ...(typeof task.model === 'string' && task.model.trim() ? { initialModel: task.model.trim() } : {}),
1570
1576
  });
1571
1577
  if (!launchResult?.success) {
1572
1578
  const reason = launchResult?.error || 'launch_cli_failed';
@@ -502,6 +502,14 @@ export interface MeshWorkQueueEntry {
502
502
  * replicas. Absent on ordinary tasks. Rides in the payload JSON (no column).
503
503
  */
504
504
  consensusGroupId?: string;
505
+ /**
506
+ * MAGI-KIND-PANEL (model axis): model override for the session that executes this
507
+ * task. When the task auto-launches a session, this is passed to launch_cli as
508
+ * `initialModel` (ACP → setConfigOption; CLI → modelLaunchArgs template). Absent on
509
+ * ordinary tasks. Rides in the payload JSON (no column). Best-effort — a provider
510
+ * that cannot honor the model still runs the task (never a fatal launch error).
511
+ */
512
+ model?: string;
505
513
  /**
506
514
  * M1: why this task is held back (e.g. "dependency_failed:<taskId>").
507
515
  * Only set by the system on dependency failure under the 'block' policy;
@@ -772,6 +780,8 @@ export function enqueueTask(
772
780
  missionId?: string;
773
781
  /** MAGI: consensus group id shared by every replica of a mesh_magi_review fan-out. */
774
782
  consensusGroupId?: string;
783
+ /** MAGI-KIND-PANEL: model override forwarded to the executing session's launch (initialModel). */
784
+ model?: string;
775
785
  /** Explicit task id for batch/template flows (M5). Random UUID when omitted. */
776
786
  id?: string;
777
787
  /** (3) Originating coordinator session id (for session-anchored completion routing). */
@@ -816,6 +826,7 @@ export function enqueueTask(
816
826
  ...(dependsOn.length > 0 ? { dependsOn } : {}),
817
827
  ...(typeof opts?.missionId === 'string' && opts.missionId.trim() ? { missionId: opts.missionId.trim() } : {}),
818
828
  ...(typeof opts?.consensusGroupId === 'string' && opts.consensusGroupId.trim() ? { consensusGroupId: opts.consensusGroupId.trim() } : {}),
829
+ ...(typeof opts?.model === 'string' && opts.model.trim() ? { model: opts.model.trim() } : {}),
819
830
  ...(typeof opts?.sourceCoordinatorSessionId === 'string' && opts.sourceCoordinatorSessionId.trim()
820
831
  ? { sourceCoordinatorSessionId: opts.sourceCoordinatorSessionId.trim() }
821
832
  : {}),
@@ -594,6 +594,16 @@ export interface ProviderModule {
594
594
  /** Auto-implement spawn config — controls how this provider is invoked for autonomous script generation */
595
595
  autoImpl?: ProviderAutoImplSpawnConfig;
596
596
  };
597
+ /**
598
+ * MAGI-KIND-PANEL (model axis): template for expanding an `initialModel` selection
599
+ * into launch args for a CLI provider. `{{model}}` is substituted with the model
600
+ * string; e.g. `['--model', '{{model}}']` for claude-cli → `--model opus`. Applied
601
+ * at session launch when `initialModel` is passed AND this provider is a plain CLI
602
+ * (ACP providers instead route the model through setConfigOption). A CLI provider
603
+ * with no template silently ignores `initialModel` at launch (best-effort; a model
604
+ * request never fails a launch). Absent → no launch-time model selection for CLI.
605
+ */
606
+ modelLaunchArgs?: string[];
597
607
  /** Delay before submitting typed CLI input (provider-specific TUI tuning) */
598
608
  sendDelayMs?: number;
599
609
  /** Submit key used after typing into CLI PTY (default: carriage return) */
@@ -124,6 +124,11 @@
124
124
  "minimum": 0,
125
125
  "description": "Delay between pasting prompt text and pressing Enter."
126
126
  },
127
+ "modelLaunchArgs": {
128
+ "type": "array",
129
+ "items": { "type": "string" },
130
+ "description": "Template for expanding an initialModel selection into launch args. '{{model}}' is substituted with the model string (e.g. ['--model', '{{model}}'] → --model opus). Applied at launch when a model is requested for this CLI provider (MAGI kind-panel model axis). Absent → no launch-time model selection."
131
+ },
127
132
  "scriptCallBudgetMs": {
128
133
  "type": "integer",
129
134
  "minimum": 1,
@@ -14,7 +14,7 @@
14
14
  import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
15
15
  import type { MeshMissionSummary, MeshMissionSlimSummary } from './mesh/mesh-missions.js';
16
16
  import type { MeshMagiActivitySummary } from './mesh/mesh-magi-status.js';
17
- import type { MagiPanelMap } from '@adhdev/mesh-shared';
17
+ import type { MagiPanelMap, MagiKindPanelMap } from '@adhdev/mesh-shared';
18
18
 
19
19
  // ─── Core Mesh Types ────────────────────────────
20
20
 
@@ -803,6 +803,14 @@ export interface LocalMeshConfig {
803
803
  * Optional: absent on configs written before MAGI existed.
804
804
  */
805
805
  magiPanels?: MagiPanelMap;
806
+ /**
807
+ * MAGI-KIND-PANEL: per-task_kind panel bindings (machine-local). Keyed by
808
+ * task_kind (rca / design / claim_audit / freeform); each maps to ≥1
809
+ * `(node × provider × model?)` slot. A `mesh_magi_review` invoked with a bare
810
+ * `task_kind` resolves its panel from here — an unconfigured kind is a hard
811
+ * error, never a synthesized fallback. Optional; absent on pre-feature configs.
812
+ */
813
+ magiKindPanels?: MagiKindPanelMap;
806
814
  }
807
815
 
808
816
  export interface LocalMeshEntry {