@adhdev/daemon-core 0.9.82-rc.438 → 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
  /**
@@ -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[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.438",
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.438",
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",
@@ -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' };
@@ -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,
package/src/index.ts CHANGED
@@ -204,6 +204,7 @@ export type { CreateMeshOptions, UpdateMeshOptions, AddNodeOptions } from './con
204
204
  // them without taking a direct @adhdev/mesh-shared dependency).
205
205
  export type {
206
206
  MagiPanel, MagiPanelMember, MagiPanelMap, MagiMode, MagiTaskKind, MagiPanelDefaultKind,
207
+ MagiSlot, MagiKindPanelMap,
207
208
  MagiClaim, MagiClaimStance, MagiAgentResponse,
208
209
  MagiResponseSource, MagiReplicaGitRef, MagiGitSkew, MagiSynthesizedResponse,
209
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.`
@@ -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
@@ -34,11 +40,21 @@ import {
34
40
  validateMeshWorktreeBootstrapConfig,
35
41
  type RepoMeshWorktreeBootstrapConfig,
36
42
  } from './worktree-bootstrap-config.js';
43
+ import {
44
+ CHANGE_IMPACT_CONFIG_LOCATIONS,
45
+ loadChangeImpactConfig,
46
+ suggestChangeImpactConfig,
47
+ validateChangeImpactConfig,
48
+ type ChangeImpactConfig,
49
+ } from '../git/change-impact-config.js';
50
+ import { listMagiKindPanels } from '../config/mesh-config.js';
51
+ import type { MagiKindPanelMap } from '@adhdev/mesh-shared';
37
52
  import type { CLIInfo } from '../detection/cli-detector.js';
38
53
 
39
54
  /** Canonical write targets — the first/preferred location for each config family. */
40
55
  export const MESH_INIT_REFINE_CONFIG_PATH = MESH_REFINE_CONFIG_LOCATIONS[0];
41
56
  export const MESH_INIT_WORKTREE_BOOTSTRAP_CONFIG_PATH = MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS[0];
57
+ export const MESH_INIT_CHANGE_IMPACT_CONFIG_PATH = CHANGE_IMPACT_CONFIG_LOCATIONS[0];
42
58
 
43
59
  /**
44
60
  * Default lockfiles whose change should invalidate a 'ready' bootstrap.
@@ -61,7 +77,7 @@ export interface MeshInitConfigFileResult {
61
77
  written: boolean;
62
78
  /** Why it was not written (already_exists / not_suggested / no change). */
63
79
  skippedReason?: 'already_exists' | 'no_suggestion';
64
- config?: RepoMeshRefineConfig | RepoMeshWorktreeBootstrapConfig;
80
+ config?: RepoMeshRefineConfig | RepoMeshWorktreeBootstrapConfig | ChangeImpactConfig;
65
81
  }
66
82
 
67
83
  /**
@@ -158,16 +174,50 @@ export interface RunMeshInitOptions {
158
174
  overwrite?: boolean;
159
175
  }
160
176
 
177
+ /**
178
+ * A snapshot of the currently-saved config for each domain, so the coordinator can
179
+ * render a current-vs-suggested diff to the user before an init/reinit overwrite.
180
+ * This is READ-ONLY echo — every field is what is on disk / machine-local right now,
181
+ * never a suggestion. Absent domains are reported as `undefined` config.
182
+ */
183
+ export interface MeshInitCurrentConfigEcho {
184
+ /** Currently-saved `.adhdev/refine.json` (repo-committed) — undefined when absent/invalid. */
185
+ refine?: RepoMeshRefineConfig;
186
+ /** Currently-saved `.adhdev/worktree_bootstrap.json` (repo-committed). */
187
+ worktreeBootstrap?: RepoMeshWorktreeBootstrapConfig;
188
+ /** Currently-saved `.adhdev/change-impact.json` (repo-committed). */
189
+ changeImpact?: ChangeImpactConfig;
190
+ /** Per-domain source type so the coordinator can tell "absent" from "invalid". */
191
+ sourceTypes: {
192
+ refine: string;
193
+ worktreeBootstrap: string;
194
+ changeImpact: string;
195
+ };
196
+ /**
197
+ * Currently-configured MAGI kind→panel slot bindings (machine-local
198
+ * ~/.adhdev/meshes.json). Empty object when none configured. This is a
199
+ * machine-local echo — the coordinator labels it as such vs. the repo files.
200
+ */
201
+ magiKindPanels: MagiKindPanelMap;
202
+ }
203
+
161
204
  export interface RunMeshInitResult {
162
205
  success: true;
163
206
  workspace: string;
164
207
  dryRun: boolean;
165
208
  refine: MeshInitConfigFileResult;
166
209
  worktreeBootstrap: MeshInitConfigFileResult;
210
+ changeImpact: MeshInitConfigFileResult;
167
211
  providers: {
168
212
  providerPriority: string[];
169
213
  installedProviders: Array<{ id: string; displayName: string; version?: string }>;
170
214
  };
215
+ /**
216
+ * Read-only snapshot of the currently-saved config per domain (repo files +
217
+ * machine-local kind panels). Lets the coordinator present a current-vs-suggested
218
+ * diff to the user before any overwrite. Always populated (dry-run and write).
219
+ */
220
+ currentConfig: MeshInitCurrentConfigEcho;
171
221
  note: string;
172
222
  }
