@adhdev/daemon-core 0.9.82-rc.437 → 0.9.82-rc.439

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.
@@ -4,7 +4,13 @@
4
4
  * One-shot helper that onboards an existing git project into Repo Mesh:
5
5
  * 1. suggest + write `.adhdev/refine.json` (Refinery validation config)
6
6
  * 2. suggest + write `.adhdev/worktree_bootstrap.json` (worktree bootstrap config)
7
- * 3. recommend a node providerPriority from the installed CLI providers
7
+ * 3. suggest + write `.adhdev/change-impact.json` (change-impact classification)
8
+ * 4. recommend a node providerPriority from the installed CLI providers
9
+ *
10
+ * mesh_init now covers all THREE repo-committed `.adhdev/*` config families that
11
+ * carry a suggest→validate→write engine (refine + worktree_bootstrap +
12
+ * change-impact). `.adhdev/mesh.json` (coordinator prompt + operating notes) has
13
+ * its own dedicated export/write path (export_mesh_json_config / write_mesh_json_config).
8
14
  *
9
15
  * Design contract (matches the rest of the mesh tooling):
10
16
  * - Heuristics are suggestion/scaffold only. The written files are the
@@ -19,17 +25,20 @@
19
25
  */
20
26
  import { type RepoMeshRefineConfig, type RepoMeshRefineValidationCommandConfig } from './refine-config.js';
21
27
  import { type RepoMeshWorktreeBootstrapConfig } from './worktree-bootstrap-config.js';
28
+ import { type ChangeImpactConfig } from '../git/change-impact-config.js';
29
+ import type { MagiKindPanelMap } from '@adhdev/mesh-shared';
22
30
  import type { CLIInfo } from '../detection/cli-detector.js';
23
31
  /** Canonical write targets — the first/preferred location for each config family. */
24
32
  export declare const MESH_INIT_REFINE_CONFIG_PATH: string;
25
33
  export declare const MESH_INIT_WORKTREE_BOOTSTRAP_CONFIG_PATH: string;
34
+ export declare const MESH_INIT_CHANGE_IMPACT_CONFIG_PATH: string;
26
35
  export interface MeshInitConfigFileResult {
27
36
  path: string;
28
37
  relativePath: string;
29
38
  written: boolean;
30
39
  /** Why it was not written (already_exists / not_suggested / no change). */
31
40
  skippedReason?: 'already_exists' | 'no_suggestion';
32
- config?: RepoMeshRefineConfig | RepoMeshWorktreeBootstrapConfig;
41
+ config?: RepoMeshRefineConfig | RepoMeshWorktreeBootstrapConfig | ChangeImpactConfig;
33
42
  }
34
43
  /**
35
44
  * Build a worktree_bootstrap scaffold from package scripts. Mirrors
@@ -58,12 +67,39 @@ export interface RunMeshInitOptions {
58
67
  /** When true, overwrite an existing config file. Defaults to false (never clobber). */
59
68
  overwrite?: boolean;
60
69
  }
