@adhdev/daemon-core 0.9.82-rc.411 → 0.9.82-rc.413

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.
@@ -1,77 +1,42 @@
1
1
  /**
2
2
  * Repo-shared declarative mesh config — `.adhdev/mesh.json`
3
3
  *
4
- * A repo-committed, machine-independent BASE for mesh declarative settings
5
- * (policy + coordinator prompt override/append + operating notes). It is the
6
- * lowest layer of a LOCAL-WINS merge chain:
4
+ * A repo-committed, machine-independent file carrying ONLY the repo-shared
5
+ * coordinator-prompt config and operating notes:
7
6
  *
8
- * DEFAULT_MESH_POLICY → .adhdev/mesh.json (repo base) → machine-local
9
- * meshes.json / coordinator-prompt files (always win)
10
- *
11
- * The merge is **in-memory only**. The on-disk machine-local `meshes.json` is
12
- * NEVER mutated by this module — local config keeps winning and the merge is
13
- * applied transiently on the coordinator launch + display paths. This keeps the
14
- * repo file a shared default that a machine can always override without the
15
- * override being silently rewritten back to the repo shape.
16
- *
17
- * Three zones (scope = mesh-global in v1; per-node scope is not introduced):
18
- * - policy — RepoMeshPolicy fields; local fields that differ from
19
- * DEFAULT_MESH_POLICY win, otherwise the repo base shows
20
- * through (true per-field LOCAL-WINS).
21
7
  * - coordinator — systemPromptOverride (local wins, else repo) and
22
8
  * systemPromptAppend (repo append + local append BOTH
23
9
  * stack, repo first).
24
10
  * - operatingNotes — repo-declared baseline notes merged with the runtime
25
11
  * ledger notes; on duplicate text the ledger note wins.
12
+ * - limits — advisory-only (`maxNoteChars`, `maxNotes`,
13
+ * `coordinator.maxPromptChars`); recorded, never enforced in v1.
26
14
  *
27
- * Advisory-only fields (`coordinator.maxPromptChars`, `limits.maxNoteChars`,
28
- * `limits.maxNotes`) are accepted and preserved in the schema but NOT enforced
29
- * in v1 they document operator intent only.
30
- *
31
- * SCHEDULING OVERLAY this same `.adhdev/mesh.json` also carries the in-tree
32
- * scheduling product surface (`policy.scheduling`). Instead of the scattered
33
- * `schedulingStrategy` / `maxParallelTasks` knobs (each with its own default +
34
- * clamp), an operator declares one `policy.scheduling` block checked into the
35
- * repo. It layers ON TOP of the stored meshes.json policy with LOCAL-WINS — a
36
- * value present in the repo file overrides the persisted mesh policy, so the
37
- * repo is the source of truth for how its own work distributes. Absent keys
38
- * leave the stored policy untouched (a partial overlay, not a replacement).
15
+ * POLICY IS NOT HERE. RepoMeshPolicy (the 15 scheduling/approval fields) is
16
+ * MACHINE-LOCAL only it lives in meshes.json and is never sourced from, merged
17
+ * with, or overlaid by a repo file. There is no `policy` zone and no
18
+ * `policy.scheduling` overlay in mesh.json; the scheduler reads the stored
19
+ * machine-local policy directly. The repo file shapes the coordinator prompt and
20
+ * operating notes, nothing else.
39
21
  *
40
- * .adhdev/mesh.json
41
- * { "policy": { "scheduling": {
42
- * "distribution": "spread" | "in_order", // → schedulingStrategy
43
- * "maxParallel": 1..8, // → maxParallelTasks (clamped)
44
- * "readonlyMultiplier": 2 // read-only cap multiplier (default 2)
45
- * } } }
22
+ * The coordinator/operatingNotes merge is **in-memory only**: the on-disk
23
+ * machine-local `meshes.json` is never mutated by this module.
46
24
  *
47
- * The raw 4-union schedulingStrategy escape hatch is preserved elsewhere: a
48
- * hand-edited meshes.json may still write any of the four raw strategies and the
49
- * scheduler honors it. The scheduling overlay exposes only the 2-mode
50
- * `distribution` façade, mapped through distributionToStrategy.
51
- *
52
- * The declarative-config loader (`loadRepoMeshJsonConfig`) and the scheduling
53
- * overlay loader (`loadMeshJsonConfig`) read the SAME file but resolve different
54
- * zones; they coexist intentionally — coordinator launch/display consume the
55
- * declarative zones, the scheduler consumes the scheduling overlay.
25
+ * FILE SEPARATION: refine / worktree-bootstrap / change-impact configs live in
26
+ * their OWN `.adhdev/*` files with their own loaders they are NOT inlined into
27
+ * mesh.json. `loadRepoSettings` (config/repo-settings.ts) assembles all of them
28
+ * into one object for consumers.
56
29
  *
57
30
  * SECURITY: declarative only. JSON/YAML is parsed; arbitrary JS (.js) is NOT
58
31
  * supported, so loading a config can never execute code.
59
32
  */