173
223
 
@@ -210,18 +260,58 @@ export function runMeshInit(
210
260
  overwrite,
211
261
  });
212
262
 
263
+ // change-impact classification (.adhdev/change-impact.json). suggest/validate/save
264
+ // mirror refine's engine exactly, so it drops into the same applyConfigSuggestion
265
+ // gate: existing-wins unless overwrite, dry-run by default, validated before write.
266
+ const changeImpactLoaded = loadChangeImpactConfig(workspace);
267
+ const changeImpact = applyConfigSuggestion({
268
+ workspace,
269
+ relativePath: MESH_INIT_CHANGE_IMPACT_CONFIG_PATH,
270
+ existing: changeImpactLoaded.sourceType === 'repo_file' ? changeImpactLoaded.config : undefined,
271
+ suggestedConfig: suggestChangeImpactConfig(workspace).suggestedConfig,
272
+ validate: (config) => validateChangeImpactConfig(config, MESH_INIT_CHANGE_IMPACT_CONFIG_PATH).valid,
273
+ write,
274
+ overwrite,
275
+ });
276
+
213
277
  const providers = suggestNodeProviderPriority(detected);
214
278
 
279
+ // Read-only echo of the currently-saved config per domain so the coordinator can
280
+ // present a current-vs-suggested diff before any overwrite (init vs reinit). This
281
+ // never suggests — it reports what is on disk / machine-local right now.
282
+ const refineLoaded = loadMeshRefineConfig(mesh, workspace);
283
+ const bootstrapLoaded = loadMeshWorktreeBootstrapConfig(mesh, workspace);
284
+ let magiKindPanels: MagiKindPanelMap = {};
285
+ try {
286
+ magiKindPanels = listMagiKindPanels();
287
+ } catch {
288
+ // Machine-local config unreadable — echo an empty binding rather than failing init.
289
+ magiKindPanels = {};
290
+ }
291
+ const currentConfig: MeshInitCurrentConfigEcho = {
292
+ refine: refineLoaded.config,
293
+ worktreeBootstrap: bootstrapLoaded.config,
294
+ changeImpact: changeImpactLoaded.sourceType === 'repo_file' ? changeImpactLoaded.config : undefined,
295
+ sourceTypes: {
296
+ refine: refineLoaded.sourceType,
297
+ worktreeBootstrap: bootstrapLoaded.sourceType,
298
+ changeImpact: changeImpactLoaded.sourceType,
299
+ },
300
+ magiKindPanels,
301
+ };
302
+
215
303
  return {
216
304
  success: true,
217
305
  workspace,
218
306
  dryRun: !write,
219
307
  refine,
220
308
  worktreeBootstrap,
309
+ changeImpact,
221
310
  providers,
311
+ currentConfig,
222
312
  note: write
223
- ? 'Configs written to disk are the execution source of truth; suggestions are scaffold and only take effect once saved. providerPriority is a recommendation — apply it to node policy via clone/policy update.'
224
- : 'Dry-run: no files written. Re-run with write=true to persist the suggested configs. Heuristic suggestions never execute until saved as repo config.',
313
+ ? 'Configs written to disk are the execution source of truth; suggestions are scaffold and only take effect once saved. providerPriority is a recommendation — apply it to node policy via clone/policy update. currentConfig echoes what was on disk before this run.'
314
+ : 'Dry-run: no files written. Re-run with write=true to persist the suggested configs. Heuristic suggestions never execute until saved as repo config. Use currentConfig to diff current-vs-suggested before overwriting.',
225
315
  };