70
+ /**
71
+ * A snapshot of the currently-saved config for each domain, so the coordinator can
72
+ * render a current-vs-suggested diff to the user before an init/reinit overwrite.
73
+ * This is READ-ONLY echo — every field is what is on disk / machine-local right now,
74
+ * never a suggestion. Absent domains are reported as `undefined` config.
75
+ */
76
+ export interface MeshInitCurrentConfigEcho {
77
+ /** Currently-saved `.adhdev/refine.json` (repo-committed) — undefined when absent/invalid. */
78
+ refine?: RepoMeshRefineConfig;
79
+ /** Currently-saved `.adhdev/worktree_bootstrap.json` (repo-committed). */
80
+ worktreeBootstrap?: RepoMeshWorktreeBootstrapConfig;
81
+ /** Currently-saved `.adhdev/change-impact.json` (repo-committed). */
82
+ changeImpact?: ChangeImpactConfig;
83
+ /** Per-domain source type so the coordinator can tell "absent" from "invalid". */
84
+ sourceTypes: {
85
+ refine: string;
86
+ worktreeBootstrap: string;
87
+ changeImpact: string;
88
+ };
89
+ /**
90
+ * Currently-configured MAGI kind→panel slot bindings (machine-local
91
+ * ~/.adhdev/meshes.json). Empty object when none configured. This is a
92
+ * machine-local echo — the coordinator labels it as such vs. the repo files.
93
+ */
94
+ magiKindPanels: MagiKindPanelMap;
95
+ }
61
96
  export interface RunMeshInitResult {
62
97
  success: true;
63
98
  workspace: string;
64
99
  dryRun: boolean;
65
100
  refine: MeshInitConfigFileResult;
66
101
  worktreeBootstrap: MeshInitConfigFileResult;
102
+ changeImpact: MeshInitConfigFileResult;
67
103
  providers: {
68
104
  providerPriority: string[];
69
105
  installedProviders: Array<{
@@ -72,6 +108,12 @@ export interface RunMeshInitResult {
72
108
  version?: string;
73
109
  }>;
74
110
  };
111
+ /**
112
+ * Read-only snapshot of the currently-saved config per domain (repo files +
113
+ * machine-local kind panels). Lets the coordinator present a current-vs-suggested
114
+ * diff to the user before any overwrite. Always populated (dry-run and write).
115
+ */
116
+ currentConfig: MeshInitCurrentConfigEcho;
75
117
  note: string;
76
118
  }
77
119
  /**
@@ -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) */
@@ -26,5 +26,5 @@ export interface NativeHistorySessionMeta {
26
26
  preview?: string;
27
27
  workspace?: string;
28
28
  }
29
- export declare function readSession(sessionPath: string): NativeHistorySession | null;
29
+ export declare function readSession(sessionPath: string, requestedSessionId?: string): NativeHistorySession | null;
30
30
  export declare function listSessions(_watchPath: string): Promise<NativeHistorySessionMeta[]>;
@@ -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.437",
3
+ "version": "0.9.82-rc.439",
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.437",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.439",
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
@@ -232,6 +232,95 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
232
232
  }
233
233
  },
234
234
 
235
+ // Gated WRITE path for `.adhdev/mesh.json` — the sibling of export_mesh_json_config
236
+ // (which only DRAFTS). Same write/overwrite/dry-run contract as mesh_init's config
237
+ // writer: defaults to dry-run (no write), never clobbers an existing repo mesh.json
238
+ // unless overwrite=true, and validates the scaffold before persisting. The scaffold
239
+ // is built from the machine-local mesh entry (coordinator prompt override/append);
240
+ // policy/operating-notes are intentionally NOT exported (see buildMeshJsonConfigScaffold).
241
+ write_mesh_json_config: async (_ctx: MedFamilyContext, args: any) => {
242
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
243
+ if (!meshId) return { success: false, error: 'meshId required' };
244
+ const workspace = typeof args?.workspace === 'string' && args.workspace.trim() ? args.workspace.trim() : process.cwd();
245
+ const write = args?.write === true;
246
+ const overwrite = args?.overwrite === true;
247
+ try {
248
+ const { getMesh } = await import('../../config/mesh-config.js');
249
+ const mesh = getMesh(meshId);
250
+ if (!mesh) return { success: false, error: 'Mesh not found' };
251
+ const {
252
+ buildMeshJsonConfigScaffold,
253
+ serializeMeshJsonConfigScaffold,
254
+ loadRepoMeshJsonConfig,
255
+ normalizeRepoMeshDeclarativeConfig,
256
+ MESH_JSON_CONFIG_LOCATIONS,
257
+ } = await import('../../config/mesh-json-config.js');
258
+ const { mkdirSync, writeFileSync } = await import('fs');
259
+ const { dirname, join } = await import('path');
260
+
261
+ const scaffold = buildMeshJsonConfigScaffold(mesh);
262
+ const scaffoldJson = serializeMeshJsonConfigScaffold(scaffold);
263
+ const relativePath = MESH_JSON_CONFIG_LOCATIONS[0];
264
+ const absolutePath = join(workspace, relativePath);
265
+
266
+ // Validate before we ever touch disk — never write an unusable mesh.json.
267
+ const validation = normalizeRepoMeshDeclarativeConfig(scaffold);
268
+ if (!validation.valid) {
269
+ return { success: false, meshId, error: `invalid mesh.json scaffold: ${validation.errors.join('; ')}` };
270
+ }
271
+
272
+ // existing-wins: a repo mesh.json already present is kept unless overwrite=true.
273
+ const existing = loadRepoMeshJsonConfig(workspace);
274
+ const existingPresent = existing.sourceType === 'repo_file' || existing.sourceType === 'invalid';
275
+ if (existingPresent && !overwrite) {
276
+ return {
277
+ success: true,
278
+ meshId,
279
+ written: false,
280
+ dryRun: !write,
281
+ skippedReason: 'already_exists',
282
+ path: absolutePath,
283
+ relativePath,
284
+ existing: existing.config,
285
+ existingSourceType: existing.sourceType,
286
+ scaffold,
287
+ scaffoldJson,
288
+ note: 'A repo mesh.json already exists — kept as-is. Re-run with overwrite=true to replace it (this silently drops operator hand-edits, so present a current-vs-suggested diff first).',
289
+ };
290
+ }
291
+
292
+ if (!write) {
293
+ return {
294
+ success: true,
295
+ meshId,
296
+ written: false,
297
+ dryRun: true,
298
+ path: absolutePath,
299
+ relativePath,
300
+ scaffold,
301
+ scaffoldJson,
302
+ note: 'Dry-run: nothing written. Re-run with write=true to persist to the repo (commit target). meshes.json is untouched.',
303
+ };
304
+ }
305
+
306
+ mkdirSync(dirname(absolutePath), { recursive: true });
307
+ writeFileSync(absolutePath, `${scaffoldJson}\n`, 'utf-8');
308
+ return {
309
+ success: true,
310
+ meshId,
311
+ written: true,
312
+ dryRun: false,
313
+ path: absolutePath,
314
+ relativePath,
315
+ scaffold,
316
+ scaffoldJson,
317
+ note: 'Wrote .adhdev/mesh.json (repo commit target). Commit it to the repo; meshes.json (machine-local) is unchanged.',
318
+ };
319
+ } catch (e: any) {
320
+ return { success: false, error: e.message };
321
+ }
322
+ },
323
+
235
324
  delete_mesh: async (_ctx: MedFamilyContext, args: any) => {
236
325
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
237
326
  if (!meshId) return { success: false, error: 'meshId required' };
@@ -302,6 +391,48 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
302
391
  }