60
33
 
61
- import { existsSync, readFileSync, statSync } from 'fs';
34
+ import { existsSync, readFileSync } from 'fs';
62
35
  import { join } from 'path';
63
36
  import * as yaml from 'js-yaml';
64
- import {
65
- mergeAndNormalizePolicy,
66
- type RepoMeshPolicy,
67
- type RepoMeshCoordinatorConfig,
68
- type LocalMeshEntry,
69
- type RepoMeshDistribution,
70
- distributionToStrategy,
71
- normalizeMeshDistribution,
72
- resolveMaxParallelTasks,
73
- MESH_MAX_PARALLEL_TASKS_MIN,
74
- MESH_MAX_PARALLEL_TASKS_MAX,
37
+ import type {
38
+ RepoMeshCoordinatorConfig,
39
+ LocalMeshEntry,
75
40
  } from '../repo-mesh-types.js';
76
41
  import type { CoordinatorOperatingNote } from '../mesh/coordinator-prompt.js';
77
42
 
@@ -97,11 +62,11 @@ export interface RepoMeshDeclarativeLimits {
97
62
 
98
63
  /**
99
64
  * Parsed + normalized `.adhdev/mesh.json` shape. Every field is optional except
100
- * version so a repo can declare only the zone(s) it cares about.
65
+ * version so a repo can declare only the zone(s) it cares about. Policy is NOT a
66
+ * zone here — it is machine-local (meshes.json) only.
101
67
  */
102
68
  export interface RepoMeshDeclarativeConfig {
103
69
  version: 1;
104
- policy?: Partial<RepoMeshPolicy>;
105
70
  coordinator?: RepoMeshDeclarativeCoordinatorConfig;
106
71
  operatingNotes?: CoordinatorOperatingNote[];
107
72
  limits?: RepoMeshDeclarativeLimits;
@@ -117,31 +82,10 @@ export interface RepoMeshJsonConfigLoadResult {
117
82
  error?: string;
118
83
  }
119
84
 
120
- // ─── Types (scheduling overlay) ─────────────────
121
-
122
- export interface MeshJsonSchedulingConfig {
123
- /** User-facing distribution mode; mapped to schedulingStrategy on apply. */
124
- distribution?: RepoMeshDistribution;
125
- /** Global write-task parallel cap (clamped to [1, 8] on apply). */
126
- maxParallel?: number;
127
- /** Read-only diagnosis cap multiplier. Missing → 2. */
128
- readonlyMultiplier?: number;
129
- }
130
-
131
- export interface MeshJsonConfig {
132
- scheduling?: MeshJsonSchedulingConfig;
133
- }
134
-
135
- export interface MeshJsonConfigLoadResult {
136
- config?: MeshJsonConfig;
137
- source: string;
138
- sourceType: 'repo_file' | 'unavailable' | 'invalid';
139
- path?: string;
140
- error?: string;
141
- /** Cache key: path + mtime, or a sentinel for the missing/invalid case. */
142
- sourceKey: string;
143
- }
144
-
85
+ // MESH_JSON_CONFIG_LOCATIONS is retained for backward compatibility: it is the
86
+ // shared list of `.adhdev/mesh.*` filenames the loader probes (and other modules
87
+ // reference for diagnostics). The file now carries only coordinator + operating
88
+ // notes; there is no scheduling overlay.
145
89
  export const MESH_JSON_CONFIG_LOCATIONS = [
146
90
  '.adhdev/mesh.json',
147
91
  '.adhdev/mesh.yaml',
@@ -156,9 +100,6 @@ export const MESH_JSON_CONFIG_SCHEMA = {
156
100
  required: ['version'],
157
101
  properties: {
158
102
  version: { const: 1 },
159
- // policy is validated/normalized through mergeAndNormalizePolicy at merge
160
- // time; the schema here only asserts it is an object.
161
- policy: { type: 'object' },
162
103
  coordinator: {
163
104
  type: 'object',
164
105
  additionalProperties: false,
@@ -242,14 +183,6 @@ export function normalizeRepoMeshDeclarativeConfig(parsed: unknown): {
242
183
 
243
184
  const config: RepoMeshDeclarativeConfig = { version: 1 };
244
185
 
245
- if (parsed.policy !== undefined) {
246
- if (isRecord(parsed.policy)) {
247
- config.policy = parsed.policy as Partial<RepoMeshPolicy>;
248
- } else {
249
- errors.push('policy must be an object when provided');
250
- }
251
- }
252
-
253
186
  if (parsed.coordinator !== undefined) {
254
187
  if (isRecord(parsed.coordinator)) {
255
188
  const coord: RepoMeshDeclarativeCoordinatorConfig = {};
@@ -334,51 +267,7 @@ export function loadRepoMeshJsonConfig(workspace?: string): RepoMeshJsonConfigLo
334
267
  };
335
268
  }
336
269
 
337
- // ─── Declarative config: Merge (LOCAL-WINS) ─────
338
-
339
- function deepEqual(a: unknown, b: unknown): boolean {
340
- if (a === b) return true;
341
- try { return JSON.stringify(a) === JSON.stringify(b); } catch { return false; }
342
- }
343
-
344
- /**
345
- * Reduce a (fully-normalized) machine-local policy to ONLY the fields that
346
- * genuinely differ from DEFAULT_MESH_POLICY. A meshes.json policy is always
347
- * stored fully-defaulted, so a naive merge would let it shadow every repo-base
348
- * field — defeating the repo default. By keeping only the truly-overridden
349
- * fields, an untouched local field falls through to the repo base while a local
350
- * field the operator actually changed still wins. Optional fields absent from
351
- * DEFAULT_MESH_POLICY (e.g. allowedProviders, schedulingStrategy) count as an
352
- * override whenever present.
353
- */
354
- export function diffPolicyFromDefault(local: Partial<RepoMeshPolicy> | undefined): Partial<RepoMeshPolicy> {
355
- if (!local || typeof local !== 'object') return {};
356
- const out: Record<string, unknown> = {};
357
- // Compare against the NORMALIZED default (not the raw DEFAULT_MESH_POLICY) so a
358
- // fully-normalized local policy — whose nested autoFastForward is filled out by
359
- // normalizeAutoFastForwardPolicy — does not spuriously read as an override.
360
- const def = mergeAndNormalizePolicy(undefined, undefined) as unknown as Record<string, unknown>;
361
- for (const [key, value] of Object.entries(local)) {
362
- if (value === undefined) continue;
363
- if (!deepEqual(value, def[key])) out[key] = value;
364
- }
365
- return out as Partial<RepoMeshPolicy>;
366
- }
367
-
368
- /**
369
- * Effective mesh policy = DEFAULT → repo base → local overrides (per-field
370
- * LOCAL-WINS). When `local` is all-default the repo base shows through entirely
371
- * (case i); when `local` overrides a field that field wins (case ii). Nested
372
- * objects (autoFastForward) are still merged per-field by mergeAndNormalizePolicy.
373
- */
374
- export function mergeEffectiveMeshPolicy(
375
- repoPolicy: Partial<RepoMeshPolicy> | undefined,
376
- localPolicy: RepoMeshPolicy | Partial<RepoMeshPolicy> | undefined,
377
- ): RepoMeshPolicy {
378
- const repoMerged = mergeAndNormalizePolicy(undefined, repoPolicy);
379
- const localOverrides = diffPolicyFromDefault(localPolicy);
380
- return mergeAndNormalizePolicy(repoMerged, localOverrides);
381
- }
270
+ // ─── Declarative config: Merge (coordinator + operating notes) ─────
382
271
 
383
272
  /**
384
273
  * Effective coordinator config. systemPromptOverride: local wins, else repo.
@@ -440,18 +329,19 @@ export function mergeEffectiveOperatingNotes(
440
329
 
441
330
  /**
442
331
  * Produce an in-memory effective mesh by layering the repo declarative config
443
- * UNDER the machine-local mesh entry (policy + coordinator zones). Returns a new
444
- * object; the input mesh and on-disk meshes.json are never mutated. operatingNotes
445
- * are merged separately at launch (they ride the prompt context, not the mesh).
332
+ * UNDER the machine-local mesh entry coordinator zone ONLY. Policy is
333
+ * machine-local and is left exactly as the stored mesh carries it (never sourced
334
+ * from the repo file). Returns a new object; the input mesh and on-disk
335
+ * meshes.json are never mutated. operatingNotes are merged separately at launch
336
+ * (they ride the prompt context, not the mesh).
446
337
  */
447
- export function applyRepoMeshConfig<T extends Pick<LocalMeshEntry, 'policy' | 'coordinator'>>(
338
+ export function applyRepoMeshConfig<T extends Pick<LocalMeshEntry, 'coordinator'>>(
448
339
  mesh: T,
449
340
  repoConfig: RepoMeshDeclarativeConfig | null | undefined,
450
341
  ): T {
451
342
  if (!repoConfig) return mesh;
452
343
  return {
453
344
  ...mesh,
454
- policy: mergeEffectiveMeshPolicy(repoConfig.policy, mesh.policy),
455
345
  coordinator: mergeEffectiveCoordinatorConfig(repoConfig.coordinator, mesh.coordinator),
456
346
  };
457
347
  }
@@ -461,18 +351,16 @@ export function applyRepoMeshConfig<T extends Pick<LocalMeshEntry, 'policy' | 'c
461
351
  /**
462
352
  * Build a `.adhdev/mesh.json` DRAFT from a machine-local mesh entry. This is a
463
353
  * scaffold for the operator to review and commit — NOT an automatic migration.
464
- * It captures the local policy (fully normalized) plus any coordinator prompt
465
- * override/append so a repo can adopt the current machine's settings as the
466
- * shared base. Operating notes are intentionally NOT exported: those are runtime
467
- * ledger lessons, and a repo should declare baseline notes deliberately.
354
+ * It captures the coordinator prompt override/append so a repo can adopt the
355
+ * current machine's prompt customization as the shared base. Policy is NOT
356
+ * exported it is machine-local only and has no place in mesh.json. Operating
357
+ * notes are intentionally NOT exported either: those are runtime ledger lessons,
358
+ * and a repo should declare baseline notes deliberately.
468
359
  */
469
360
  export function buildMeshJsonConfigScaffold(
470
- mesh: Pick<LocalMeshEntry, 'policy' | 'coordinator'>,
361
+ mesh: Pick<LocalMeshEntry, 'coordinator'>,
471
362
  ): RepoMeshDeclarativeConfig {
472
- const scaffold: RepoMeshDeclarativeConfig = {
473
- version: 1,
474
- policy: mergeAndNormalizePolicy(undefined, mesh.policy),
475
- };
363
+ const scaffold: RepoMeshDeclarativeConfig = { version: 1 };
476
364
  const coord: RepoMeshDeclarativeCoordinatorConfig = {};
477
365
  const override = mesh.coordinator?.systemPromptOverride;
478
366
  if (typeof override === 'string' && override.trim()) coord.systemPromptOverride = override;
@@ -486,132 +374,3 @@ export function buildMeshJsonConfigScaffold(
486
374
  export function serializeMeshJsonConfigScaffold(config: RepoMeshDeclarativeConfig): string {
487
375
  return JSON.stringify(config, null, 2);
488
376
  }
489
-
490
- // ─── Scheduling overlay: validate / load ────────
491
-
492
- /**
493
- * Validate a parsed `.adhdev/mesh.*` document into a MeshJsonConfig (scheduling
494
- * overlay). Tolerant of an empty/partial file (an empty object is valid and
495
- * yields no overlay); strict about malformed values so a typo surfaces as
496
- * `invalid` rather than silently no-op'ing.
497
- */
498
- export function validateMeshJsonConfig(raw: unknown, source = 'inline'): { valid: boolean; errors: string[]; config?: MeshJsonConfig } {
499
- const errors: string[] = [];
500
- if (!isRecord(raw)) {
501
- return { valid: false, errors: [`${source}: config must be an object`] };
502
- }
503
- const config: MeshJsonConfig = {};
504
-
505
- const policy = raw.policy;
506
- const schedulingRaw = isRecord(policy) ? policy.scheduling : undefined;
507
- if (schedulingRaw !== undefined) {
508
- if (!isRecord(schedulingRaw)) {
509
- errors.push('policy.scheduling must be an object');
510
- } else {
511
- const scheduling: MeshJsonSchedulingConfig = {};
512
- if (schedulingRaw.distribution !== undefined) {
513
- if (typeof schedulingRaw.distribution !== 'string'
514
- || !['spread', 'in_order'].includes(schedulingRaw.distribution.trim())) {
515
- errors.push("policy.scheduling.distribution must be 'spread' or 'in_order'");
516
- } else {
517
- scheduling.distribution = normalizeMeshDistribution(schedulingRaw.distribution);
518
- }
519
- }
520
- if (schedulingRaw.maxParallel !== undefined) {
521
- const n = Number(schedulingRaw.maxParallel);
522
- if (!Number.isFinite(n) || n < MESH_MAX_PARALLEL_TASKS_MIN || n > MESH_MAX_PARALLEL_TASKS_MAX) {
523
- errors.push(`policy.scheduling.maxParallel must be a number in [${MESH_MAX_PARALLEL_TASKS_MIN}, ${MESH_MAX_PARALLEL_TASKS_MAX}]`);
524
- } else {
525
- scheduling.maxParallel = Math.floor(n);
526
- }
527
- }
528
- if (schedulingRaw.readonlyMultiplier !== undefined) {
529
- const n = Number(schedulingRaw.readonlyMultiplier);
530
- if (!Number.isFinite(n) || n < 1) {
531
- errors.push('policy.scheduling.readonlyMultiplier must be a number >= 1');
532
- } else {
533
- scheduling.readonlyMultiplier = Math.floor(n);
534
- }
535
- }
536
- // Reject unknown keys inside the scheduling block to catch typos early.
537
- for (const key of Object.keys(schedulingRaw)) {
538
- if (!['distribution', 'maxParallel', 'readonlyMultiplier'].includes(key)) {
539
- errors.push(`policy.scheduling.${key} is not a recognized field`);
540
- }
541
- }
542
- if (Object.keys(scheduling).length) config.scheduling = scheduling;
543
- }
544
- }
545
-
546
- return { valid: errors.length === 0, errors, config: errors.length === 0 ? config : undefined };
547
- }
548
-
549
- // mtime-keyed cache: the scheduler re-reads on every reconcile tick (~4s), so cache
550
- // the parse result and only re-read when the file's mtime (or its presence) changes.
551
- const cache = new Map<string, { sourceKey: string; result: MeshJsonConfigLoadResult }>();
552
-
553
- /**
554
- * Locate and load the `.adhdev/mesh.json` scheduling overlay for a repo root.
555
- * Declarative only — never executes config. Cached by path + mtime so repeated
556
- * scheduler reads are cheap.
557
- */
558
- export function loadMeshJsonConfig(repoRoot: string): MeshJsonConfigLoadResult {
559
- if (!repoRoot) {
560
- return { source: 'unavailable', sourceType: 'unavailable', error: 'no repo root', sourceKey: 'unavailable' };
561
- }
562
- for (const relative of MESH_JSON_CONFIG_LOCATIONS) {
563
- const configPath = join(repoRoot, relative);
564
- if (!existsSync(configPath)) continue;
565
- let mtimeMs = 0;
566
- try {
567
- mtimeMs = statSync(configPath).mtimeMs;
568
- } catch {
569
- mtimeMs = 0;
570
- }
571
- const cacheKey = configPath;
572
- const sourceKey = `file:${configPath}:${mtimeMs}`;
573
- const cached = cache.get(cacheKey);
574
- if (cached && cached.sourceKey === sourceKey) return cached.result;
575
- let result: MeshJsonConfigLoadResult;
576
- try {
577
- const text = readFileSync(configPath, 'utf-8');
578
- const parsed = parseConfigText(configPath, text);
579
- const validation = validateMeshJsonConfig(parsed, relative);
580
- result = validation.valid
581
- ? { config: validation.config, source: relative, sourceType: 'repo_file', path: configPath, sourceKey }
582
- : { source: relative, sourceType: 'invalid', path: configPath, error: validation.errors.join('; '), sourceKey: `invalid:${configPath}:${mtimeMs}` };
583
- } catch (error: any) {
584
- result = { source: relative, sourceType: 'invalid', path: configPath, error: error?.message || String(error), sourceKey: `error:${configPath}` };
585
- }
586
- cache.set(cacheKey, { sourceKey: result.sourceKey, result });
587
- return result;
588
- }
589
- return {
590
- source: 'unavailable',
591
- sourceType: 'unavailable',
592
- error: `No .adhdev/mesh config found. Checked: ${MESH_JSON_CONFIG_LOCATIONS.join(', ')}`,
593
- sourceKey: 'unavailable',
594
- };
595
- }
596
-
597
- /** Test-only: drop the mtime cache so a per-run temp file is re-read. */
598
- export function __resetMeshJsonConfigCacheForTests(): void {
599
- cache.clear();
600
- }
601
-
602
- /**
603
- * The effective scheduling knobs after layering the repo-local `.adhdev/mesh.json`
604
- * overlay on top of a stored mesh policy (LOCAL-WINS). This is the single resolution
605
- * point the scheduler reads, so the strategy/caps it acts on always reflect the
606
- * in-tree override when present and fall back to the persisted policy otherwise.
607
- */
608
- export interface EffectiveMeshScheduling {
609
- /** Resolved raw strategy (override distribution → strategy, else stored policy). */
610
- strategy: ReturnType<typeof distributionToStrategy> | RepoMeshPolicy['schedulingStrategy'];
611
- /** Effective, clamped global write cap. */
612
- maxParallelTasks: number;
613
- /** Read-only cap multiplier (override, else default 2). */
614
- readonlyMultiplier?: number;
615
- /** True when an `.adhdev/mesh.json` overlay actually contributed a value. */
616
- overrideApplied: boolean;
617
- }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Repo Settings — one unified loader over the repo-committed `.adhdev/*` config
3
+ * files (the "single source" assembly point).
4
+ *
5
+ * DESIGN (option B — file separation preserved): the repo keeps its declarative
6
+ * config in SEPARATE files, each with its own dedicated loader:
7
+ *
8
+ * .adhdev/mesh.json → loadRepoMeshJsonConfig (coordinator + operatingNotes)
9
+ * .adhdev/refine.json → loadMeshRefineConfig (Refinery validation)
10
+ * .adhdev/worktree_bootstrap.json → loadMeshWorktreeBootstrapConfig
11
+ * .adhdev/change-impact.json → loadChangeImpactConfig
12
+ *
13
+ * Nothing is inlined into mesh.json. `loadRepoSettings` simply CALLS each
14
+ * dedicated loader and assembles the results into one `RepoSettings` object so a
15
+ * consumer can read every repo-shared setting through a single call instead of
16
+ * threading four loaders. The per-file loaders remain the source of truth (and
17
+ * stay independently usable); this is a convenience aggregator, not a new schema.
18
+ *
19
+ * IMPORTANT — policy is NOT here. RepoMeshPolicy (the 15 scheduling/approval
20
+ * fields) is MACHINE-LOCAL only: it lives in meshes.json and is never sourced
21
+ * from or merged with a repo file. mesh.json carries only coordinator prompt
22
+ * config + operating notes (+ advisory limits).
23
+ *
24
+ * The refine/worktree-bootstrap loaders also honor a machine-local INLINE seam
25
+ * (mesh.refineConfig / mesh.policy.worktreeBootstrap, etc.) — that seam is for
26
+ * machine-local config and is unchanged here; mesh.json gains no such zone.
27
+ */
28
+
29
+ import {
30
+ loadRepoMeshJsonConfig,
31
+ type RepoMeshDeclarativeCoordinatorConfig,
32
+ type RepoMeshDeclarativeLimits,
33
+ type RepoMeshJsonConfigLoadResult,
34
+ } from './mesh-json-config.js';
35
+ import {
36
+ loadMeshRefineConfig,
37
+ type MeshRefineConfigLoadResult,
38
+ } from '../mesh/refine-config.js';
39
+ import {
40
+ loadMeshWorktreeBootstrapConfig,
41
+ type WorktreeBootstrapConfigLoadResult,
42
+ } from '../mesh/worktree-bootstrap-config.js';
43
+ import {
44
+ loadChangeImpactConfig,
45
+ type ChangeImpactConfigLoadResult,
46
+ } from '../git/change-impact-config.js';
47
+ import type { CoordinatorOperatingNote } from '../mesh/coordinator-prompt.js';
48
+
49
+ export interface LoadRepoSettingsOptions {
50
+ /** Workspace path whose `.adhdev/*` files are resolved (mesh.json / refine / bootstrap). */
51
+ workspace: string;
52
+ /**
53
+ * Machine-local mesh entry, consulted ONLY for the INLINE seam of the
54
+ * refine / worktree-bootstrap loaders (mesh.refineConfig etc.). Optional —
55
+ * omit it and only the repo files are considered. NEVER used to source policy.
56
+ */
57
+ mesh?: any;
58
+ /**
59
+ * Repo root for change-impact resolution. Defaults to `workspace` when omitted
60
+ * (change-impact compares the daemon build commit against the workspace HEAD).
61
+ */
62
+ repoRoot?: string;
63
+ }
64
+
65
+ /**
66
+ * The assembled repo-shared settings. Each sub-config carries its own load
67
+ * result (source / sourceType / error) so a consumer can tell "absent" from
68
+ * "invalid" per file. `coordinator` / `operatingNotes` / `limits` are lifted out
69
+ * of the mesh.json load result for convenience; `meshJson` keeps the full result
70
+ * (e.g. for the `sourceType === 'invalid'` warning on coordinator launch).
71
+ */
72
+ export interface RepoSettings {
73
+ /** Repo-shared coordinator prompt config (override/append) from `.adhdev/mesh.json`. */
74
+ coordinator?: RepoMeshDeclarativeCoordinatorConfig;
75
+ /** Repo-declared baseline operating notes from `.adhdev/mesh.json`. */
76
+ operatingNotes?: CoordinatorOperatingNote[];
77
+ /** Advisory-only limits from `.adhdev/mesh.json` (recorded, not enforced in v1). */
78
+ limits?: RepoMeshDeclarativeLimits;
79
+ /** Full `.adhdev/mesh.json` declarative load result (coordinator/operatingNotes source). */
80
+ meshJson: RepoMeshJsonConfigLoadResult;
81
+ /** `.adhdev/refine.json` Refinery validation config load result. */
82
+ refine: MeshRefineConfigLoadResult;
83
+ /** `.adhdev/worktree_bootstrap.json` bootstrap config load result. */
84
+ worktreeBootstrap: WorktreeBootstrapConfigLoadResult;
85
+ /** `.adhdev/change-impact.json` change-impact config load result. */
86
+ changeImpact: ChangeImpactConfigLoadResult;
87
+ }
88
+
89
+ /**
90
+ * Load every repo-committed `.adhdev/*` setting for a workspace and assemble them
91
+ * into one object. Each dedicated loader degrades to an `unavailable`/`invalid`
92
+ * load result rather than throwing, so this never throws on a missing or broken
93
+ * file — a consumer inspects the per-sub-config `sourceType`.
94
+ */
95
+ export function loadRepoSettings(opts: LoadRepoSettingsOptions): RepoSettings {
96
+ const workspace = typeof opts.workspace === 'string' ? opts.workspace : '';
97
+ const mesh = opts.mesh;
98
+ const repoRoot = typeof opts.repoRoot === 'string' && opts.repoRoot ? opts.repoRoot : workspace;
99
+
100
+ const meshJson = loadRepoMeshJsonConfig(workspace);
101
+
102
+ return {
103
+ coordinator: meshJson.config?.coordinator,
104
+ operatingNotes: meshJson.config?.operatingNotes,
105
+ limits: meshJson.config?.limits,
106
+ meshJson,
107
+ refine: loadMeshRefineConfig(mesh, workspace),
108
+ worktreeBootstrap: loadMeshWorktreeBootstrapConfig(mesh, workspace),
109
+ changeImpact: loadChangeImpactConfig(repoRoot),
110
+ };
111
+ }
package/src/index.ts CHANGED
@@ -199,7 +199,7 @@ export type { CreateMeshOptions, UpdateMeshOptions, AddNodeOptions } from './con
199
199
  // e.g. the mcp-server, which depends only on @adhdev/daemon-core — can
200
200
  // canonicalize daemon-id and node-id forms without taking a direct
201
201
  // @adhdev/mesh-shared dependency). ──
202
- export { expandDaemonIdForms, daemonIdsEquivalent, machineCoreFromDaemonId } from '@adhdev/mesh-shared';
202
+ export { expandDaemonIdForms, daemonIdsEquivalent, machineCoreFromDaemonId, canonicalDaemonId } from '@adhdev/mesh-shared';
203
203
  export { normalizeMeshNodeId, meshNodeIdMatches } from '@adhdev/mesh-shared';
204
204
 
205
205
  // ── Mesh Coordinator ──
@@ -237,6 +237,11 @@ export type {
237
237
  RepoMeshRefineConfig,
238
238
  RepoMeshRefineValidationCommandConfig,
239
239
  } from './mesh/refine-config.js';
240
+ // Unified repo-settings loader: assembles the separate `.adhdev/*` config files
241
+ // (mesh.json coordinator/operatingNotes, refine, worktree-bootstrap, change-impact)
242
+ // into one object. Policy is machine-local and not part of repo settings.
243
+ export { loadRepoSettings } from './config/repo-settings.js';
244
+ export type { RepoSettings, LoadRepoSettingsOptions } from './config/repo-settings.js';
240
245
 
241
246
  // ── Mesh Task Ledger ──
242
247
  export { appendLedgerEntry, appendRemoteLedgerEntries, buildTaskCompletionEvidence, normalizeMeshWorkerResult, readLedgerEntries, readLedgerSlice, readLedgerSliceFromStore, getLedgerSummary, getLedgerDir, getSessionRecoveryContext, MAX_LEDGER_SLICE_LIMIT } from './mesh/mesh-ledger.js';
@@ -49,6 +49,16 @@ export type MeshLedgerKind =
49
49
  // the ledger so it survives coordinator restarts and is provider-neutral.
50
50
  // payload: { text, category?, createdAt?, sourceCoordinator? }
51
51
  | 'coordinator_operating_note'
52
+ // Mission audit trail: mission record mutations (mesh_mission_upsert) so the
53
+ // ledger captures mission lifecycle, not just task events. Without these a
54
+ // mission create / goal rewrite / status transition left no ledger trace,
55
+ // breaking audit continuity and post-restart recovery.
56
+ // mission_created payload: { missionId, title, goalSummary, goalLength, goalTruncated, status }
57
+ // mission_status_changed payload: { missionId, title, fromStatus, toStatus }
58
+ // mission_goal_updated payload: { missionId, title, prevGoalSummary, nextGoalSummary, prevGoalLength, nextGoalLength, goalTruncated }
59
+ | 'mission_created'
60
+ | 'mission_status_changed'
61
+ | 'mission_goal_updated'
52
62
  ;
53
63
 
54
64
  export interface MeshLedgerEntry {
@@ -16,6 +16,19 @@ import { randomUUID } from 'crypto';
16
16
  import { MeshRuntimeStore } from './mesh-runtime-store.js';
17
17
  import { getQueue } from './mesh-work-queue.js';
18
18
  import { computeMeshMissionStats, type MeshMissionStats } from './mesh-task-stats.js';
19
+ import { appendLedgerEntry } from './mesh-ledger.js';
20
+
21
+ /**
22
+ * Max chars of mission goal text written into a ledger entry payload. Mission
23
+ * goals can be hundreds–thousands of chars; the ledger is an append-only audit
24
+ * stream, so we store a bounded summary (+ a length/truncated flag) rather than
25
+ * the full text to keep the ledger from bloating on repeated goal rewrites.
26
+ */
27
+ const LEDGER_GOAL_SUMMARY_MAX = 200;
28
+
29
+ function summarizeGoalForLedger(goal: string): string {
30
+ return goal.length > LEDGER_GOAL_SUMMARY_MAX ? goal.slice(0, LEDGER_GOAL_SUMMARY_MAX) : goal;
31
+ }
19
32
 
20
33
  export type MeshMissionStatus = 'active' | 'paused' | 'completed' | 'abandoned';
21
34
 
@@ -102,6 +115,8 @@ export function upsertMeshMission(meshId: string, input: {
102
115
  const id = typeof input.id === 'string' && input.id.trim() ? input.id.trim() : randomUUID();
103
116
  const store = MeshRuntimeStore.getInstance();
104
117
  const existing = store.getMission(meshId, id);
118
+ const prevStatus = existing ? normalizeMissionStatus(existing.status) : null;
119
+ const prevGoal = existing?.goal ?? '';
105
120
  const record = {
106
121
  id,
107
122
  meshId,
@@ -111,7 +126,77 @@ export function upsertMeshMission(meshId: string, input: {
111
126
  };
112
127
  store.upsertMission(record);
113
128
  const saved = store.getMission(meshId, id)!;
114
- return { ...saved, status: normalizeMissionStatus(saved.status) };
129
+ const result: MeshMissionRecord = { ...saved, status: normalizeMissionStatus(saved.status) };
130
+
131
+ // Mission audit trail: record this mutation in the mesh ledger so mission
132
+ // lifecycle (create, goal rewrite, status transition) is auditable alongside
133
+ // task events and survives a coordinator restart. Best-effort: a ledger
134
+ // failure must never break the primary mission write.
135
+ appendMissionLedgerEntries(meshId, {
136
+ isCreate: !existing,
137
+ record: result,
138
+ prevStatus,
139
+ prevGoal,
140
+ });
141
+
142
+ return result;
143
+ }
144
+
145
+ /**
146
+ * Append the relevant ledger entries for a mission upsert. Emits at most one
147
+ * entry per distinct change:
148
+ * - new mission → mission_created
149
+ * - status differs from prior → mission_status_changed
150
+ * - goal text differs from prior → mission_goal_updated (no-op rewrites skipped)
151
+ */
152
+ function appendMissionLedgerEntries(
153
+ meshId: string,
154
+ args: { isCreate: boolean; record: MeshMissionRecord; prevStatus: MeshMissionStatus | null; prevGoal: string },
155
+ ): void {
156
+ const { isCreate, record, prevStatus, prevGoal } = args;
157
+ try {
158
+ if (isCreate) {
159
+ const goal = record.goal ?? '';
160
+ appendLedgerEntry(meshId, {
161
+ kind: 'mission_created',
162
+ payload: {
163
+ missionId: record.id,
164
+ title: record.title,
165
+ goalSummary: summarizeGoalForLedger(goal),
166
+ goalLength: goal.length,
167
+ goalTruncated: goal.length > LEDGER_GOAL_SUMMARY_MAX,
168
+ status: record.status,
169
+ },
170
+ });
171
+ return;
172
+ }
173
+ if (prevStatus !== null && prevStatus !== record.status) {
174
+ appendLedgerEntry(meshId, {
175
+ kind: 'mission_status_changed',
176
+ payload: {
177
+ missionId: record.id,
178
+ title: record.title,
179
+ fromStatus: prevStatus,
180
+ toStatus: record.status,
181
+ },
182
+ });
183
+ }
184
+ const nextGoal = record.goal ?? '';
185
+ if (nextGoal !== prevGoal) {
186
+ appendLedgerEntry(meshId, {
187
+ kind: 'mission_goal_updated',
188
+ payload: {
189
+ missionId: record.id,
190
+ title: record.title,
191
+ prevGoalSummary: summarizeGoalForLedger(prevGoal),
192
+ nextGoalSummary: summarizeGoalForLedger(nextGoal),
193
+ prevGoalLength: prevGoal.length,
194
+ nextGoalLength: nextGoal.length,
195
+ goalTruncated: prevGoal.length > LEDGER_GOAL_SUMMARY_MAX || nextGoal.length > LEDGER_GOAL_SUMMARY_MAX,
196
+ },
197
+ });
198
+ }
199
+ } catch { /* audit trail is best-effort; never break the mission write */ }
115
200
  }
116
201
 
117
202
  export function getMeshMissions(meshId: string, statuses?: MeshMissionStatus[]): MeshMissionRecord[] {