@adhdev/daemon-core 0.9.66 → 0.9.67

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,18 @@
1
+ /**
2
+ * Coordinator Prompt — System prompt template for mesh coordinator sessions
3
+ *
4
+ * When an MCP server starts in mesh mode, this prompt is injected into the
5
+ * coordinator agent's context so it understands:
6
+ * 1. What the mesh is (repo, nodes, policy)
7
+ * 2. What tools are available
8
+ * 3. How to orchestrate work across nodes
9
+ *
10
+ * The prompt is generated dynamically from the current mesh state.
11
+ */
12
+ import type { LocalMeshEntry, RepoMeshStatus } from '../repo-mesh-types.js';
13
+ export interface CoordinatorPromptContext {
14
+ mesh: LocalMeshEntry;
15
+ status?: RepoMeshStatus;
16
+ userInstruction?: string;
17
+ }
18
+ export declare function buildCoordinatorSystemPrompt(ctx: CoordinatorPromptContext): string;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Mesh Sync — Sync local mesh config to/from cloud D1
3
+ *
4
+ * When cloud is available, this module pushes local mesh config
5
+ * to the server and pulls remote meshes that were created from
6
+ * other machines. The local ~/.adhdev/meshes.json remains the
7
+ * canonical source; cloud is a persistence/relay layer.
8
+ *
9
+ * This is called lazily (not on daemon startup) — only when the
10
+ * user explicitly opens the mesh page or runs `adhdev mesh sync`.
11
+ */
12
+ export interface MeshSyncTransport {
13
+ /** GET /api/v1/repo-meshes */
14
+ listRemoteMeshes(): Promise<{
15
+ meshes: RemoteMeshRecord[];
16
+ }>;
17
+ /** POST /api/v1/repo-meshes */
18
+ createRemoteMesh(data: {
19
+ name: string;
20
+ repo_identity: string;
21
+ repo_remote_url?: string;
22
+ default_branch?: string;
23
+ policy?: string;
24
+ }): Promise<{
25
+ mesh: RemoteMeshRecord;
26
+ }>;
27
+ /** DELETE /api/v1/repo-meshes/:id */
28
+ deleteRemoteMesh(meshId: string): Promise<void>;
29
+ }
30
+ export interface RemoteMeshRecord {
31
+ id: string;
32
+ name: string;
33
+ repo_identity: string;
34
+ repo_remote_url: string | null;
35
+ default_branch: string | null;
36
+ policy: string;
37
+ status: string;
38
+ created_at: string;
39
+ updated_at: string;
40
+ }
41
+ export interface MeshSyncResult {
42
+ pushed: number;
43
+ pulled: number;
44
+ deleted: number;
45
+ errors: string[];
46
+ }
47
+ /**
48
+ * Push local meshes to cloud (upsert by repo_identity).
49
+ * Pull remote meshes that don't exist locally.
50
+ */
51
+ export declare function syncMeshes(transport: MeshSyncTransport): Promise<MeshSyncResult>;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Repo Mesh Types — Cross-package type definitions for repo-scoped orchestration
3
+ *
4
+ * A Repo Mesh is a repo-scoped execution environment that groups
5
+ * machines/workspaces around one Git repository identity. A coordinator
6
+ * agent delegates work to mesh nodes via natural conversation.
7
+ *
8
+ * These types are OSS-level and usable without cloud infrastructure.
9
+ * Import via: import type { ... } from '@adhdev/daemon-core/repo-mesh-types'
10
+ *
11
+ * IMPORTANT: This file must remain runtime-free (types only).
12
+ */
13
+ import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
14
+ export interface RepoMesh {
15
+ id: string;
16
+ name: string;
17
+ repoIdentity: string;
18
+ repoRemoteUrl?: string;
19
+ defaultBranch?: string;
20
+ policy: RepoMeshPolicy;
21
+ coordinator: RepoMeshCoordinatorConfig;
22
+ projectContext: ProjectContextSnapshot;
23
+ nodes: RepoMeshNode[];
24
+ status: 'active' | 'archived' | 'deleted';
25
+ }
26
+ export interface RepoMeshNode {
27
+ id: string;
28
+ daemonId: string;
29
+ machineId?: string;
30
+ machineLabel: string;
31
+ workspace: string;
32
+ repoRoot?: string;
33
+ git?: GitCompactSummary;
34
+ providers: string[];
35
+ detectedCapabilities: RepoMeshNodeCapabilities;
36
+ userOverrides: Partial<RepoMeshNodeCapabilities>;
37
+ effectiveCapabilities: RepoMeshNodeCapabilities;
38
+ policy: RepoMeshNodePolicy;
39
+ health: RepoMeshNodeHealth;
40
+ status: 'enabled' | 'disabled' | 'removed';
41
+ }
42
+ export type RepoMeshNodeHealth = 'online' | 'offline' | 'degraded' | 'dirty' | 'wrong_branch' | 'unknown';
43
+ export interface RepoMeshPolicy {
44
+ requirePreTaskCheckpoint: boolean;
45
+ requirePostTaskCheckpoint: boolean;
46
+ requireApprovalForPush: boolean;
47
+ requireApprovalForDestructiveGit: boolean;
48
+ dirtyWorkspaceBehavior: 'block' | 'warn' | 'checkpoint_then_continue';
49
+ maxParallelTasks: number;
50
+ allowedProviders?: string[];
51
+ }
52
+ export interface RepoMeshNodePolicy {
53
+ readOnly?: boolean;
54
+ canPush?: boolean;
55
+ maxConcurrentSessions?: number;
56
+ }
57
+ export declare const DEFAULT_MESH_POLICY: RepoMeshPolicy;
58
+ export interface RepoMeshNodeCapabilities {
59
+ platform?: string;
60
+ packageManagers?: string[];
61
+ detectedCommands?: DetectedCommand[];
62
+ canRunLongJobs?: boolean;
63
+ canRunDocker?: boolean;
64
+ canRunBrowserE2E?: boolean;
65
+ canAccessSecrets?: boolean;
66
+ canPush?: boolean;
67
+ readOnly?: boolean;
68
+ userLabels?: string[];
69
+ }
70
+ export interface DetectedCommand {
71
+ command: string;
72
+ sourcePath: string;
73
+ confidence: 'high' | 'medium' | 'low';
74
+ requiresApproval?: boolean;
75
+ }
76
+ export interface ProjectContextSnapshot {
77
+ version: number;
78
+ generatedAt: string;
79
+ sources: ProjectContextSource[];
80
+ repo: {
81
+ identity: string;
82
+ remoteUrl?: string;
83
+ defaultBranch?: string;
84
+ currentBranches: string[];
85
+ };
86
+ layout: {
87
+ packageManager?: string;
88
+ workspaceFiles: string[];
89
+ packageRoots: string[];
90
+ likelyEntryPoints: string[];
91
+ };
92
+ commands: {
93
+ build?: DetectedCommand[];
94
+ test?: DetectedCommand[];
95
+ typecheck?: DetectedCommand[];
96
+ lint?: DetectedCommand[];
97
+ e2e?: DetectedCommand[];
98
+ };
99
+ instructions: {
100
+ files: string[];
101
+ summary: string;
102
+ };
103
+ conventions: {
104
+ pathHints: string[];
105
+ validationNotes: string[];
106
+ riskyAreas: string[];
107
+ };
108
+ }
109
+ export interface ProjectContextSource {
110
+ kind: 'daemon_status' | 'git' | 'project_file' | 'instruction_file' | 'user_override' | 'probe_error';
111
+ nodeId?: string;
112
+ path?: string;
113
+ observedAt: string;
114
+ confidence: 'high' | 'medium' | 'low';
115
+ }
116
+ export interface RepoMeshCoordinatorConfig {
117
+ /** Provider to use for coordinator session (e.g. 'claude-cli', 'cursor') */
118
+ providerType?: string;
119
+ /** Preferred node to run coordinator on (null = auto) */
120
+ preferredNodeId?: string;
121
+ /** Additional system prompt context for coordinator */
122
+ systemPromptSuffix?: string;
123
+ }
124
+ /**
125
+ * Local mesh configuration stored in ~/.adhdev/meshes.json
126
+ * Used by OSS standalone mode without cloud infrastructure.
127
+ */
128
+ export interface LocalMeshConfig {
129
+ meshes: LocalMeshEntry[];
130
+ }
131
+ export interface LocalMeshEntry {
132
+ id: string;
133
+ name: string;
134
+ repoIdentity: string;
135
+ repoRemoteUrl?: string;
136
+ defaultBranch?: string;
137
+ policy: RepoMeshPolicy;
138
+ coordinator: RepoMeshCoordinatorConfig;
139
+ nodes: LocalMeshNodeEntry[];
140
+ createdAt: string;
141
+ updatedAt: string;
142
+ }
143
+ export interface LocalMeshNodeEntry {
144
+ id: string;
145
+ workspace: string;
146
+ repoRoot?: string;
147
+ userOverrides: Partial<RepoMeshNodeCapabilities>;
148
+ policy: RepoMeshNodePolicy;
149
+ /** For single-machine mesh: same daemon, different worktree */
150
+ isLocalWorktree?: boolean;
151
+ }
152
+ export interface RepoMeshStatus {
153
+ meshId: string;
154
+ meshName: string;
155
+ repoIdentity: string;
156
+ refreshedAt: string;
157
+ nodes: RepoMeshNodeStatus[];
158
+ }
159
+ export interface RepoMeshNodeStatus {
160
+ nodeId: string;
161
+ machineLabel: string;
162
+ workspace: string;
163
+ health: RepoMeshNodeHealth;
164
+ git?: GitRepoStatus;
165
+ providers: string[];
166
+ activeSessions: string[];
167
+ error?: string;
168
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.9.66",
3
+ "version": "0.9.67",
4
4
  "description": "ADHDev local session host core \u2014 session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.66",
3
+ "version": "0.9.67",
4
4
  "description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Repo Mesh Config — Local mesh configuration stored in ~/.adhdev/meshes.json
3
+ *
4
+ * Manages repo mesh definitions for OSS standalone mode.
5
+ * Cloud mode syncs these to D1 via server routes; standalone mode
6
+ * uses this file as the single source of truth.
7
+ */
8
+
9
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { randomUUID } from 'crypto';
12
+ import { getConfigDir } from './config.js';
13
+ import type {
14
+ LocalMeshConfig,
15
+ LocalMeshEntry,
16
+ LocalMeshNodeEntry,
17
+ RepoMeshPolicy,
18
+ RepoMeshNodePolicy,
19
+ RepoMeshNodeCapabilities,
20
+ RepoMeshCoordinatorConfig,
21
+ } from '../repo-mesh-types.js';
22
+ import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
23
+
24
+ // ─── Persistence ────────────────────────────────
25
+
26
+ function getMeshConfigPath(): string {
27
+ return join(getConfigDir(), 'meshes.json');
28
+ }
29
+
30
+ function loadMeshConfig(): LocalMeshConfig {
31
+ const path = getMeshConfigPath();
32
+ if (!existsSync(path)) return { meshes: [] };
33
+ try {
34
+ const raw = JSON.parse(readFileSync(path, 'utf-8'));
35
+ if (!raw || !Array.isArray(raw.meshes)) return { meshes: [] };
36
+ return raw as LocalMeshConfig;
37
+ } catch {
38
+ return { meshes: [] };
39
+ }
40
+ }
41
+
42
+ function saveMeshConfig(config: LocalMeshConfig): void {
43
+ const path = getMeshConfigPath();
44
+ writeFileSync(path, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
45
+ }
46
+
47
+ // ─── Repo Identity Normalization ────────────────
48
+
49
+ /**
50
+ * Normalize a Git remote URL into a stable identity string.
51
+ * e.g. "git@github.com:user/repo.git" → "github.com/user/repo"
52
+ * "https://github.com/user/repo.git" → "github.com/user/repo"
53
+ */
54
+ export function normalizeRepoIdentity(remoteUrl: string): string {
55
+ let identity = remoteUrl.trim();
56
+
57
+ // HTTPS format first (takes priority over SSH fallback)
58
+ if (identity.startsWith('http://') || identity.startsWith('https://')) {
59
+ try {
60
+ const url = new URL(identity);
61
+ const path = url.pathname.replace(/^\//, '').replace(/\.git$/, '');
62
+ return `${url.hostname}/${path}`;
63
+ } catch {
64
+ // fall through
65
+ }
66
+ }
67
+
68
+ // SSH format: git@host:owner/repo.git or ssh://git@host/owner/repo.git
69
+ const sshMatch = identity.match(/^(?:ssh:\/\/)?[\w.-]+@([\w.-]+)[:/]([\w.\-/]+?)(?:\.git)?$/);
70
+ if (sshMatch) return `${sshMatch[1]}/${sshMatch[2]}`;
71
+
72
+ return identity;
73
+ }
74
+
75
+ // ─── CRUD Operations ────────────────────────────
76
+
77
+ export function listMeshes(): LocalMeshEntry[] {
78
+ return loadMeshConfig().meshes;
79
+ }
80
+
81
+ export function getMesh(meshId: string): LocalMeshEntry | undefined {
82
+ return loadMeshConfig().meshes.find(m => m.id === meshId);
83
+ }
84
+
85
+ export function getMeshByRepo(repoIdentity: string): LocalMeshEntry | undefined {
86
+ return loadMeshConfig().meshes.find(m => m.repoIdentity === repoIdentity);
87
+ }
88
+
89
+ export interface CreateMeshOptions {
90
+ name: string;
91
+ repoRemoteUrl?: string;
92
+ repoIdentity?: string;
93
+ defaultBranch?: string;
94
+ policy?: Partial<RepoMeshPolicy>;
95
+ coordinator?: RepoMeshCoordinatorConfig;
96
+ }
97
+
98
+ export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
99
+ const config = loadMeshConfig();
100
+
101
+ if (config.meshes.length >= 20) {
102
+ throw new Error('Maximum 20 meshes allowed');
103
+ }
104
+
105
+ const repoIdentity = opts.repoIdentity || (opts.repoRemoteUrl ? normalizeRepoIdentity(opts.repoRemoteUrl) : '');
106
+ if (!repoIdentity) throw new Error('Either repoRemoteUrl or repoIdentity is required');
107
+
108
+ const now = new Date().toISOString();
109
+ const mesh: LocalMeshEntry = {
110
+ id: `mesh_${randomUUID().replace(/-/g, '')}`,
111
+ name: opts.name.trim().slice(0, 100),
112
+ repoIdentity,
113
+ repoRemoteUrl: opts.repoRemoteUrl,
114
+ defaultBranch: opts.defaultBranch,
115
+ policy: { ...DEFAULT_MESH_POLICY, ...opts.policy },
116
+ coordinator: opts.coordinator || {},
117
+ nodes: [],
118
+ createdAt: now,
119
+ updatedAt: now,
120
+ };
121
+
122
+ config.meshes.push(mesh);
123
+ saveMeshConfig(config);
124
+ return mesh;
125
+ }
126
+
127
+ export interface UpdateMeshOptions {
128
+ name?: string;
129
+ defaultBranch?: string;
130
+ policy?: Partial<RepoMeshPolicy>;
131
+ coordinator?: RepoMeshCoordinatorConfig;
132
+ }
133
+
134
+ export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEntry | undefined {
135
+ const config = loadMeshConfig();
136
+ const mesh = config.meshes.find(m => m.id === meshId);
137
+ if (!mesh) return undefined;
138
+
139
+ if (opts.name !== undefined) mesh.name = opts.name.trim().slice(0, 100);
140
+ if (opts.defaultBranch !== undefined) mesh.defaultBranch = opts.defaultBranch;
141
+ if (opts.policy) mesh.policy = { ...mesh.policy, ...opts.policy };
142
+ if (opts.coordinator) mesh.coordinator = opts.coordinator;
143
+ mesh.updatedAt = new Date().toISOString();
144
+
145
+ saveMeshConfig(config);
146
+ return mesh;
147
+ }
148
+
149
+ export function deleteMesh(meshId: string): boolean {
150
+ const config = loadMeshConfig();
151
+ const idx = config.meshes.findIndex(m => m.id === meshId);
152
+ if (idx === -1) return false;
153
+ config.meshes.splice(idx, 1);
154
+ saveMeshConfig(config);
155
+ return true;
156
+ }
157
+
158
+ // ─── Node Operations ────────────────────────────
159
+
160
+ export interface AddNodeOptions {
161
+ workspace: string;
162
+ repoRoot?: string;
163
+ userOverrides?: Partial<RepoMeshNodeCapabilities>;
164
+ policy?: RepoMeshNodePolicy;
165
+ isLocalWorktree?: boolean;
166
+ }
167
+
168
+ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntry | undefined {
169
+ const config = loadMeshConfig();
170
+ const mesh = config.meshes.find(m => m.id === meshId);
171
+ if (!mesh) return undefined;
172
+
173
+ if (mesh.nodes.length >= 10) {
174
+ throw new Error('Maximum 10 nodes per mesh');
175
+ }
176
+
177
+ // Check duplicate workspace
178
+ if (mesh.nodes.some(n => n.workspace === opts.workspace)) {
179
+ throw new Error('This workspace is already in the mesh');
180
+ }
181
+
182
+ const node: LocalMeshNodeEntry = {
183
+ id: `node_${randomUUID().replace(/-/g, '')}`,
184
+ workspace: opts.workspace.trim(),
185
+ repoRoot: opts.repoRoot,
186
+ userOverrides: opts.userOverrides || {},
187
+ policy: opts.policy || {},
188
+ isLocalWorktree: opts.isLocalWorktree,
189
+ };
190
+
191
+ mesh.nodes.push(node);
192
+ mesh.updatedAt = new Date().toISOString();
193
+ saveMeshConfig(config);
194
+ return node;
195
+ }
196
+
197
+ export function removeNode(meshId: string, nodeId: string): boolean {
198
+ const config = loadMeshConfig();
199
+ const mesh = config.meshes.find(m => m.id === meshId);
200
+ if (!mesh) return false;
201
+
202
+ const idx = mesh.nodes.findIndex(n => n.id === nodeId);
203
+ if (idx === -1) return false;
204
+
205
+ mesh.nodes.splice(idx, 1);
206
+ mesh.updatedAt = new Date().toISOString();
207
+ saveMeshConfig(config);
208
+ return true;
209
+ }
210
+
211
+ export function updateNode(
212
+ meshId: string,
213
+ nodeId: string,
214
+ opts: { userOverrides?: Partial<RepoMeshNodeCapabilities>; policy?: RepoMeshNodePolicy },
215
+ ): LocalMeshNodeEntry | undefined {
216
+ const config = loadMeshConfig();
217
+ const mesh = config.meshes.find(m => m.id === meshId);
218
+ if (!mesh) return undefined;
219
+
220
+ const node = mesh.nodes.find(n => n.id === nodeId);
221
+ if (!node) return undefined;
222
+
223
+ if (opts.userOverrides) node.userOverrides = { ...node.userOverrides, ...opts.userOverrides };
224
+ if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
225
+ mesh.updatedAt = new Date().toISOString();
226
+ saveMeshConfig(config);
227
+ return node;
228
+ }
package/src/index.ts CHANGED
@@ -81,6 +81,26 @@ export type {
81
81
  ExtensionProviderState,
82
82
  } from './shared-types.js';
83
83
 
84
+ // ── Repo Mesh Types (cross-package) ──
85
+ export type {
86
+ RepoMesh,
87
+ RepoMeshNode,
88
+ RepoMeshNodeHealth,
89
+ RepoMeshPolicy,
90
+ RepoMeshNodePolicy,
91
+ RepoMeshNodeCapabilities,
92
+ DetectedCommand,
93
+ ProjectContextSnapshot,
94
+ ProjectContextSource,
95
+ RepoMeshCoordinatorConfig,
96
+ LocalMeshConfig,
97
+ LocalMeshEntry,
98
+ LocalMeshNodeEntry,
99
+ RepoMeshStatus,
100
+ RepoMeshNodeStatus,
101
+ } from './repo-mesh-types.js';
102
+ export { DEFAULT_MESH_POLICY } from './repo-mesh-types.js';
103
+
84
104
  // ── Git Surface ──
85
105
  export * from './git/index.js';
86
106
 
@@ -111,6 +131,19 @@ export type { RecentActivityEntry } from './config/recent-activity.js';
111
131
  export { getSavedProviderSessions, upsertSavedProviderSession } from './config/saved-sessions.js';
112
132
  export type { SavedProviderSessionEntry } from './config/saved-sessions.js';
113
133
 
134
+ // ── Mesh Config ──
135
+ export {
136
+ listMeshes, getMesh, getMeshByRepo, createMesh, updateMesh, deleteMesh,
137
+ addNode, removeNode, updateNode, normalizeRepoIdentity,
138
+ } from './config/mesh-config.js';
139
+ export type { CreateMeshOptions, UpdateMeshOptions, AddNodeOptions } from './config/mesh-config.js';
140
+
141
+ // ── Mesh Coordinator ──
142
+ export { buildCoordinatorSystemPrompt } from './mesh/coordinator-prompt.js';
143
+ export type { CoordinatorPromptContext } from './mesh/coordinator-prompt.js';
144
+ export { syncMeshes } from './mesh/mesh-sync.js';
145
+ export type { MeshSyncTransport, MeshSyncResult, RemoteMeshRecord } from './mesh/mesh-sync.js';
146
+
114
147
  // ── State Store ──
115
148
  export { loadState, saveState, resetState } from './config/state-store.js';
116
149
  export type { DaemonState } from './config/state-store.js';
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Coordinator Prompt — System prompt template for mesh coordinator sessions
3
+ *
4
+ * When an MCP server starts in mesh mode, this prompt is injected into the
5
+ * coordinator agent's context so it understands:
6
+ * 1. What the mesh is (repo, nodes, policy)
7
+ * 2. What tools are available
8
+ * 3. How to orchestrate work across nodes
9
+ *
10
+ * The prompt is generated dynamically from the current mesh state.
11
+ */
12
+
13
+ import type {
14
+ LocalMeshEntry,
15
+ RepoMeshPolicy,
16
+ RepoMeshStatus,
17
+ RepoMeshNodeStatus,
18
+ } from '../repo-mesh-types.js';
19
+
20
+ // ─── Prompt Builder ─────────────────────────────
21
+
22
+ export interface CoordinatorPromptContext {
23
+ mesh: LocalMeshEntry;
24
+ status?: RepoMeshStatus;
25
+ userInstruction?: string;
26
+ }
27
+
28
+ export function buildCoordinatorSystemPrompt(ctx: CoordinatorPromptContext): string {
29
+ const { mesh, status, userInstruction } = ctx;
30
+ const sections: string[] = [];
31
+
32
+ // ── Identity ──
33
+ sections.push(`You are a **Repo Mesh Coordinator** — a technical team lead who orchestrates work across multiple agent sessions on a shared Git repository.
34
+
35
+ Your mesh: **${mesh.name}**
36
+ Repository: \`${mesh.repoIdentity}\`${mesh.defaultBranch ? `\nDefault branch: \`${mesh.defaultBranch}\`` : ''}`);
37
+
38
+ // ── Nodes ──
39
+ if (status?.nodes?.length) {
40
+ sections.push(buildNodeStatusSection(status.nodes));
41
+ } else if (mesh.nodes.length) {
42
+ sections.push(buildNodeConfigSection(mesh));
43
+ } else {
44
+ sections.push('## Nodes\nNo nodes configured yet. Ask the user to add nodes with `adhdev mesh add-node`.');
45
+ }
46
+
47
+ // ── Policy ──
48
+ sections.push(buildPolicySection(mesh.policy));
49
+
50
+ // ── Tools ──
51
+ sections.push(TOOLS_SECTION);
52
+
53
+ // ── Workflow ──
54
+ sections.push(WORKFLOW_SECTION);
55
+
56
+ // ── Rules ──
57
+ sections.push(RULES_SECTION);
58
+
59
+ // ── User instruction ──
60
+ if (userInstruction) {
61
+ sections.push(`## Additional Context\n${userInstruction}`);
62
+ }
63
+
64
+ if (mesh.coordinator.systemPromptSuffix) {
65
+ sections.push(mesh.coordinator.systemPromptSuffix);
66
+ }
67
+
68
+ return sections.join('\n\n');
69
+ }
70
+
71
+ // ─── Section Builders ───────────────────────────
72
+
73
+ function buildNodeStatusSection(nodes: RepoMeshNodeStatus[]): string {
74
+ const lines = ['## Current Node Status', ''];
75
+ for (const n of nodes) {
76
+ const healthIcon = n.health === 'online' ? '🟢' :
77
+ n.health === 'dirty' ? '🟡' :
78
+ n.health === 'offline' ? '⚫' : '🔴';
79
+ const sessions = n.activeSessions.length > 0
80
+ ? `sessions: ${n.activeSessions.join(', ')}`
81
+ : 'no active sessions';
82
+ const branch = n.git?.branch ? `branch: \`${n.git.branch}\`` : '';
83
+ lines.push(`- ${healthIcon} **${n.machineLabel}** (${n.nodeId})`);
84
+ lines.push(` workspace: \`${n.workspace}\` | ${branch} | ${sessions}`);
85
+ if (n.error) lines.push(` ⚠️ ${n.error}`);
86
+ }
87
+ return lines.join('\n');
88
+ }
89
+
90
+ function buildNodeConfigSection(mesh: LocalMeshEntry): string {
91
+ const lines = ['## Configured Nodes', ''];
92
+ for (const n of mesh.nodes) {
93
+ const labels: string[] = [];
94
+ if (n.isLocalWorktree) labels.push('worktree');
95
+ if (n.policy.readOnly) labels.push('read-only');
96
+ const suffix = labels.length ? ` [${labels.join(', ')}]` : '';
97
+ lines.push(`- **${n.workspace}** (${n.id})${suffix}`);
98
+ }
99
+ lines.push('', '_Use `mesh_status` to probe live health before delegating work._');
100
+ return lines.join('\n');
101
+ }
102
+
103
+ function buildPolicySection(policy: RepoMeshPolicy): string {
104
+ const rules: string[] = [];
105
+ if (policy.requirePreTaskCheckpoint) rules.push('- Create a git checkpoint **before** starting each task');
106
+ if (policy.requirePostTaskCheckpoint) rules.push('- Create a git checkpoint **after** each task completes');
107
+ if (policy.requireApprovalForPush) rules.push('- **Ask for user approval** before pushing to remote');
108
+ if (policy.requireApprovalForDestructiveGit) rules.push('- **Ask for user approval** before destructive git operations (force push, reset, etc.)');
109
+
110
+ const dirtyBehavior = {
111
+ block: '- **Do not** send tasks to nodes with dirty workspaces',
112
+ warn: '- Warn the user if a node has uncommitted changes before sending a task',
113
+ checkpoint_then_continue: '- Auto-checkpoint dirty nodes before sending tasks',
114
+ }[policy.dirtyWorkspaceBehavior] || '';
115
+ if (dirtyBehavior) rules.push(dirtyBehavior);
116
+
117
+ rules.push(`- Maximum **${policy.maxParallelTasks}** tasks running in parallel`);
118
+
119
+ return `## Policy\n${rules.join('\n')}`;
120
+ }
121
+
122
+ const TOOLS_SECTION = `## Available Tools
123
+
124
+ | Tool | Purpose |
125
+ |------|---------|
126
+ | \`mesh_status\` | Check all nodes' health, git state, and active sessions |
127
+ | \`mesh_list_nodes\` | List nodes with workspace paths |
128
+ | \`mesh_launch_session\` | Start a new agent session on a node |
129
+ | \`mesh_send_task\` | Send a task (natural language) to a running agent |
130
+ | \`mesh_read_chat\` | Read an agent's recent messages to check progress |
131
+ | \`mesh_git_status\` | Check git status on a specific node |
132
+ | \`mesh_checkpoint\` | Create a git checkpoint on a node |
133
+ | \`mesh_approve\` | Approve/reject a pending agent action |`;
134
+
135
+ const WORKFLOW_SECTION = `## Orchestration Workflow
136
+
137
+ 1. **Assess** — Call \`mesh_status\` to see which nodes are healthy and available.
138
+ 2. **Plan** — Decompose the user's request into independent tasks for parallel execution, or sequential tasks when dependencies exist.
139
+ 3. **Delegate** — For each task:
140
+ a. Pick the best node (consider: health, dirty state, current workload).
141
+ b. If no session exists, call \`mesh_launch_session\` to start one.
142
+ c. Call \`mesh_send_task\` with a clear, self-contained natural-language instruction.
143
+ 4. **Monitor** — Periodically call \`mesh_read_chat\` to check progress. Handle approvals via \`mesh_approve\`.
144
+ 5. **Verify** — When a task reports completion, call \`mesh_git_status\` to verify changes were made.
145
+ 6. **Checkpoint** — Call \`mesh_checkpoint\` to save the work.
146
+ 7. **Report** — Summarize what was done, what changed, and any issues.`;
147
+
148
+ const RULES_SECTION = `## Rules
149
+
150
+ - **Be conversational.** Delegate work the way a tech lead would — clear, specific instructions in natural language.
151
+ - **Don't inspect code.** Trust the agent's output. Verify via git diff/status, not by reading source files.
152
+ - **Don't over-parallelize.** Start with 1-2 concurrent tasks. Scale up if they succeed.
153
+ - **Handle failures gracefully.** If a task fails, read the chat to understand why, then retry or reassign.
154
+ - **Keep the user informed.** Report progress after each delegation round.
155
+ - **Respect node capabilities.** Don't send build tasks to read-only nodes. Don't push from nodes that aren't allowed to.
156
+ - **Never fabricate tool results.** Always call the actual tool; never pretend you did.`;