226
316
  }
227
317
 
@@ -234,7 +324,7 @@ function applyConfigSuggestion(input: {
234
324
  workspace: string;
235
325
  relativePath: string;
236
326
  existing?: unknown;
237
- suggestedConfig?: RepoMeshRefineConfig | RepoMeshWorktreeBootstrapConfig;
327
+ suggestedConfig?: RepoMeshRefineConfig | RepoMeshWorktreeBootstrapConfig | ChangeImpactConfig;
238
328
  validate: (config: unknown) => boolean;
239
329
  write: boolean;
240
330
  overwrite: boolean;
@@ -2813,7 +2813,13 @@ export class CliProviderInstance implements ProviderInstance {
2813
2813
  typeof data.providerSessionId === 'string' ? data.providerSessionId : '',
2814
2814
  );
2815
2815
  if (patchedProviderSessionId) {
2816
- this.promoteProviderSessionId(patchedProviderSessionId);
2816
+ // A provider-response id is authoritative when it carries an
2817
+ // explicit `new_session` marker (the CLI genuinely started a new
2818
+ // conversation). Without that marker it's just an observed id and
2819
+ // must not hijack an existing binding (see promoteProviderSessionId).
2820
+ this.promoteProviderSessionId(patchedProviderSessionId, {
2821
+ authoritative: data.sessionEvent === 'new_session',
2822
+ });
2817
2823
  }
2818
2824
 
2819
2825
  if (data.sessionEvent === 'new_session') {
@@ -3167,10 +3173,26 @@ export class CliProviderInstance implements ProviderInstance {
3167
3173
  return lines.join('\n');
3168
3174
  }
3169
3175
 
3170
- private promoteProviderSessionId(sessionId: string): void {
3176
+ private promoteProviderSessionId(sessionId: string, opts: { authoritative?: boolean } = {}): void {
3171
3177
  const nextSessionId = String(sessionId || '').trim();
3172
3178
  if (!nextSessionId || nextSessionId === this.providerSessionId) return;
3173
3179
 
3180
+ // Sticky binding: once this instance is bound to a provider session,
3181
+ // an *observed* id (one discovered from a status parse or the native
3182
+ // history reader) must NOT hijack the live binding. hermes ≥0.14
3183
+ // spawns a fresh `sessions` row per internal sub-session, so a
3184
+ // newest-wins native read surfaces a different id mid-turn on every
3185
+ // poll; accepting it would re-bind the instance, re-hydrate unbounded
3186
+ // history (daemon saturation) and reset completion detection so the
3187
+ // turn never finalizes. Only an *authoritative* change — the first
3188
+ // bind (no id yet) or an explicit provider `new_session`/resume — may
3189
+ // replace an existing binding. Legitimate resume/new-session paths
3190
+ // pass authoritative:true and are unaffected.
3191
+ if (this.providerSessionId && !opts.authoritative) {
3192
+ LOG.debug('CLI', `[${this.type}] ignoring non-authoritative session id ${nextSessionId} (bound to ${this.providerSessionId})`);
3193
+ return;
3194
+ }
3195
+
3174
3196
  const previousHistorySessionId = this.providerSessionId || this.instanceId;
3175
3197
  const previousProviderSessionId = this.providerSessionId;
3176
3198
  this.providerSessionId = nextSessionId;
@@ -66,7 +66,7 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
66
66
  try { fs.statSync(sourcePath); } catch { /* best-effort metadata refresh */ }
67
67
  }
68
68
 
69
- const session = readByReader(reader, sourcePath, sessionId, workspace);
69
+ const session = readByReader(reader, sourcePath, sessionId, workspace, requestedProviderSid);
70
70
  if (!session) return null;
71
71
 
72
72
  if (requestedProviderSid && session.providerSessionId && session.providerSessionId !== requestedProviderSid) {
@@ -265,12 +265,18 @@ function readByReader(
265
265
  sourcePath: string,
266
266
  sessionId: string,
267
267
  workspace: string,
268
+ requestedProviderSid: string,
268
269
  ): any | null {
269
270
  switch (reader) {
270
271
  case 'claude-cli': return readClaudeCliSession(sourcePath);
271
272
  case 'codex-cli': return readCodexCliSession(sourcePath);
272
273
  case 'antigravity-cli': return readAntigravityCliSession(sourcePath, sessionId || undefined, workspace || undefined);
273
- case 'hermes-cli': return readHermesCliSession(sourcePath);
274
+ // hermes reads a *shared* state.db and would otherwise pick the newest
275
+ // source='cli' session, which drifts every read (hermes ≥0.14 writes a
276
+ // fresh row per internal sub-session). Pass the bound id so it reads
277
+ // THAT session directly instead of newest-wins. claude/codex resolve a
278
+ // per-session file upstream, so they need no equivalent pin here.
279
+ case 'hermes-cli': return readHermesCliSession(sourcePath, requestedProviderSid || undefined);
274
280
  }
275
281
  }
276
282
 
@@ -93,26 +93,42 @@ function loadMessagesForSession(db: any, sessionId: string): NativeHistoryMessag
93
93
  return out;
94
94
  }
95
95
 
96
- export function readSession(sessionPath: string): NativeHistorySession | null {
96
+ export function readSession(sessionPath: string, requestedSessionId?: string): NativeHistorySession | null {
97
97
  if (!sessionPath) return null;
98
98
 
99
- // Path mode A: SQLite — sourcePath is the state.db path. Pick the
100
- // newest source='cli' session that has at least one persisted message.
99
+ // Path mode A: SQLite — sourcePath is the state.db path.
101
100
  if (sessionPath === HERMES_STATE_DB) {
102
101
  const db = openDb();
103
102
  if (!db) return null;
104
103
  try {
105
- const row: any = db.prepare(
106
- `SELECT id, started_at FROM sessions
107
- WHERE source = 'cli' AND message_count > 0
108
- ORDER BY started_at DESC LIMIT 1`,
109
- ).get();
110
- if (!row) return null;
111
- const messages = loadMessagesForSession(db, row.id);
104
+ const pinned = String(requestedSessionId || '').trim();
105
+ let sessionId: string;
106
+ if (pinned) {
107
+ // Session pin: the caller is already bound to a specific
108
+ // provider session, so read THAT session directly instead of
109
+ // the newest-wins pick below. hermes ≥0.14 writes a fresh
110
+ // `sessions` row per internal sub-session, so an unpinned
111
+ // `ORDER BY started_at DESC LIMIT 1` drifts to a different id
112
+ // on every read → re-bind churn + reading completion evidence
113
+ // from the wrong session. loadMessagesForSession returning
114
+ // rows validates the id exists.
115
+ sessionId = pinned;
116
+ } else {
117
+ // No bound id yet (discovery): pick the newest source='cli'
118
+ // session that has at least one persisted message.
119
+ const row: any = db.prepare(
120
+ `SELECT id, started_at FROM sessions
121
+ WHERE source = 'cli' AND message_count > 0
122
+ ORDER BY started_at DESC LIMIT 1`,
123
+ ).get();
124
+ if (!row) return null;
125
+ sessionId = String(row.id);
126
+ }
127
+ const messages = loadMessagesForSession(db, sessionId);
112
128
  if (messages.length === 0) return null;
113
129
  return {
114
130
  messages,
115
- providerSessionId: String(row.id),
131
+ providerSessionId: sessionId,
116
132
  source: 'provider-native',
117
133
  sourcePath: sessionPath,
118
134
  sourceMtimeMs: statMtimeMs(sessionPath),