@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.
- package/dist/commands/med-family/mesh-crud.d.ts +27 -0
- package/dist/config/mesh-json-config.d.ts +31 -131
- package/dist/config/repo-settings.d.ts +77 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +372 -419
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +360 -408
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-ledger.d.ts +1 -1
- package/package.json +2 -2
- package/src/commands/high-family/mesh-coordinator-launch.ts +9 -4
- package/src/commands/med-family/mesh-crud.ts +85 -8
- package/src/config/mesh-json-config.ts +40 -281
- package/src/config/repo-settings.ts +111 -0
- package/src/index.ts +5 -0
- package/src/mesh/mesh-ledger.ts +10 -0
- package/src/mesh/mesh-missions.ts +86 -1
- package/src/mesh/mesh-queue-assignment.ts +15 -51
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { EventEmitter } from 'events';
|
|
16
16
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
17
|
-
export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'task_reclaimed' | 'coordinator_operating_note';
|
|
17
|
+
export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'task_reclaimed' | 'coordinator_operating_note' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated';
|
|
18
18
|
export interface MeshLedgerEntry {
|
|
19
19
|
id: string;
|
|
20
20
|
meshId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.412",
|
|
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.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.412",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -177,10 +177,15 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
|
|
|
177
177
|
|
|
178
178
|
// OPSRULES — layer the repo-shared declarative mesh config
|
|
179
179
|
// (.adhdev/mesh.json in the coordinator node workspace, else
|
|
180
|
-
// the calling cwd) UNDER the machine-local mesh entry.
|
|
181
|
-
//
|
|
182
|
-
// is never
|
|
183
|
-
//
|
|
180
|
+
// the calling cwd) UNDER the machine-local mesh entry. Only the
|
|
181
|
+
// COORDINATOR zone (prompt override/append) is merged — policy is
|
|
182
|
+
// machine-local and is never sourced from the repo file. The merge
|
|
183
|
+
// is in-memory only: meshes.json on disk is never mutated. Coordinator
|
|
184
|
+
// launch needs only the mesh.json zone (coordinator + operatingNotes),
|
|
185
|
+
// so it reads that dedicated loader directly rather than the unified
|
|
186
|
+
// loadRepoSettings (which would also touch refine/bootstrap via the
|
|
187
|
+
// machine-local inline seam). The effective mesh feeds prompt
|
|
188
|
+
// building; operating notes are merged with the runtime ledger below.
|
|
184
189
|
const { loadRepoMeshJsonConfig, applyRepoMeshConfig, mergeEffectiveOperatingNotes } =
|
|
185
190
|
await import('../../config/mesh-json-config.js');
|
|
186
191
|
const repoMeshConfigLoad = loadRepoMeshJsonConfig(workspace);
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
runMeshWorktreeBootstrap,
|
|
17
17
|
type WorktreeBootstrapState,
|
|
18
18
|
} from '../../mesh/worktree-bootstrap-config.js';
|
|
19
|
+
import { loadRepoSettings } from '../../config/repo-settings.js';
|
|
19
20
|
import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../../mesh/mesh-events.js';
|
|
20
21
|
import { loadConfig } from '../../config/config.js';
|
|
21
22
|
import {
|
|
@@ -24,8 +25,61 @@ import {
|
|
|
24
25
|
readMeshNodeMachineId,
|
|
25
26
|
} from '../router.js';
|
|
26
27
|
import type { CommandRouterResult } from '../router.js';
|
|
28
|
+
import type { GitRepoIdentity } from '../../git/git-types.js';
|
|
27
29
|
import type { MedFamilyContext, MedFamilyHandler } from './types.js';
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Decision for syncing a freshly-cloned worktree's `oss` submodule to its clone
|
|
33
|
+
* source node, applying resolveWorktreeBaseStartPoint's origin-tip-priority
|
|
34
|
+
* policy to the submodule.
|
|
35
|
+
*
|
|
36
|
+
* On clone, `submodule update --init` checks the worktree's `oss` out at the
|
|
37
|
+
* gitlink recorded in the FRESH root base — which the root base-stale fix
|
|
38
|
+
* branches from origin/main, so it is the up-to-date origin tip. The clone
|
|
39
|
+
* source node's *working* `oss` SHA can lag that tip. The original sync blindly
|
|
40
|
+
* checked out the source SHA whenever it merely differed, which REWINDS the
|
|
41
|
+
* submodule back onto the stale source and re-introduces staleness.
|
|
42
|
+
*
|
|
43
|
+
* Guard policy (never rewind, mirror the base-start-point resolver):
|
|
44
|
+
* - source SHA == worktree SHA → `noop`
|
|
45
|
+
* - source SHA is an ancestor of worktree → `skip_rewind` (source is behind; keep fresh tip)
|
|
46
|
+
* - worktree SHA is an ancestor of source → `advance` (source strictly newer; safe fast-forward)
|
|
47
|
+
* - neither is an ancestor (diverged) → `skip_diverged` (keep fresh tip; coordinator reconciles)
|
|
48
|
+
*
|
|
49
|
+
* Both SHAs must already be resolvable in `ossCtx` (the caller fetches the
|
|
50
|
+
* source SHA first). A non-1 git exit (unresolvable SHA / real failure) is
|
|
51
|
+
* rethrown so the caller can fall back to keeping the fresh worktree HEAD.
|
|
52
|
+
*/
|
|
53
|
+
export type OssCloneSyncAction = 'noop' | 'advance' | 'skip_rewind' | 'skip_diverged';
|
|
54
|
+
|
|
55
|
+
export async function decideOssCloneSync(
|
|
56
|
+
ossCtx: GitRepoIdentity,
|
|
57
|
+
worktreeOssSha: string,
|
|
58
|
+
sourceSha: string,
|
|
59
|
+
rg: (ctx: GitRepoIdentity, argv: string[], opts?: { timeoutMs?: number }) => Promise<unknown>,
|
|
60
|
+
): Promise<OssCloneSyncAction> {
|
|
61
|
+
if (!worktreeOssSha || !sourceSha || worktreeOssSha === sourceSha) return 'noop';
|
|
62
|
+
|
|
63
|
+
const isAncestor = async (ancestor: string, descendant: string): Promise<boolean> => {
|
|
64
|
+
try {
|
|
65
|
+
await rg(ossCtx, ['merge-base', '--is-ancestor', ancestor, descendant], { timeoutMs: 10000 });
|
|
66
|
+
return true;
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
// `merge-base --is-ancestor` exits 1 for a clean "not an ancestor".
|
|
69
|
+
// Any other exit (128 = unresolvable commit, etc.) is a real failure.
|
|
70
|
+
if (err?.exitCode === 1 || err?.code === 1) return false;
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Source is an ancestor of the fresh worktree tip → checking it out rewinds.
|
|
76
|
+
if (await isAncestor(sourceSha, worktreeOssSha)) return 'skip_rewind';
|
|
77
|
+
// Worktree tip is an ancestor of source → source is strictly newer → safe FF.
|
|
78
|
+
if (await isAncestor(worktreeOssSha, sourceSha)) return 'advance';
|
|
79
|
+
// Neither is an ancestor → diverged → keep the fresh worktree HEAD.
|
|
80
|
+
return 'skip_diverged';
|
|
81
|
+
}
|
|
82
|
+
|
|
29
83
|
export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
30
84
|
list_meshes: async (_ctx: MedFamilyContext, _args: any) => {
|
|
31
85
|
try {
|
|
@@ -129,7 +183,8 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
129
183
|
// entry. This is an export scaffold for the operator to review and commit to
|
|
130
184
|
// the repo, NOT an automatic data migration: nothing is written to disk and
|
|
131
185
|
// meshes.json is untouched. The returned `scaffold` (object) + `scaffoldJson`
|
|
132
|
-
// (2-space text) capture the
|
|
186
|
+
// (2-space text) capture the coordinator prompt override/append (policy is
|
|
187
|
+
// machine-local and is intentionally NOT exported into mesh.json).
|
|
133
188
|
export_mesh_json_config: async (_ctx: MedFamilyContext, args: any) => {
|
|
134
189
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
135
190
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
@@ -637,7 +692,10 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
637
692
|
};
|
|
638
693
|
|
|
639
694
|
const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
|
|
640
|
-
|
|
695
|
+
// Read the worktree bootstrap config through the unified RepoSettings
|
|
696
|
+
// loader (file-separated `.adhdev/worktree_bootstrap.json`; machine-local
|
|
697
|
+
// inline seam honored). runMeshWorktreeBootstrap below re-loads it to run.
|
|
698
|
+
const loadedBootstrap = loadRepoSettings({ workspace: result.worktreePath, mesh }).worktreeBootstrap;
|
|
641
699
|
const runningBootstrapState: WorktreeBootstrapState = {
|
|
642
700
|
status: 'running',
|
|
643
701
|
required: loadedBootstrap.config?.required !== false,
|
|
@@ -679,13 +737,32 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
679
737
|
const worktreeOssHeadOut = await rg(ossCtx, ['rev-parse', 'HEAD'], { timeoutMs: 10000 });
|
|
680
738
|
const worktreeOssSha = (typeof worktreeOssHeadOut === 'string' ? worktreeOssHeadOut : (worktreeOssHeadOut as any)?.stdout ?? '').trim();
|
|
681
739
|
|
|
682
|
-
if (worktreeOssSha !== sourceSha) {
|
|
683
|
-
//
|
|
740
|
+
if (worktreeOssSha && worktreeOssSha !== sourceSha) {
|
|
741
|
+
// Bring the source node's oss HEAD into the worktree object DB so
|
|
742
|
+
// both SHAs are resolvable for the ancestry (rewind) guard below.
|
|
684
743
|
await rg(ossCtx, ['fetch', `${sourceWorkspace}/oss`, 'HEAD'], { timeoutMs: 60000 });
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
744
|
+
|
|
745
|
+
// Rewind guard: the worktree oss HEAD was just checked out from the
|
|
746
|
+
// FRESH (origin/main-derived) root base. Only advance to the source
|
|
747
|
+
// SHA when it is strictly newer — never rewind to a stale source.
|
|
748
|
+
let ossAction: OssCloneSyncAction;
|
|
749
|
+
try {
|
|
750
|
+
ossAction = await decideOssCloneSync(ossCtx, worktreeOssSha, sourceSha, rg);
|
|
751
|
+
} catch (decideErr: any) {
|
|
752
|
+
ossAction = 'skip_diverged';
|
|
753
|
+
console.warn(`[mesh] oss submodule sync guard could not resolve ancestry (kept fresh worktree HEAD): ${decideErr?.message ?? decideErr}`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (ossAction === 'advance') {
|
|
757
|
+
await rg(ossCtx, ['checkout', sourceSha], { timeoutMs: 10000 });
|
|
758
|
+
await rg(worktreeCtx, ['add', 'oss'], { timeoutMs: 10000 });
|
|
759
|
+
await rg(worktreeCtx, ['commit', '-m', 'chore: sync oss to source node HEAD on clone'], { timeoutMs: 10000 });
|
|
760
|
+
console.log(`[mesh] Advanced oss submodule to newer source HEAD ${sourceSha.slice(0, 8)} in worktree`);
|
|
761
|
+
} else if (ossAction === 'skip_rewind') {
|
|
762
|
+
console.warn(`[mesh] Skipped oss submodule rewind on clone: source node oss ${sourceSha.slice(0, 8)} is an ancestor of the fresh worktree oss ${worktreeOssSha.slice(0, 8)} — kept fresher worktree HEAD`);
|
|
763
|
+
} else if (ossAction === 'skip_diverged') {
|
|
764
|
+
console.warn(`[mesh] Skipped oss submodule sync on clone: source node oss ${sourceSha.slice(0, 8)} diverged from the fresh worktree oss ${worktreeOssSha.slice(0, 8)} — kept worktree HEAD (coordinator reconciles)`);
|
|
765
|
+
}
|
|
689
766
|
}
|
|
690
767
|
}
|
|
691
768
|
} catch (ossErr: any) {
|
|
@@ -1,77 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Repo-shared declarative mesh config — `.adhdev/mesh.json`
|
|
3
3
|
*
|
|
4
|
-
* A repo-committed, machine-independent
|
|
5
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
-
*
|
|
41
|
-
*
|
|
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
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
|
34
|
+
import { existsSync, readFileSync } from 'fs';
|
|
62
35
|
import { join } from 'path';
|
|
63
36
|
import * as yaml from 'js-yaml';
|
|
64
|
-
import {
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 (
|
|
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
|
|
444
|
-
*
|
|
445
|
-
*
|
|
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, '
|
|
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
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
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, '
|
|
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
|
-
}
|