303
392
  },
304
393
 
394
+ // ─── MAGI kind → panel bindings (MAGI-KIND-PANEL, machine-local config) ───
395
+ // Per-task_kind slot lists in ~/.adhdev/meshes.json `magiKindPanels`. Same
396
+ // owner-only gating and structured-error precedent as the magi_panel_* handlers
397
+ // above (not listed in canPeerUsePrivilegedShareCommand → owner-only). set/remove
398
+ // are WRITE commands; list is read-only. normalizeMagiSlots (inside setMagiKindPanel)
399
+ // surfaces invalid_magi_kind_panel: … messages verbatim for the editor.
400
+ magi_kind_panel_list: async (_ctx: MedFamilyContext, _args: any) => {
401
+ try {
402
+ const { listMagiKindPanels } = await import('../../config/mesh-config.js');
403
+ return { success: true, kindPanels: listMagiKindPanels() };
404
+ } catch (e: any) {
405
+ return { success: false, error: e.message };
406
+ }
407
+ },
408
+
409
+ magi_kind_panel_set: async (_ctx: MedFamilyContext, args: any) => {
410
+ const kind = typeof args?.kind === 'string' ? args.kind.trim() : '';
411
+ if (!kind) return { success: false, error: 'invalid_magi_kind_panel: task_kind is required' };
412
+ try {
413
+ const { setMagiKindPanel } = await import('../../config/mesh-config.js');
414
+ // normalizeMagiTaskKindKey + normalizeMagiSlots (inside setMagiKindPanel)
415
+ // validate the kind and each slot (provider required; model/nodeId optional;
416
+ // replica counts clamped). Structured errors flow back as `error`.
417
+ const slots = setMagiKindPanel(kind, args?.slots);
418
+ return { success: true, kind, slots };
419
+ } catch (e: any) {
420
+ return { success: false, error: e.message };
421
+ }
422
+ },
423
+
424
+ magi_kind_panel_remove: async (_ctx: MedFamilyContext, args: any) => {
425
+ const kind = typeof args?.kind === 'string' ? args.kind.trim() : '';
426
+ if (!kind) return { success: false, error: 'invalid_magi_kind_panel: task_kind is required' };
427
+ try {
428
+ const { removeMagiKindPanel } = await import('../../config/mesh-config.js');
429
+ const removed = removeMagiKindPanel(kind);
430
+ return { success: true, removed };
431
+ } catch (e: any) {
432
+ return { success: false, error: e.message };
433
+ }
434
+ },
435
+
305
436
  add_mesh_node: async (ctx: MedFamilyContext, args: any) => {
306
437
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
307
438
  const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
@@ -1820,6 +1820,16 @@ function callProviderNativeHistoryRead(
1820
1820
  const result = fn({
1821
1821
  agentType,
1822
1822
  sessionId: normalizedSessionId,
1823
+ // Arm the native-history executor's session pin guard. When the
1824
+ // instance is already bound to a provider session, pass that id as
1825
+ // `providerSessionId` so the executor rejects any *other* newest
1826
+ // session it would otherwise pick (hermes ≥0.14 creates a fresh
1827
+ // `sessions` row per internal sub-session, so an unpinned
1828
+ // newest-wins query drifts to a different id on every read →
1829
+ // re-bind churn + unbounded history re-hydration). When there is no
1830
+ // bound id yet (first-bind / workspace-only discovery) this is '',
1831
+ // which leaves the guard disarmed so discovery still works.
1832
+ providerSessionId: normalizedSessionId,
1823
1833
  historySessionId: normalizedSessionId,
1824
1834
  workspace,
1825
1835
  format: canonicalHistory?.format,
@@ -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
@@ -203,6 +204,7 @@ export type { CreateMeshOptions, UpdateMeshOptions, AddNodeOptions } from './con
203
204
  // them without taking a direct @adhdev/mesh-shared dependency).
204
205
  export type {
205
206
  MagiPanel, MagiPanelMember, MagiPanelMap, MagiMode, MagiTaskKind, MagiPanelDefaultKind,
207
+ MagiSlot, MagiKindPanelMap,
206
208
  MagiClaim, MagiClaimStance, MagiAgentResponse,
207
209
  MagiResponseSource, MagiReplicaGitRef, MagiGitSkew, MagiSynthesizedResponse,
208
210
  MagiClusterCategory, MagiClusterMember, MagiClaimCluster, MagiSynthesis,
@@ -207,6 +207,9 @@ Repository: \`${mesh.repoIdentity}\`${mesh.defaultBranch ? `\nDefault branch: \`
207
207
  // ── Workflow ──
208
208
  sections.push(WORKFLOW_SECTION);
209
209
 
210
+ // ── Onboarding / Reinit ──
211
+ sections.push(ONBOARDING_SECTION);
212
+
210
213
  // ── Rules ──
211
214
  sections.push(buildRulesSection(coordinatorCliType));
212
215
 
@@ -255,6 +258,7 @@ function readUserPromptFile(cliType: string | undefined, suffix: string): string
255
258
  * {{policy}} — full policy section
256
259
  * {{tools}} — the canonical tools table
257
260
  * {{workflow}} — the canonical orchestration workflow
261
+ * {{onboarding}} — the guided init/reinit onboarding section
258
262
  * {{rules}} — the canonical rules section (with coordinatorNote)
259
263
  * {{toolExposurePreflight}} — the MCP-missing preflight reminder
260
264
  *
@@ -281,6 +285,7 @@ function expandPromptPlaceholders(template: string, ctx: CoordinatorPromptContex
281
285
  policy: buildPolicySection(mergeAndNormalizePolicy(undefined, mesh.policy)),
282
286
  tools: TOOLS_SECTION,
283
287
  workflow: WORKFLOW_SECTION,
288
+ onboarding: ONBOARDING_SECTION,
284
289
  rules: buildRulesSection(coordinatorCliType),
285
290
  toolExposurePreflight: TOOL_EXPOSURE_PREFLIGHT_SECTION,
286
291
  };
@@ -493,7 +498,12 @@ const TOOLS_SECTION = `## Available Tools
493
498
  | \`mesh_clone_node\` | Create a worktree node for isolated parallel branch work |
494
499
  | \`mesh_refine_node\` | Validate and merge a completed worktree node back into its base branch |
495
500
  | \`mesh_remove_node\` | Remove a node (cleans up worktree if applicable) |
496
- | \`mesh_cleanup_sessions\` | Manually clean up delegated session records for a node |`;
501
+ | \`mesh_cleanup_sessions\` | Manually clean up delegated session records for a node |
502
+ | \`mesh_init\` | Guided onboarding for a fresh repo: dry-run scan → suggest \`.adhdev/*\` configs (refine/bootstrap/change-impact) + providerPriority + current-config echo; gated write on approval |
503
+ | \`mesh_reinit\` | Re-onboard an already-configured repo: re-suggest with overwrite semantics + current-vs-suggested diff; dry-run preview first, per-section approval before write |
504
+ | \`mesh_write_mesh_json_config\` | Gated write of \`.adhdev/mesh.json\` (repo coordinator-prompt config) from the mesh entry — dry-run/overwrite like mesh_init |
505
+ | \`mesh_magi_kind_panel_set\` | Bind a task_kind → MAGI kind-panel slots (machine-local, wholesale replacement — approve current-vs-new first) |
506
+ | \`mesh_magi_kind_panel_list\` | List configured task_kind → MAGI kind-panel slot bindings (machine-local, read-only) |`;
497
507
 
498
508
  const TOOL_EXPOSURE_PREFLIGHT_SECTION = `## Tool Exposure Preflight
499
509
 
@@ -529,6 +539,29 @@ Follow these recovery rules:
529
539
  3. **If no recovery context**: The stop may be intentional (normal completion). Use \`mesh_read_chat\` once to verify, then move on.
530
540
  4. **Always record what happened**: After handling a failure, briefly note the outcome in your report to the user.`;
531
541
 
542
+ const ONBOARDING_SECTION = `## Onboarding / Reinit
543
+
544
+ When the user asks to **set up / configure / onboard** this repo for Repo Mesh (or to **re-init / reconfigure** an already-onboarded repo), run ONE guided, approval-gated conversation. You draft, the user approves, the daemon writes. Never auto-write a heuristic suggestion without an explicit user approval turn.
545
+
546
+ **Save scopes — label every draft with its scope before asking for approval:**
547
+ - **repo-file (commit target)** — \`.adhdev/refine.json\`, \`.adhdev/worktree_bootstrap.json\`, \`.adhdev/change-impact.json\`, \`.adhdev/mesh.json\`. These are committed to the repository and shared with every machine/contributor.
548
+ - **machine-local** — MAGI kind→panel bindings and named MAGI panels, node providerPriority (\`~/.adhdev/meshes.json\`). These stay on this machine and are NOT committed.
549
+
550
+ **Guided sequence:**
551
+ 1. **Scan (dry-run)** — Call \`mesh_init\` (write=false, the default). It returns per-domain suggested configs for refine / worktree_bootstrap / change-impact, a recommended providerPriority, AND \`currentConfig\` — the currently-saved config per domain (repo files + machine-local \`magiKindPanels\`). Nothing is written.
552
+ 2. **Present drafts** — For each domain, show the user the suggested config with its **save scope label** (repo-file vs machine-local). When \`currentConfig\` already has a saved value for a domain (init on a partially-onboarded repo, or any reinit), present a **current-vs-suggested diff**, not just the suggestion.
553
+ 3. **Approve → gated write** — Only after the user approves, call the matching gated-write tool:
554
+ - repo \`.adhdev/*\` config files → \`mesh_init\` with \`write=true\` (and \`overwrite=true\` ONLY for domains the user approved replacing).
555
+ - \`.adhdev/mesh.json\` (coordinator prompt / operating notes) → \`mesh_write_mesh_json_config\` (write=true, overwrite only if approved).
556
+ - machine-local MAGI kind→panel slots → \`mesh_magi_kind_panel_set\` (write=true). NOTE: a kind binding is a **wholesale replacement** of that kind's slot list — present the current-vs-new slots first.
557
+ - machine-local named MAGI panels → \`mesh_magi_panel_set\`. providerPriority → apply via node policy update.
558
+
559
+ **init vs reinit:**
560
+ - **\`mesh_init\`** — for a fresh, never-onboarded repo. Existing config files are kept (existing-wins) unless the user explicitly approves overwrite. Use for first-time setup.
561
+ - **\`mesh_reinit\`** — for a repo that is already onboarded and needs its config refreshed. It re-suggests with OVERWRITE semantics and returns the current-vs-suggested \`currentConfig\` echo. Its first call is a DRY-RUN preview: you MUST present the per-section current-vs-suggested diff and get EXPLICIT per-section approval before re-invoking with write=true. Overwrite is a wholesale replacement, so it silently drops operator hand-edits if you skip the diff — never do that.
562
+
563
+ `;
564
+
532
565
  function buildRulesSection(coordinatorCliType?: string): string {
533
566
  const coordinatorNote = coordinatorCliType
534
567
  ? `\n- **Coordinator runtime is not a delegation default.** This coordinator is running as \`${coordinatorCliType}\`, but delegated node sessions must follow the user's requested provider, not the coordinator's own runtime.`