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

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.
@@ -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
@@ -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[] {
@@ -13,9 +13,8 @@ import { createSessionDelivery, updateSessionDeliveryStatus } from './mesh-deliv
13
13
  import { MeshRuntimeStore } from './mesh-runtime-store.js';
14
14
  import { traceMeshEventDrop } from './mesh-event-trace.js';
15
15
  import { awaitWithWarmupDeadline, resolveWarmupDeadlineOpts } from './mesh-warmup-deadline.js';
16
- import { resolveDelegatedWorkerAutoApprove, resolveProviderMaxParallel, resolveNodeSchedulingPriority, normalizeMeshSchedulingStrategy, resolveMaxParallelTasks, resolveMaxReadonlyParallelTasks, distributionToStrategy } from '../repo-mesh-types.js';
16
+ import { resolveDelegatedWorkerAutoApprove, resolveProviderMaxParallel, resolveNodeSchedulingPriority, normalizeMeshSchedulingStrategy, resolveMaxParallelTasks, resolveMaxReadonlyParallelTasks } from '../repo-mesh-types.js';
17
17
  import type { RepoMeshSchedulingStrategy } from '../repo-mesh-types.js';
18
- import { loadMeshJsonConfig, type MeshJsonSchedulingConfig } from '../config/mesh-json-config.js';
19
18
  import { normalizeMeshNodeId, meshNodeIdMatches, daemonIdsEquivalent, normalizeMeshWorkspaceForCompare, meshWorkspacesEquivalent, type MeshNodeIdentified } from '@adhdev/mesh-shared';
20
19
  import { findTerminalLedgerEvidenceForTask, hasUnterminalDirectDispatchLedgerEntry } from './mesh-events-stale.js';
21
20
  import { readNonEmptyString } from './mesh-events-utils.js';
@@ -825,46 +824,15 @@ function nodeActiveLoad(meshId: string, nodeId: string): number {
825
824
  }
826
825
 
827
826
  /**
828
- * Resolve the canonical repo root for reading a mesh's in-tree `.adhdev/mesh.json`
829
- * overlay. Prefers a base (non-worktree) node's repoRoot/workspace — the canonical
830
- * checkout that carries the repo file and falls back to any node so a worktree-only
831
- * mesh still resolves a root. Returns '' when no node declares a path.
832
- */
833
- function resolveMeshRepoRootForScheduling(mesh: any): string {
834
- const nodes = Array.isArray(mesh?.nodes) ? mesh.nodes : [];
835
- const pickRoot = (n: any) => readNonEmptyString(n?.repoRoot) || readNonEmptyString(n?.workspace);
836
- const base = nodes.find((n: any) => n?.isLocalWorktree !== true && pickRoot(n));
837
- if (base) return pickRoot(base);
838
- const anyNode = nodes.find((n: any) => pickRoot(n));
839
- return anyNode ? pickRoot(anyNode) : '';
840
- }
841
-
842
- /**
843
- * The repo-local `.adhdev/mesh.json` `policy.scheduling` overlay for this mesh, when
844
- * present and valid. LOCAL-WINS: a value here overrides the stored mesh policy. Cached
845
- * by mtime in the loader, so calling this on every reconcile tick is cheap.
846
- */
847
- function resolveMeshSchedulingOverride(mesh: any): MeshJsonSchedulingConfig | undefined {
848
- const repoRoot = resolveMeshRepoRootForScheduling(mesh);
849
- if (!repoRoot) return undefined;
850
- try {
851
- return loadMeshJsonConfig(repoRoot).config?.scheduling;
852
- } catch {
853
- return undefined;
854
- }
855
- }
856
-
857
- /**
858
- * The mesh-wide scheduling strategy. Resolution order (LOCAL-WINS):
859
- * 1. `.adhdev/mesh.json` policy.scheduling.distribution (2-mode → strategy), then
860
- * 2. the stored mesh policy schedulingStrategy raw 4-union (escape hatch), then
861
- * 3. 'first_eligible' (strict no-change default).
862
- * Only governs the final tie-break; eligibility, capacity, and priority gates apply
863
- * identically to every strategy.
827
+ * The mesh-wide scheduling strategy, read from the MACHINE-LOCAL stored mesh
828
+ * policy. Resolution order:
829
+ * 1. the stored mesh policy schedulingStrategy raw 4-union, then
830
+ * 2. 'first_eligible' (strict no-change default).
831
+ * Policy is machine-local only — there is no repo-file (`.adhdev/mesh.json`)
832
+ * overlay. Only governs the final tie-break; eligibility, capacity, and priority
833
+ * gates apply identically to every strategy.
864
834
  */
865
835
  function resolveSchedulingStrategy(mesh: any): RepoMeshSchedulingStrategy {
866
- const override = resolveMeshSchedulingOverride(mesh);
867
- if (override?.distribution) return distributionToStrategy(override.distribution);
868
836
  return normalizeMeshSchedulingStrategy(mesh?.policy?.schedulingStrategy);
869
837
  }
870
838
 
@@ -1131,19 +1099,15 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
1131
1099
  const pending = queue.filter(task => task.status === 'pending');
1132
1100
  if (!pending.length) return false;
1133
1101
 
1134
- // Write cap + read-only cap resolved through the shared helpers, with the
1135
- // repo-local `.adhdev/mesh.json` overlay winning over the stored policy
1136
- // (LOCAL-WINS). Both the cap value and the read-only multiplier route through
1137
- // the same resolvers the observability projection uses, so the enforced and
1138
- // exposed caps can never drift.
1139
- const schedulingOverride = resolveMeshSchedulingOverride(mesh);
1140
- const maxParallelTasks = resolveMaxParallelTasks(
1141
- schedulingOverride?.maxParallel ?? mesh?.policy?.maxParallelTasks,
1142
- );
1102
+ // Write cap + read-only cap resolved through the shared helpers from the
1103
+ // MACHINE-LOCAL stored mesh policy (no repo-file overlay). These are the same
1104
+ // resolvers the observability projection uses, so the enforced and exposed
1105
+ // caps can never drift.
1106
+ const maxParallelTasks = resolveMaxParallelTasks(mesh?.policy?.maxParallelTasks);
1143
1107
  // Read-only diagnoses carry no isolation/merge cost, so they are exempt from the
1144
1108
  // write-task parallel cap. To prevent runaway auto-launch they get their own,
1145
- // higher safety cap (readonlyMultiplier × the write cap, default 2×).
1146
- const maxReadonlyParallelTasks = resolveMaxReadonlyParallelTasks(maxParallelTasks, schedulingOverride?.readonlyMultiplier);
1109
+ // higher safety cap (default 2× the write cap).
1110
+ const maxReadonlyParallelTasks = resolveMaxReadonlyParallelTasks(maxParallelTasks);
1147
1111
  for (const task of pending) {
1148
1112
  const isReadonly = isTaskReadonly(task);
1149
1113
  if (isReadonly) {