@adhdev/daemon-core 0.9.77-rc.9 → 0.9.78
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/boot/daemon-lifecycle.d.ts +3 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
- package/dist/commands/mesh-coordinator.d.ts +10 -0
- package/dist/commands/router.d.ts +4 -1
- package/dist/config/mesh-config.d.ts +1 -0
- package/dist/git/git-worktree.d.ts +15 -2
- package/dist/index.d.ts +11 -6
- package/dist/index.js +2117 -300
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2102 -300
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +14 -7
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +55 -0
- package/dist/mesh/mesh-ledger.d.ts +84 -4
- package/dist/mesh/mesh-sync.d.ts +4 -12
- package/dist/mesh/mesh-visualization.d.ts +70 -0
- package/dist/mesh/mesh-work-queue.d.ts +58 -1
- package/dist/mesh/p2p-relay-failure.d.ts +35 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +6 -0
- package/dist/repo-mesh-types.d.ts +2 -0
- package/dist/shared-types.d.ts +38 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +5 -0
- package/src/cli-adapters/provider-cli-adapter.ts +30 -5
- package/src/commands/cli-manager.ts +0 -4
- package/src/commands/mesh-coordinator.ts +55 -7
- package/src/commands/router.ts +964 -26
- package/src/commands/stream-commands.ts +8 -1
- package/src/config/config.ts +2 -1
- package/src/config/mesh-config.ts +2 -0
- package/src/config/workspaces.ts +1 -1
- package/src/git/git-worktree.ts +56 -4
- package/src/index.d.ts +3 -0
- package/src/index.ts +30 -6
- package/src/mesh/coordinator-prompt.ts +21 -10
- package/src/mesh/mesh-events.ts +532 -22
- package/src/mesh/mesh-ledger-reconciliation.ts +115 -0
- package/src/mesh/mesh-ledger.ts +209 -8
- package/src/mesh/mesh-sync.ts +4 -34
- package/src/mesh/mesh-visualization.ts +341 -0
- package/src/mesh/mesh-work-queue.ts +183 -17
- package/src/mesh/p2p-relay-failure.ts +152 -0
- package/src/providers/acp-provider-instance.ts +2 -1
- package/src/providers/chat-message-normalization.ts +33 -1
- package/src/providers/cli-provider-instance.ts +155 -31
- package/src/providers/extension-provider-instance.ts +2 -1
- package/src/providers/ide-provider-instance.ts +2 -2
- package/src/repo-mesh-types.ts +2 -0
- package/src/shared-types.ts +38 -0
package/src/commands/router.ts
CHANGED
|
@@ -43,6 +43,7 @@ import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDa
|
|
|
43
43
|
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
44
44
|
import { homedir } from 'os';
|
|
45
45
|
import { join as pathJoin, resolve as pathResolve } from 'path';
|
|
46
|
+
import * as fs from 'fs';
|
|
46
47
|
|
|
47
48
|
type ReleaseChannel = 'stable' | 'preview';
|
|
48
49
|
const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
|
|
@@ -114,9 +115,271 @@ async function resolveProviderTypeFromPriority(args: {
|
|
|
114
115
|
|
|
115
116
|
return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
|
|
116
117
|
}
|
|
117
|
-
import * as fs from 'fs';
|
|
118
|
-
|
|
119
118
|
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
119
|
+
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
120
|
+
type MeshRefineValidationCommand = {
|
|
121
|
+
command: string;
|
|
122
|
+
args: string[];
|
|
123
|
+
displayCommand: string;
|
|
124
|
+
category: string;
|
|
125
|
+
source: string;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
type MeshRefineValidationSummary = {
|
|
129
|
+
status: MeshRefineValidationStatus;
|
|
130
|
+
required: true;
|
|
131
|
+
commandsRun: Array<Record<string, unknown>>;
|
|
132
|
+
rejectedCommands: Array<Record<string, unknown>>;
|
|
133
|
+
skippedReason?: string;
|
|
134
|
+
timeoutMs: number;
|
|
135
|
+
outputLimitBytes: number;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
139
|
+
const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
140
|
+
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
141
|
+
const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
|
|
142
|
+
const REFINE_VALIDATION_MAX_COMMANDS = 4;
|
|
143
|
+
|
|
144
|
+
function truncateValidationOutput(value: unknown): string {
|
|
145
|
+
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
146
|
+
if (text.length <= REFINE_VALIDATION_SUMMARY_CHARS) return text;
|
|
147
|
+
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readPackageScripts(workspace: string): Record<string, string> {
|
|
151
|
+
try {
|
|
152
|
+
const packageJsonPath = pathJoin(workspace, 'package.json');
|
|
153
|
+
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
154
|
+
return parsed?.scripts && typeof parsed.scripts === 'object' && !Array.isArray(parsed.scripts)
|
|
155
|
+
? parsed.scripts as Record<string, string>
|
|
156
|
+
: {};
|
|
157
|
+
} catch {
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function tokenizeValidationCommand(command: string): string[] | null {
|
|
163
|
+
const trimmed = command.trim();
|
|
164
|
+
if (!trimmed) return null;
|
|
165
|
+
// Fail closed: the gate never hands shell syntax to a shell. Package-manager
|
|
166
|
+
// scripts are invoked via execFile(binary, args), and metacharacters/quotes are
|
|
167
|
+
// rejected before tokenization so `npm run test && rm -rf` cannot be smuggled in.
|
|
168
|
+
if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
|
|
169
|
+
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
170
|
+
if (!tokens.length) return null;
|
|
171
|
+
if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
|
|
172
|
+
return tokens;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function scriptMatchesValidationCategory(scriptName: string, category: string): boolean {
|
|
176
|
+
return scriptName === category || scriptName.startsWith(`${category}:`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parsePackageManagerValidationCommand(
|
|
180
|
+
rawCommand: string,
|
|
181
|
+
category: string,
|
|
182
|
+
scripts: Record<string, string>,
|
|
183
|
+
source: string,
|
|
184
|
+
): { command?: MeshRefineValidationCommand; rejected?: Record<string, unknown> } {
|
|
185
|
+
const tokens = tokenizeValidationCommand(rawCommand);
|
|
186
|
+
if (!tokens) {
|
|
187
|
+
return { rejected: { command: rawCommand, category, source, reason: 'unsafe command string is not allowlisted' } };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const [binary, second, third, ...rest] = tokens;
|
|
191
|
+
let scriptName = '';
|
|
192
|
+
let command = binary;
|
|
193
|
+
let args: string[] = [];
|
|
194
|
+
|
|
195
|
+
if ((binary === 'npm' || binary === 'pnpm' || binary === 'bun') && second === 'run' && third) {
|
|
196
|
+
scriptName = third;
|
|
197
|
+
args = ['run', scriptName, ...rest];
|
|
198
|
+
} else if (binary === 'npm' && second === 'test' && !third) {
|
|
199
|
+
scriptName = 'test';
|
|
200
|
+
args = ['test'];
|
|
201
|
+
} else if (binary === 'yarn' && second === 'run' && third) {
|
|
202
|
+
scriptName = third;
|
|
203
|
+
args = ['run', scriptName, ...rest];
|
|
204
|
+
} else if (binary === 'yarn' && second && !third) {
|
|
205
|
+
scriptName = second;
|
|
206
|
+
args = [scriptName];
|
|
207
|
+
} else {
|
|
208
|
+
return { rejected: { command: rawCommand, category, source, reason: 'command is not a supported package-manager script invocation' } };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!scriptName || !Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
212
|
+
return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script is not declared in package.json' } };
|
|
213
|
+
}
|
|
214
|
+
if (!scriptMatchesValidationCategory(scriptName, category)) {
|
|
215
|
+
return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script name is outside the validation category allowlist' } };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
command: {
|
|
220
|
+
command,
|
|
221
|
+
args,
|
|
222
|
+
displayCommand: [command, ...args].join(' '),
|
|
223
|
+
category,
|
|
224
|
+
source,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function collectProjectContextValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string; confidence?: string }> {
|
|
230
|
+
const commands = mesh?.projectContext?.commands;
|
|
231
|
+
if (!commands || typeof commands !== 'object' || Array.isArray(commands)) return [];
|
|
232
|
+
const candidates: Array<{ command: string; category: string; source: string; confidence?: string }> = [];
|
|
233
|
+
for (const category of REFINE_VALIDATION_CATEGORIES) {
|
|
234
|
+
const entries = Array.isArray(commands[category]) ? commands[category] : [];
|
|
235
|
+
for (const entry of entries) {
|
|
236
|
+
if (typeof entry?.command !== 'string') continue;
|
|
237
|
+
candidates.push({
|
|
238
|
+
command: entry.command,
|
|
239
|
+
category,
|
|
240
|
+
source: typeof entry.sourcePath === 'string' ? entry.sourcePath : 'projectContext.commands',
|
|
241
|
+
confidence: typeof entry.confidence === 'string' ? entry.confidence : undefined,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return candidates.sort((a, b) => {
|
|
246
|
+
const rank = (value?: string) => value === 'high' ? 0 : value === 'medium' ? 1 : 2;
|
|
247
|
+
return rank(a.confidence) - rank(b.confidence);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function collectPolicyValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string }> {
|
|
252
|
+
const policy = mesh?.policy && typeof mesh.policy === 'object' && !Array.isArray(mesh.policy) ? mesh.policy : {};
|
|
253
|
+
const configured = Array.isArray(policy.validationCommands)
|
|
254
|
+
? policy.validationCommands
|
|
255
|
+
: Array.isArray(policy.validationGate?.commands)
|
|
256
|
+
? policy.validationGate.commands
|
|
257
|
+
: [];
|
|
258
|
+
return configured
|
|
259
|
+
.map((entry: any) => typeof entry === 'string' ? { command: entry, category: '', source: 'mesh.policy.validationCommands' } : entry)
|
|
260
|
+
.filter((entry: any) => entry && typeof entry.command === 'string')
|
|
261
|
+
.map((entry: any) => {
|
|
262
|
+
const commandText = entry.command.trim();
|
|
263
|
+
const category = REFINE_VALIDATION_CATEGORIES.find(cat => commandText.includes(` ${cat}`)) ?? '';
|
|
264
|
+
return { command: commandText, category, source: 'mesh.policy.validationCommands' };
|
|
265
|
+
})
|
|
266
|
+
.filter((entry: any) => !!entry.category);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function selectMeshRefineValidationCommands(mesh: any, workspace: string): { commands: MeshRefineValidationCommand[]; rejectedCommands: Array<Record<string, unknown>>; source: string } {
|
|
270
|
+
const scripts = readPackageScripts(workspace);
|
|
271
|
+
const rejectedCommands: Array<Record<string, unknown>> = [];
|
|
272
|
+
const selected: MeshRefineValidationCommand[] = [];
|
|
273
|
+
const seen = new Set<string>();
|
|
274
|
+
const candidates = [
|
|
275
|
+
...collectPolicyValidationCandidates(mesh),
|
|
276
|
+
...collectProjectContextValidationCandidates(mesh),
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
for (const candidate of candidates) {
|
|
280
|
+
const parsed = parsePackageManagerValidationCommand(candidate.command, candidate.category, scripts, candidate.source);
|
|
281
|
+
if (parsed.rejected) {
|
|
282
|
+
rejectedCommands.push(parsed.rejected);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (!parsed.command || seen.has(parsed.command.displayCommand)) continue;
|
|
286
|
+
selected.push(parsed.command);
|
|
287
|
+
seen.add(parsed.command.displayCommand);
|
|
288
|
+
if (selected.length >= REFINE_VALIDATION_MAX_COMMANDS) break;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!selected.length && candidates.length === 0) {
|
|
292
|
+
for (const category of REFINE_VALIDATION_CATEGORIES) {
|
|
293
|
+
if (!Object.prototype.hasOwnProperty.call(scripts, category)) continue;
|
|
294
|
+
const fallback = parsePackageManagerValidationCommand(`npm run ${category}`, category, scripts, 'package.json:scripts');
|
|
295
|
+
if (fallback.command && !seen.has(fallback.command.displayCommand)) {
|
|
296
|
+
selected.push(fallback.command);
|
|
297
|
+
seen.add(fallback.command.displayCommand);
|
|
298
|
+
} else if (fallback.rejected) {
|
|
299
|
+
rejectedCommands.push(fallback.rejected);
|
|
300
|
+
}
|
|
301
|
+
if (selected.length >= 2) break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
commands: selected,
|
|
307
|
+
rejectedCommands,
|
|
308
|
+
source: selected.some(command => command.source === 'mesh.policy.validationCommands')
|
|
309
|
+
? 'mesh_policy'
|
|
310
|
+
: selected.some(command => command.source !== 'package.json:scripts')
|
|
311
|
+
? 'project_context'
|
|
312
|
+
: selected.length
|
|
313
|
+
? 'package_json_scripts'
|
|
314
|
+
: 'unavailable',
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function runMeshRefineValidationGate(mesh: any, workspace: string): Promise<MeshRefineValidationSummary> {
|
|
319
|
+
const { execFile } = await import('node:child_process');
|
|
320
|
+
const { promisify } = await import('node:util');
|
|
321
|
+
const execFileAsync = promisify(execFile);
|
|
322
|
+
const selection = selectMeshRefineValidationCommands(mesh, workspace);
|
|
323
|
+
const summary: MeshRefineValidationSummary = {
|
|
324
|
+
status: 'skipped',
|
|
325
|
+
required: true,
|
|
326
|
+
commandsRun: [],
|
|
327
|
+
rejectedCommands: selection.rejectedCommands,
|
|
328
|
+
skippedReason: undefined,
|
|
329
|
+
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
330
|
+
outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
if (!selection.commands.length) {
|
|
334
|
+
summary.skippedReason = 'validation_unavailable: no allowlisted projectContext, mesh policy, or package.json build/test/typecheck/lint command was available';
|
|
335
|
+
return summary;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const candidate of selection.commands) {
|
|
339
|
+
const startedAt = Date.now();
|
|
340
|
+
try {
|
|
341
|
+
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
342
|
+
cwd: workspace,
|
|
343
|
+
encoding: 'utf8',
|
|
344
|
+
timeout: REFINE_VALIDATION_TIMEOUT_MS,
|
|
345
|
+
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
346
|
+
env: { ...process.env, CI: process.env.CI || '1' },
|
|
347
|
+
});
|
|
348
|
+
summary.commandsRun.push({
|
|
349
|
+
command: candidate.command,
|
|
350
|
+
args: candidate.args,
|
|
351
|
+
displayCommand: candidate.displayCommand,
|
|
352
|
+
category: candidate.category,
|
|
353
|
+
source: candidate.source,
|
|
354
|
+
passed: true,
|
|
355
|
+
exitCode: 0,
|
|
356
|
+
durationMs: Date.now() - startedAt,
|
|
357
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
358
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
359
|
+
});
|
|
360
|
+
} catch (error: any) {
|
|
361
|
+
summary.commandsRun.push({
|
|
362
|
+
command: candidate.command,
|
|
363
|
+
args: candidate.args,
|
|
364
|
+
displayCommand: candidate.displayCommand,
|
|
365
|
+
category: candidate.category,
|
|
366
|
+
source: candidate.source,
|
|
367
|
+
passed: false,
|
|
368
|
+
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
369
|
+
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
370
|
+
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
371
|
+
durationMs: Date.now() - startedAt,
|
|
372
|
+
stdout: truncateValidationOutput(error?.stdout),
|
|
373
|
+
stderr: truncateValidationOutput(error?.stderr || error?.message),
|
|
374
|
+
});
|
|
375
|
+
summary.status = 'failed';
|
|
376
|
+
return summary;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
summary.status = 'passed';
|
|
381
|
+
return summary;
|
|
382
|
+
}
|
|
120
383
|
|
|
121
384
|
function loadYamlModule(): { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string } {
|
|
122
385
|
return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
|
|
@@ -154,6 +417,35 @@ function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Re
|
|
|
154
417
|
return { config: baseConfig, sourceHome, sourceConfigPath };
|
|
155
418
|
}
|
|
156
419
|
|
|
420
|
+
function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
|
|
421
|
+
const {
|
|
422
|
+
model: _model,
|
|
423
|
+
provider: _provider,
|
|
424
|
+
default_model: _defaultModel,
|
|
425
|
+
defaultProvider: _defaultProvider,
|
|
426
|
+
default_provider: _defaultProviderSnake,
|
|
427
|
+
modelProvider: _modelProvider,
|
|
428
|
+
model_provider: _modelProviderSnake,
|
|
429
|
+
...sanitized
|
|
430
|
+
} = config;
|
|
431
|
+
const delegation = sanitized.delegation;
|
|
432
|
+
if (delegation && typeof delegation === 'object' && !Array.isArray(delegation)) {
|
|
433
|
+
const {
|
|
434
|
+
model: _delegationModel,
|
|
435
|
+
provider: _delegationProvider,
|
|
436
|
+
modelProvider: _delegationModelProvider,
|
|
437
|
+
model_provider: _delegationModelProviderSnake,
|
|
438
|
+
...delegationRest
|
|
439
|
+
} = delegation;
|
|
440
|
+
if (Object.keys(delegationRest).length > 0) {
|
|
441
|
+
sanitized.delegation = delegationRest;
|
|
442
|
+
} else {
|
|
443
|
+
delete sanitized.delegation;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return sanitized;
|
|
447
|
+
}
|
|
448
|
+
|
|
157
449
|
function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
|
|
158
450
|
if (pathResolve(sourceHome) === pathResolve(targetHome)) return;
|
|
159
451
|
for (const fileName of ['.env', 'auth.json']) {
|
|
@@ -332,7 +624,7 @@ export class DaemonCommandRouter {
|
|
|
332
624
|
this.deps = deps;
|
|
333
625
|
}
|
|
334
626
|
|
|
335
|
-
|
|
627
|
+
public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
336
628
|
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
337
629
|
this.inlineMeshCache.set(meshId, inlineMesh as any);
|
|
338
630
|
return inlineMesh as any;
|
|
@@ -388,10 +680,238 @@ export class DaemonCommandRouter {
|
|
|
388
680
|
return false;
|
|
389
681
|
}
|
|
390
682
|
|
|
683
|
+
private async cleanupLocalWorktreeNode(args: {
|
|
684
|
+
mesh: any;
|
|
685
|
+
node: any;
|
|
686
|
+
nodeId: string;
|
|
687
|
+
}): Promise<{ success: true; skipped?: boolean; removedPath?: string; repoRoot?: string; reason?: string; fallback?: string; forced?: boolean; convergence?: Record<string, unknown> } | { success: false; code: string; error: string; recoveryHint: string; convergence?: Record<string, unknown> }> {
|
|
688
|
+
const workspace = typeof args.node?.workspace === 'string' ? args.node.workspace.trim() : '';
|
|
689
|
+
if (!workspace) {
|
|
690
|
+
return {
|
|
691
|
+
success: false,
|
|
692
|
+
code: 'mesh_worktree_cleanup_missing_workspace',
|
|
693
|
+
error: `Worktree node '${args.nodeId}' is missing workspace metadata`,
|
|
694
|
+
recoveryHint: 'Inspect the mesh node record before removing it, or remove stale metadata manually only after confirming no managed worktree remains.',
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const worktreeExists = fs.existsSync(workspace);
|
|
699
|
+
const sourceNode = args.node?.clonedFromNodeId
|
|
700
|
+
? args.mesh?.nodes?.find((n: any) => n.id === args.node.clonedFromNodeId || n.nodeId === args.node.clonedFromNodeId)
|
|
701
|
+
: args.mesh?.nodes?.find((n: any) => !n.isLocalWorktree);
|
|
702
|
+
const repoRoot = typeof sourceNode?.repoRoot === 'string' && sourceNode.repoRoot.trim()
|
|
703
|
+
? sourceNode.repoRoot.trim()
|
|
704
|
+
: typeof sourceNode?.workspace === 'string' && sourceNode.workspace.trim()
|
|
705
|
+
? sourceNode.workspace.trim()
|
|
706
|
+
: '';
|
|
707
|
+
|
|
708
|
+
if (!worktreeExists) {
|
|
709
|
+
return { success: true, skipped: true, removedPath: workspace, repoRoot: repoRoot || undefined, reason: 'worktree_path_missing' };
|
|
710
|
+
}
|
|
711
|
+
if (!repoRoot || !fs.existsSync(repoRoot)) {
|
|
712
|
+
return {
|
|
713
|
+
success: false,
|
|
714
|
+
code: 'mesh_worktree_cleanup_missing_source_repo',
|
|
715
|
+
error: `Refusing to remove worktree '${workspace}' because the source repo root is unavailable`,
|
|
716
|
+
recoveryHint: 'Run mesh_remove_node from the machine that owns the source repo, or verify the source node metadata before retrying.',
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
if (typeof args.node?.worktreeBranch !== 'string' || !args.node.worktreeBranch.trim()) {
|
|
720
|
+
return {
|
|
721
|
+
success: false,
|
|
722
|
+
code: 'mesh_worktree_cleanup_missing_branch',
|
|
723
|
+
error: `Refusing to remove worktree '${workspace}' because worktreeBranch metadata is missing`,
|
|
724
|
+
recoveryHint: 'Confirm this is an ADHDev-managed worktree before removing it manually; managed worktree nodes include worktreeBranch metadata.',
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const { resolveWorktreePath, listWorktrees, removeWorktree } = await import('../git/git-worktree.js');
|
|
729
|
+
const normalizePath = (value: string) => {
|
|
730
|
+
const resolved = pathResolve(value);
|
|
731
|
+
try { return fs.realpathSync(resolved); } catch { return resolved; }
|
|
732
|
+
};
|
|
733
|
+
const expectedPath = normalizePath(resolveWorktreePath(repoRoot, String(args.mesh?.name || args.mesh?.id || 'mesh'), args.node.worktreeBranch));
|
|
734
|
+
const actualPath = normalizePath(workspace);
|
|
735
|
+
if (actualPath !== expectedPath) {
|
|
736
|
+
return {
|
|
737
|
+
success: false,
|
|
738
|
+
code: 'mesh_worktree_cleanup_unexpected_path',
|
|
739
|
+
error: `Refusing to remove worktree '${workspace}' because it is not at the expected managed path '${expectedPath}'`,
|
|
740
|
+
recoveryHint: 'Use git worktree list/status to inspect the path. Retry only after confirming the mesh node metadata points to an ADHDev-managed worktree.',
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const entries = await listWorktrees(repoRoot);
|
|
745
|
+
const managedEntry = entries.find(entry => normalizePath(entry.path) === actualPath);
|
|
746
|
+
if (!managedEntry) {
|
|
747
|
+
return {
|
|
748
|
+
success: false,
|
|
749
|
+
code: 'mesh_worktree_cleanup_not_registered',
|
|
750
|
+
error: `Refusing to remove '${workspace}' because it is not registered in git worktree list for '${repoRoot}'`,
|
|
751
|
+
recoveryHint: 'Inspect git worktree list --porcelain from the source repo. If the path was already removed, prune git worktrees before retrying.',
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
if (managedEntry.branch && managedEntry.branch !== args.node.worktreeBranch) {
|
|
755
|
+
return {
|
|
756
|
+
success: false,
|
|
757
|
+
code: 'mesh_worktree_cleanup_branch_mismatch',
|
|
758
|
+
error: `Refusing to remove '${workspace}' because git reports branch '${managedEntry.branch}', expected '${args.node.worktreeBranch}'`,
|
|
759
|
+
recoveryHint: 'Inspect the worktree branch and mesh metadata before retrying cleanup.',
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const forceFallbackConvergence = await this.getWorktreeForceCleanupConvergence({
|
|
764
|
+
repoRoot,
|
|
765
|
+
workspace,
|
|
766
|
+
node: args.node,
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
try {
|
|
770
|
+
const result = await removeWorktree(repoRoot, workspace, {
|
|
771
|
+
requireClean: true,
|
|
772
|
+
allowSubmoduleForceFallback: forceFallbackConvergence.allow,
|
|
773
|
+
});
|
|
774
|
+
return {
|
|
775
|
+
success: true,
|
|
776
|
+
removedPath: result.removedPath,
|
|
777
|
+
repoRoot,
|
|
778
|
+
...(result.fallback ? {
|
|
779
|
+
fallback: result.fallback,
|
|
780
|
+
forced: result.forced,
|
|
781
|
+
reason: result.reason,
|
|
782
|
+
convergence: forceFallbackConvergence,
|
|
783
|
+
} : {}),
|
|
784
|
+
};
|
|
785
|
+
} catch (e: any) {
|
|
786
|
+
const message = String(e?.message || e || 'worktree cleanup failed');
|
|
787
|
+
const dirty = message.includes('dirty worktree') || message.includes('local changes');
|
|
788
|
+
const submoduleForceBlocked = /working trees containing submodules cannot be moved or removed/i.test(message) && !forceFallbackConvergence.allow;
|
|
789
|
+
return {
|
|
790
|
+
success: false,
|
|
791
|
+
code: dirty
|
|
792
|
+
? 'mesh_worktree_cleanup_dirty'
|
|
793
|
+
: submoduleForceBlocked
|
|
794
|
+
? 'mesh_worktree_cleanup_force_fallback_blocked'
|
|
795
|
+
: 'mesh_worktree_cleanup_failed',
|
|
796
|
+
error: submoduleForceBlocked
|
|
797
|
+
? `${message}; refusing --force fallback because convergence could not be verified: ${forceFallbackConvergence.error || 'unknown convergence state'}`
|
|
798
|
+
: message,
|
|
799
|
+
recoveryHint: dirty
|
|
800
|
+
? 'Commit, stash, or intentionally discard the worktree changes before retrying mesh_remove_node. The mesh registry entry is preserved until cleanup is safe.'
|
|
801
|
+
: submoduleForceBlocked
|
|
802
|
+
? 'Verify the worktree branch is merged/contained in the source default branch (for example origin/main) or mark the node with a safe branchConvergence final state before retrying. The mesh registry entry is preserved.'
|
|
803
|
+
: 'Inspect git worktree status/list from the source repo and retry after resolving the reported cleanup failure.',
|
|
804
|
+
...(submoduleForceBlocked ? { convergence: forceFallbackConvergence } : {}),
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
private async getWorktreeForceCleanupConvergence(args: {
|
|
810
|
+
repoRoot: string;
|
|
811
|
+
workspace: string;
|
|
812
|
+
node: any;
|
|
813
|
+
}): Promise<{ allow: boolean; status?: string; source?: string; ref?: string; error?: string }> {
|
|
814
|
+
const metadataStatus = typeof args.node?.branchConvergence?.status === 'string'
|
|
815
|
+
? args.node.branchConvergence.status
|
|
816
|
+
: '';
|
|
817
|
+
if (metadataStatus === 'merged_to_main' || metadataStatus === 'cleanup_candidate') {
|
|
818
|
+
return { allow: true, status: metadataStatus, source: 'node_branch_convergence' };
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const { execFile } = await import('node:child_process');
|
|
822
|
+
const { promisify } = await import('node:util');
|
|
823
|
+
const execFileAsync = promisify(execFile);
|
|
824
|
+
const runGit = async (gitArgs: string[], cwd: string): Promise<string> => {
|
|
825
|
+
const { stdout } = await execFileAsync('git', gitArgs, {
|
|
826
|
+
cwd,
|
|
827
|
+
encoding: 'utf8',
|
|
828
|
+
timeout: 30_000,
|
|
829
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
830
|
+
windowsHide: true,
|
|
831
|
+
});
|
|
832
|
+
return String(stdout || '').trim();
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
let head = '';
|
|
836
|
+
try {
|
|
837
|
+
head = await runGit(['rev-parse', 'HEAD'], args.workspace);
|
|
838
|
+
} catch (e: any) {
|
|
839
|
+
return { allow: false, error: `could not resolve worktree HEAD: ${e?.message || e}` };
|
|
840
|
+
}
|
|
841
|
+
if (!head) return { allow: false, error: 'worktree HEAD is empty' };
|
|
842
|
+
|
|
843
|
+
const candidateRefs: string[] = [];
|
|
844
|
+
try {
|
|
845
|
+
const defaultBranch = await runGit(['branch', '--show-current'], args.repoRoot);
|
|
846
|
+
if (defaultBranch) {
|
|
847
|
+
candidateRefs.push(defaultBranch, `origin/${defaultBranch}`);
|
|
848
|
+
}
|
|
849
|
+
} catch { /* fall through to common refs */ }
|
|
850
|
+
candidateRefs.push('origin/main', 'origin/master', 'main', 'master');
|
|
851
|
+
|
|
852
|
+
const seen = new Set<string>();
|
|
853
|
+
const checkedRefs: string[] = [];
|
|
854
|
+
for (const ref of candidateRefs) {
|
|
855
|
+
if (!ref || seen.has(ref)) continue;
|
|
856
|
+
seen.add(ref);
|
|
857
|
+
let commit = '';
|
|
858
|
+
try {
|
|
859
|
+
commit = await runGit(['rev-parse', '--verify', `${ref}^{commit}`], args.repoRoot);
|
|
860
|
+
} catch {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
checkedRefs.push(ref);
|
|
864
|
+
try {
|
|
865
|
+
await runGit(['merge-base', '--is-ancestor', head, commit], args.repoRoot);
|
|
866
|
+
return { allow: true, status: 'merged_to_default_ref', source: 'git_merge_base', ref };
|
|
867
|
+
} catch {
|
|
868
|
+
// Not contained in this candidate ref; keep checking other safe refs.
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return {
|
|
873
|
+
allow: false,
|
|
874
|
+
status: metadataStatus || undefined,
|
|
875
|
+
error: checkedRefs.length
|
|
876
|
+
? `worktree HEAD is not contained in checked refs: ${checkedRefs.join(', ')}`
|
|
877
|
+
: 'no default/main refs were available for convergence verification',
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
391
881
|
private isCompletedHostedSession(record: any): boolean {
|
|
392
882
|
return record?.lifecycle === 'stopped' || record?.lifecycle === 'failed' || record?.lifecycle === 'interrupted';
|
|
393
883
|
}
|
|
394
884
|
|
|
885
|
+
private async recordIntentionalMeshSessionStop(args: {
|
|
886
|
+
meshId: string;
|
|
887
|
+
nodeId: string;
|
|
888
|
+
node: any;
|
|
889
|
+
sessionId: string;
|
|
890
|
+
mode: RepoMeshSessionCleanupMode;
|
|
891
|
+
source: 'mesh_cleanup_sessions' | 'mesh_remove_node';
|
|
892
|
+
action: 'stop_session' | 'delete_session_force';
|
|
893
|
+
}): Promise<void> {
|
|
894
|
+
try {
|
|
895
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
896
|
+
appendLedgerEntry(args.meshId, {
|
|
897
|
+
kind: 'session_stopped',
|
|
898
|
+
nodeId: args.nodeId,
|
|
899
|
+
sessionId: args.sessionId,
|
|
900
|
+
payload: {
|
|
901
|
+
intentional: true,
|
|
902
|
+
reason: 'operator_cleanup',
|
|
903
|
+
intentionalStopReason: 'operator_cleanup',
|
|
904
|
+
source: args.source,
|
|
905
|
+
cleanupMode: args.mode,
|
|
906
|
+
action: args.action,
|
|
907
|
+
workspace: typeof args.node?.workspace === 'string' ? args.node.workspace : undefined,
|
|
908
|
+
},
|
|
909
|
+
});
|
|
910
|
+
} catch (e: any) {
|
|
911
|
+
LOG.warn('MeshCleanup', `Failed to record intentional cleanup stop for ${args.sessionId}: ${e?.message || e}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
395
915
|
private async cleanupMeshSessions(args: {
|
|
396
916
|
meshId: string;
|
|
397
917
|
nodeId: string;
|
|
@@ -399,6 +919,7 @@ export class DaemonCommandRouter {
|
|
|
399
919
|
mode: RepoMeshSessionCleanupMode;
|
|
400
920
|
sessionIds?: string[];
|
|
401
921
|
dryRun?: boolean;
|
|
922
|
+
source?: 'mesh_cleanup_sessions' | 'mesh_remove_node';
|
|
402
923
|
}): Promise<{ success: boolean; [key: string]: unknown }> {
|
|
403
924
|
if (args.mode === 'preserve') {
|
|
404
925
|
return { success: true, mode: 'preserve', matchedCount: 0, stoppedSessionIds: [], deletedSessionIds: [], skippedSessionIds: [] };
|
|
@@ -418,6 +939,21 @@ export class DaemonCommandRouter {
|
|
|
418
939
|
const deleteUnsupportedSessionIds: string[] = [];
|
|
419
940
|
const recordsRemainSessionIds: string[] = [];
|
|
420
941
|
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
942
|
+
const cleanupSource = args.source || 'mesh_cleanup_sessions';
|
|
943
|
+
const markedIntentionalStopSessionIds = new Set<string>();
|
|
944
|
+
const markIntentionalStop = async (sessionId: string, action: 'stop_session' | 'delete_session_force') => {
|
|
945
|
+
if (args.dryRun || markedIntentionalStopSessionIds.has(sessionId)) return;
|
|
946
|
+
markedIntentionalStopSessionIds.add(sessionId);
|
|
947
|
+
await this.recordIntentionalMeshSessionStop({
|
|
948
|
+
meshId: args.meshId,
|
|
949
|
+
nodeId: args.nodeId,
|
|
950
|
+
node: args.node,
|
|
951
|
+
sessionId,
|
|
952
|
+
mode: args.mode,
|
|
953
|
+
source: cleanupSource,
|
|
954
|
+
action,
|
|
955
|
+
});
|
|
956
|
+
};
|
|
421
957
|
const matchedBySurfaceKind = {
|
|
422
958
|
live_runtime: 0,
|
|
423
959
|
recovery_snapshot: 0,
|
|
@@ -442,7 +978,10 @@ export class DaemonCommandRouter {
|
|
|
442
978
|
try {
|
|
443
979
|
if (args.mode === 'stop') {
|
|
444
980
|
if (!completed) {
|
|
445
|
-
if (!args.dryRun)
|
|
981
|
+
if (!args.dryRun) {
|
|
982
|
+
await markIntentionalStop(sessionId, 'stop_session');
|
|
983
|
+
await this.deps.sessionHostControl.stopSession(sessionId);
|
|
984
|
+
}
|
|
446
985
|
stoppedSessionIds.push(sessionId);
|
|
447
986
|
} else {
|
|
448
987
|
skippedSessionIds.push(sessionId);
|
|
@@ -461,6 +1000,7 @@ export class DaemonCommandRouter {
|
|
|
461
1000
|
}
|
|
462
1001
|
|
|
463
1002
|
if (args.mode === 'stop_and_delete') {
|
|
1003
|
+
if (!completed) await markIntentionalStop(sessionId, 'delete_session_force');
|
|
464
1004
|
if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: true });
|
|
465
1005
|
deletedSessionIds.push(sessionId);
|
|
466
1006
|
continue;
|
|
@@ -473,6 +1013,7 @@ export class DaemonCommandRouter {
|
|
|
473
1013
|
recordsRemainSessionIds.push(sessionId);
|
|
474
1014
|
if (args.mode === 'stop_and_delete' && !completed) {
|
|
475
1015
|
try {
|
|
1016
|
+
await markIntentionalStop(sessionId, 'stop_session');
|
|
476
1017
|
await this.deps.sessionHostControl.stopSession(sessionId);
|
|
477
1018
|
stoppedSessionIds.push(sessionId);
|
|
478
1019
|
} catch (stopError: any) {
|
|
@@ -1331,6 +1872,102 @@ export class DaemonCommandRouter {
|
|
|
1331
1872
|
}
|
|
1332
1873
|
}
|
|
1333
1874
|
|
|
1875
|
+
case 'get_mesh_ledger_slice': {
|
|
1876
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1877
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1878
|
+
try {
|
|
1879
|
+
const { readLedgerSlice } = await import('../mesh/mesh-ledger.js');
|
|
1880
|
+
const kind = Array.isArray(args?.kind) ? args.kind.filter((k: any) => typeof k === 'string') : undefined;
|
|
1881
|
+
const slice = readLedgerSlice(meshId, {
|
|
1882
|
+
afterId: typeof args?.afterId === 'string' ? args.afterId : undefined,
|
|
1883
|
+
since: typeof args?.since === 'string' ? args.since : undefined,
|
|
1884
|
+
kind,
|
|
1885
|
+
limit: typeof args?.limit === 'number' ? args.limit : undefined,
|
|
1886
|
+
});
|
|
1887
|
+
return { success: true, slice };
|
|
1888
|
+
} catch (e: any) {
|
|
1889
|
+
return { success: false, error: e.message };
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
case 'import_mesh_ledger_slice': {
|
|
1894
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1895
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1896
|
+
try {
|
|
1897
|
+
const { appendRemoteLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
1898
|
+
const entries = Array.isArray(args?.entries)
|
|
1899
|
+
? args.entries as any[]
|
|
1900
|
+
: Array.isArray(args?.slice?.entries)
|
|
1901
|
+
? args.slice.entries as any[]
|
|
1902
|
+
: [];
|
|
1903
|
+
const result = appendRemoteLedgerEntries(meshId, entries as any);
|
|
1904
|
+
return { success: true, result, summary: getLedgerSummary(meshId) };
|
|
1905
|
+
} catch (e: any) {
|
|
1906
|
+
return { success: false, error: e.message };
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
case 'get_mesh_queue': {
|
|
1911
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1912
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1913
|
+
try {
|
|
1914
|
+
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
1915
|
+
const status = Array.isArray(args?.status)
|
|
1916
|
+
? args.status.map((s: any) => typeof s === 'string' ? s.trim() : '').filter(Boolean)
|
|
1917
|
+
: undefined;
|
|
1918
|
+
const queue = getQueue(meshId, { status: status as any });
|
|
1919
|
+
const summary = getMeshQueueStats(meshId);
|
|
1920
|
+
return {
|
|
1921
|
+
success: true,
|
|
1922
|
+
queue,
|
|
1923
|
+
summary,
|
|
1924
|
+
sourceOfTruth: {
|
|
1925
|
+
kind: 'mesh_work_queue_file',
|
|
1926
|
+
activeStatuses: ['pending', 'assigned'],
|
|
1927
|
+
historicalStatuses: ['completed', 'failed', 'cancelled'],
|
|
1928
|
+
notes: 'pending/assigned are active work; completed/failed/cancelled are historical records.',
|
|
1929
|
+
},
|
|
1930
|
+
};
|
|
1931
|
+
} catch (e: any) {
|
|
1932
|
+
return { success: false, error: e.message };
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
case 'cancel_mesh_queue_task': {
|
|
1937
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1938
|
+
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
1939
|
+
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
1940
|
+
try {
|
|
1941
|
+
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
1942
|
+
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
1943
|
+
const task = cancelTask(meshId, taskId, { reason });
|
|
1944
|
+
if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
|
|
1945
|
+
return { success: true, task };
|
|
1946
|
+
} catch (e: any) {
|
|
1947
|
+
return { success: false, error: e.message };
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
case 'requeue_mesh_queue_task': {
|
|
1952
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1953
|
+
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
1954
|
+
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
1955
|
+
try {
|
|
1956
|
+
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
1957
|
+
const task = requeueTask(meshId, taskId, {
|
|
1958
|
+
reason: typeof args?.reason === 'string' ? args.reason : undefined,
|
|
1959
|
+
targetNodeId: typeof args?.targetNodeId === 'string' ? args.targetNodeId.trim() : undefined,
|
|
1960
|
+
targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : undefined,
|
|
1961
|
+
clearTargetNode: args?.clearTargetNode === true,
|
|
1962
|
+
clearTargetSession: args?.clearTargetSession !== false,
|
|
1963
|
+
});
|
|
1964
|
+
if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
|
|
1965
|
+
return { success: true, task };
|
|
1966
|
+
} catch (e: any) {
|
|
1967
|
+
return { success: false, error: e.message };
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1334
1971
|
case 'add_mesh_node': {
|
|
1335
1972
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1336
1973
|
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
@@ -1403,6 +2040,7 @@ export class DaemonCommandRouter {
|
|
|
1403
2040
|
mode,
|
|
1404
2041
|
sessionIds,
|
|
1405
2042
|
dryRun: args?.dryRun === true,
|
|
2043
|
+
source: 'mesh_cleanup_sessions',
|
|
1406
2044
|
});
|
|
1407
2045
|
return result;
|
|
1408
2046
|
} catch (e: any) {
|
|
@@ -1441,10 +2079,62 @@ export class DaemonCommandRouter {
|
|
|
1441
2079
|
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
1442
2080
|
const baseBranch = baseBranchStdout.trim();
|
|
1443
2081
|
|
|
2082
|
+
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2083
|
+
if (validationSummary.status === 'failed') {
|
|
2084
|
+
return {
|
|
2085
|
+
success: false,
|
|
2086
|
+
code: 'validation_failed',
|
|
2087
|
+
convergenceStatus: 'blocked_review',
|
|
2088
|
+
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2089
|
+
branch,
|
|
2090
|
+
into: baseBranch,
|
|
2091
|
+
validationSummary,
|
|
2092
|
+
finalBranchConvergenceState: {
|
|
2093
|
+
branch,
|
|
2094
|
+
baseBranch,
|
|
2095
|
+
merged: false,
|
|
2096
|
+
removed: false,
|
|
2097
|
+
validation: 'failed',
|
|
2098
|
+
status: 'blocked_review',
|
|
2099
|
+
},
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
if (validationSummary.status === 'skipped') {
|
|
2103
|
+
return {
|
|
2104
|
+
success: false,
|
|
2105
|
+
code: 'validation_unavailable',
|
|
2106
|
+
convergenceStatus: 'blocked_review',
|
|
2107
|
+
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2108
|
+
branch,
|
|
2109
|
+
into: baseBranch,
|
|
2110
|
+
validationSummary,
|
|
2111
|
+
finalBranchConvergenceState: {
|
|
2112
|
+
branch,
|
|
2113
|
+
baseBranch,
|
|
2114
|
+
merged: false,
|
|
2115
|
+
removed: false,
|
|
2116
|
+
validation: 'unavailable',
|
|
2117
|
+
status: 'blocked_review',
|
|
2118
|
+
},
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
|
|
1444
2122
|
try {
|
|
1445
2123
|
await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
1446
2124
|
} catch (e: any) {
|
|
1447
|
-
return {
|
|
2125
|
+
return {
|
|
2126
|
+
success: false,
|
|
2127
|
+
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2128
|
+
validationSummary,
|
|
2129
|
+
finalBranchConvergenceState: {
|
|
2130
|
+
branch,
|
|
2131
|
+
baseBranch,
|
|
2132
|
+
merged: false,
|
|
2133
|
+
removed: false,
|
|
2134
|
+
validation: 'passed',
|
|
2135
|
+
status: 'not_mergeable',
|
|
2136
|
+
},
|
|
2137
|
+
};
|
|
1448
2138
|
}
|
|
1449
2139
|
|
|
1450
2140
|
const removeResult = await this.execute('remove_mesh_node', {
|
|
@@ -1459,11 +2149,27 @@ export class DaemonCommandRouter {
|
|
|
1459
2149
|
appendLedgerEntry(meshId, {
|
|
1460
2150
|
kind: 'node_removed',
|
|
1461
2151
|
nodeId,
|
|
1462
|
-
payload: { refined: true, mergedBranch: branch, into: baseBranch },
|
|
2152
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
|
|
1463
2153
|
});
|
|
1464
2154
|
} catch {}
|
|
1465
2155
|
|
|
1466
|
-
return {
|
|
2156
|
+
return {
|
|
2157
|
+
success: true,
|
|
2158
|
+
merged: true,
|
|
2159
|
+
branch,
|
|
2160
|
+
into: baseBranch,
|
|
2161
|
+
removeResult,
|
|
2162
|
+
validationSummary,
|
|
2163
|
+
finalBranchConvergenceState: {
|
|
2164
|
+
branch: baseBranch,
|
|
2165
|
+
mergedBranch: branch,
|
|
2166
|
+
baseBranch,
|
|
2167
|
+
merged: true,
|
|
2168
|
+
removed: removeResult?.success !== false,
|
|
2169
|
+
validation: 'passed',
|
|
2170
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2171
|
+
},
|
|
2172
|
+
};
|
|
1467
2173
|
} catch (e: any) {
|
|
1468
2174
|
return { success: false, error: e.message };
|
|
1469
2175
|
}
|
|
@@ -1483,25 +2189,25 @@ export class DaemonCommandRouter {
|
|
|
1483
2189
|
);
|
|
1484
2190
|
let sessionCleanup: Record<string, unknown> | undefined;
|
|
1485
2191
|
if (node && sessionCleanupMode !== 'preserve') {
|
|
1486
|
-
sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode });
|
|
2192
|
+
sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode, source: 'mesh_remove_node' });
|
|
1487
2193
|
if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
|
|
1488
2194
|
}
|
|
1489
2195
|
|
|
1490
|
-
|
|
1491
|
-
if (node?.isLocalWorktree
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
:
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
// Continue with node removal even if worktree cleanup fails
|
|
2196
|
+
let worktreeCleanup: Record<string, unknown> | undefined;
|
|
2197
|
+
if (node?.isLocalWorktree) {
|
|
2198
|
+
const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId });
|
|
2199
|
+
if (cleanupResult.success === false) {
|
|
2200
|
+
return {
|
|
2201
|
+
success: false,
|
|
2202
|
+
removed: false,
|
|
2203
|
+
code: cleanupResult.code,
|
|
2204
|
+
error: cleanupResult.error,
|
|
2205
|
+
recoveryHint: cleanupResult.recoveryHint,
|
|
2206
|
+
...(sessionCleanup ? { sessionCleanup } : {}),
|
|
2207
|
+
worktreeCleanup: cleanupResult,
|
|
2208
|
+
};
|
|
1504
2209
|
}
|
|
2210
|
+
worktreeCleanup = cleanupResult;
|
|
1505
2211
|
}
|
|
1506
2212
|
|
|
1507
2213
|
let removed = false;
|
|
@@ -1519,12 +2225,21 @@ export class DaemonCommandRouter {
|
|
|
1519
2225
|
appendLedgerEntry(meshId, {
|
|
1520
2226
|
kind: 'node_removed',
|
|
1521
2227
|
nodeId,
|
|
1522
|
-
payload: {
|
|
2228
|
+
payload: {
|
|
2229
|
+
worktree: !!node?.isLocalWorktree,
|
|
2230
|
+
sessionCleanupMode,
|
|
2231
|
+
workspace: typeof node?.workspace === 'string' ? node.workspace : undefined,
|
|
2232
|
+
daemonId: typeof node?.daemonId === 'string' ? node.daemonId : undefined,
|
|
2233
|
+
worktreeBranch: typeof node?.worktreeBranch === 'string' ? node.worktreeBranch : undefined,
|
|
2234
|
+
worktreeCleanupFallback: typeof worktreeCleanup?.fallback === 'string' ? worktreeCleanup.fallback : undefined,
|
|
2235
|
+
forced: worktreeCleanup?.forced === true ? true : undefined,
|
|
2236
|
+
forceFallbackReason: typeof worktreeCleanup?.reason === 'string' ? worktreeCleanup.reason : undefined,
|
|
2237
|
+
},
|
|
1523
2238
|
});
|
|
1524
2239
|
} catch { /* ledger append is best-effort */ }
|
|
1525
2240
|
}
|
|
1526
2241
|
|
|
1527
|
-
return { success: true, removed, ...(sessionCleanup ? { sessionCleanup } : {}) };
|
|
2242
|
+
return { success: true, removed, ...(sessionCleanup ? { sessionCleanup } : {}), ...(worktreeCleanup ? { worktreeCleanup } : {}) };
|
|
1528
2243
|
} catch (e: any) {
|
|
1529
2244
|
return { success: false, error: e.message };
|
|
1530
2245
|
}
|
|
@@ -1564,6 +2279,7 @@ export class DaemonCommandRouter {
|
|
|
1564
2279
|
workspace: result.worktreePath,
|
|
1565
2280
|
repoRoot: result.worktreePath,
|
|
1566
2281
|
daemonId: sourceNode.daemonId,
|
|
2282
|
+
machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
|
|
1567
2283
|
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
1568
2284
|
policy: { ...(sourceNode.policy || {}) },
|
|
1569
2285
|
isLocalWorktree: true,
|
|
@@ -1577,6 +2293,7 @@ export class DaemonCommandRouter {
|
|
|
1577
2293
|
workspace: result.worktreePath,
|
|
1578
2294
|
repoRoot: result.worktreePath,
|
|
1579
2295
|
daemonId: sourceNode.daemonId,
|
|
2296
|
+
machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
|
|
1580
2297
|
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
1581
2298
|
isLocalWorktree: true,
|
|
1582
2299
|
worktreeBranch: result.branch,
|
|
@@ -1711,6 +2428,105 @@ export class DaemonCommandRouter {
|
|
|
1711
2428
|
};
|
|
1712
2429
|
}
|
|
1713
2430
|
|
|
2431
|
+
// ─── CLI-command MCP registration (Codex, Gemini CLI) ───────────
|
|
2432
|
+
if (coordinatorSetup.kind === 'cli_command') {
|
|
2433
|
+
// Build coordinator prompt first — fail closed on errors.
|
|
2434
|
+
let cliCmdSystemPrompt = '';
|
|
2435
|
+
try {
|
|
2436
|
+
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
|
|
2437
|
+
} catch (error: any) {
|
|
2438
|
+
const message = error?.message || String(error);
|
|
2439
|
+
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
2440
|
+
return {
|
|
2441
|
+
success: false,
|
|
2442
|
+
code: 'mesh_coordinator_prompt_failed',
|
|
2443
|
+
error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
|
|
2444
|
+
meshId, cliType, workspace,
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// Run the provider's MCP registration command.
|
|
2449
|
+
try {
|
|
2450
|
+
const { execFileSync: execCmdSync } = await import('node:child_process');
|
|
2451
|
+
const cmdParts = coordinatorSetup.command.trim().split(/\s+/);
|
|
2452
|
+
const [regCmd, ...regArgs] = cmdParts;
|
|
2453
|
+
LOG.info('MeshCoordinator', `Running MCP registration: ${coordinatorSetup.command}`);
|
|
2454
|
+
execCmdSync(regCmd, regArgs, { stdio: 'pipe', timeout: 15_000 });
|
|
2455
|
+
} catch (error: any) {
|
|
2456
|
+
// Non-fatal — server may already be registered (providers return exit 1 on duplicate).
|
|
2457
|
+
LOG.warn('MeshCoordinator', `MCP registration command failed (may be pre-registered): ${error?.message || error}`);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// Inject system prompt using provider-native methods.
|
|
2461
|
+
// Codex: -c 'instructions="..."' CLI config override
|
|
2462
|
+
// Gemini: write GEMINI.md to workspace (auto-loaded as context)
|
|
2463
|
+
const cliCmdArgs: string[] = [];
|
|
2464
|
+
const cliCmdEnv: Record<string, string> = {};
|
|
2465
|
+
if (cliCmdSystemPrompt) {
|
|
2466
|
+
if (cliType === 'codex-cli') {
|
|
2467
|
+
// Codex reads `developer_instructions` from config.toml as system instructions.
|
|
2468
|
+
// The -c flag overrides a config key for this session only.
|
|
2469
|
+
cliCmdArgs.push('-c', `developer_instructions=${JSON.stringify(cliCmdSystemPrompt)}`);
|
|
2470
|
+
} else if (cliType === 'gemini-cli') {
|
|
2471
|
+
// Gemini CLI auto-loads GEMINI.md from CWD as project context.
|
|
2472
|
+
// Write a temporary GEMINI.md to the workspace before launch.
|
|
2473
|
+
try {
|
|
2474
|
+
const { writeFileSync: wfs, existsSync: efs, readFileSync: rfs } = await import('node:fs');
|
|
2475
|
+
const geminiMdPath = `${workspace}/GEMINI.md`;
|
|
2476
|
+
const marker = '<!-- adhdev-mesh-coordinator-prompt -->';
|
|
2477
|
+
const markerEnd = '<!-- /adhdev-mesh-coordinator-prompt -->';
|
|
2478
|
+
const block = `${marker}\n${cliCmdSystemPrompt}\n${markerEnd}`;
|
|
2479
|
+
if (efs(geminiMdPath)) {
|
|
2480
|
+
const existing = rfs(geminiMdPath, 'utf-8');
|
|
2481
|
+
// Replace existing block or append
|
|
2482
|
+
const replaced = existing.replace(
|
|
2483
|
+
new RegExp(`${marker}[\\s\\S]*?${markerEnd}`, 'g'),
|
|
2484
|
+
block,
|
|
2485
|
+
);
|
|
2486
|
+
wfs(geminiMdPath, replaced.includes(marker) ? replaced : `${existing}\n\n${block}`);
|
|
2487
|
+
} else {
|
|
2488
|
+
wfs(geminiMdPath, block);
|
|
2489
|
+
}
|
|
2490
|
+
LOG.info('MeshCoordinator', `Wrote coordinator prompt to ${workspace}/GEMINI.md`);
|
|
2491
|
+
} catch (e: any) {
|
|
2492
|
+
LOG.warn('MeshCoordinator', `Could not write GEMINI.md: ${e?.message || e}`);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
2498
|
+
cliType,
|
|
2499
|
+
dir: workspace,
|
|
2500
|
+
cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
|
|
2501
|
+
env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
|
|
2502
|
+
settings: { meshCoordinatorFor: meshId },
|
|
2503
|
+
});
|
|
2504
|
+
|
|
2505
|
+
if (!cliCmdLaunch?.success) {
|
|
2506
|
+
return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
|
|
2510
|
+
try {
|
|
2511
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2512
|
+
appendLedgerEntry(meshId, {
|
|
2513
|
+
kind: 'coordinator_started',
|
|
2514
|
+
sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
|
|
2515
|
+
providerType: cliType,
|
|
2516
|
+
payload: { workspace },
|
|
2517
|
+
});
|
|
2518
|
+
} catch { /* best-effort */ }
|
|
2519
|
+
|
|
2520
|
+
return {
|
|
2521
|
+
success: true,
|
|
2522
|
+
meshId,
|
|
2523
|
+
cliType,
|
|
2524
|
+
workspace,
|
|
2525
|
+
sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
|
|
2526
|
+
mcpRegistered: true,
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
|
|
1714
2530
|
const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
|
|
1715
2531
|
if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
|
|
1716
2532
|
return {
|
|
@@ -1777,9 +2593,11 @@ export class DaemonCommandRouter {
|
|
|
1777
2593
|
args: coordinatorSetup.mcpServer.args,
|
|
1778
2594
|
};
|
|
1779
2595
|
if (args?.inlineMesh) {
|
|
2596
|
+
const modeArgIndex = coordinatorSetup.mcpServer.args.findIndex((value: string) => value === '--mode');
|
|
2597
|
+
const mcpTransport = modeArgIndex >= 0 ? coordinatorSetup.mcpServer.args[modeArgIndex + 1] : 'ipc';
|
|
1780
2598
|
mcpServerEntry.env = {
|
|
1781
2599
|
ADHDEV_INLINE_MESH: JSON.stringify(mesh),
|
|
1782
|
-
ADHDEV_MCP_TRANSPORT: 'ipc',
|
|
2600
|
+
ADHDEV_MCP_TRANSPORT: mcpTransport === 'local' ? 'local' : 'ipc',
|
|
1783
2601
|
};
|
|
1784
2602
|
}
|
|
1785
2603
|
|
|
@@ -1801,7 +2619,10 @@ export class DaemonCommandRouter {
|
|
|
1801
2619
|
if (hadExistingMcpConfig) {
|
|
1802
2620
|
try {
|
|
1803
2621
|
const parsedExistingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
|
|
1804
|
-
|
|
2622
|
+
const existingCoordinatorConfig = hermesManualFallback
|
|
2623
|
+
? stripHermesCoordinatorTempModelProviderOverrides(parsedExistingMcpConfig)
|
|
2624
|
+
: parsedExistingMcpConfig;
|
|
2625
|
+
existingMcpConfig = { ...existingMcpConfig, ...existingCoordinatorConfig };
|
|
1805
2626
|
copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
|
|
1806
2627
|
} catch (error: any) {
|
|
1807
2628
|
LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
|
|
@@ -1893,6 +2714,123 @@ export class DaemonCommandRouter {
|
|
|
1893
2714
|
}
|
|
1894
2715
|
}
|
|
1895
2716
|
|
|
2717
|
+
case 'mesh_status': {
|
|
2718
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2719
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2720
|
+
try {
|
|
2721
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2722
|
+
const mesh = meshRecord?.mesh;
|
|
2723
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2724
|
+
|
|
2725
|
+
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
2726
|
+
const queue = getQueue(meshId);
|
|
2727
|
+
const queueSummary = getMeshQueueStats(meshId);
|
|
2728
|
+
|
|
2729
|
+
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
2730
|
+
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
2731
|
+
const ledgerSummary = getLedgerSummary(meshId);
|
|
2732
|
+
|
|
2733
|
+
const nodeStatuses = [];
|
|
2734
|
+
for (const node of mesh.nodes || []) {
|
|
2735
|
+
const status: Record<string, unknown> = {
|
|
2736
|
+
nodeId: node.id || node.nodeId,
|
|
2737
|
+
machineLabel: node.machineLabel || node.id || node.nodeId,
|
|
2738
|
+
workspace: node.workspace,
|
|
2739
|
+
repoRoot: node.repoRoot,
|
|
2740
|
+
isLocalWorktree: node.isLocalWorktree,
|
|
2741
|
+
worktreeBranch: node.worktreeBranch,
|
|
2742
|
+
daemonId: node.daemonId,
|
|
2743
|
+
machineId: node.machineId,
|
|
2744
|
+
health: 'unknown',
|
|
2745
|
+
providers: node.providers || [],
|
|
2746
|
+
activeSessions: [],
|
|
2747
|
+
};
|
|
2748
|
+
if (node.workspace && typeof node.workspace === 'string') {
|
|
2749
|
+
try {
|
|
2750
|
+
const { execFile } = await import('node:child_process');
|
|
2751
|
+
const { promisify } = await import('node:util');
|
|
2752
|
+
const execFileAsync = promisify(execFile);
|
|
2753
|
+
|
|
2754
|
+
const runGit = async (args: string[]): Promise<string> => {
|
|
2755
|
+
const result = await execFileAsync('git', ['-C', node.workspace as string, ...args], {
|
|
2756
|
+
encoding: 'utf8',
|
|
2757
|
+
timeout: 10_000,
|
|
2758
|
+
});
|
|
2759
|
+
return result.stdout.trim();
|
|
2760
|
+
};
|
|
2761
|
+
|
|
2762
|
+
const branch = await runGit(['branch', '--show-current']).catch(() => '');
|
|
2763
|
+
const porc = await runGit(['status', '--porcelain']).catch(() => '');
|
|
2764
|
+
const headCommit = await runGit(['rev-parse', '--short', 'HEAD']).catch(() => null);
|
|
2765
|
+
const headMessage = await runGit(['log', '-1', '--format=%s']).catch(() => null);
|
|
2766
|
+
const upstream = await runGit(['rev-parse', '--abbrev-ref', '@{upstream}']).catch(() => null);
|
|
2767
|
+
const aheadBehind = await runGit(['rev-list', '--left-right', '--count', '@{upstream}...HEAD']).catch(() => '');
|
|
2768
|
+
const stashCount = await runGit(['stash', 'list']).catch(() => '');
|
|
2769
|
+
|
|
2770
|
+
let ahead = 0, behind = 0;
|
|
2771
|
+
if (aheadBehind) {
|
|
2772
|
+
const parts = aheadBehind.split(/\s+/);
|
|
2773
|
+
if (parts.length >= 2) {
|
|
2774
|
+
behind = parseInt(parts[0], 10) || 0;
|
|
2775
|
+
ahead = parseInt(parts[1], 10) || 0;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
const dirty = porc.length > 0;
|
|
2780
|
+
const lines = porc ? porc.split('\n').filter(Boolean) : [];
|
|
2781
|
+
let staged = 0, modified = 0, untracked = 0, deleted = 0, renamed = 0;
|
|
2782
|
+
for (const line of lines) {
|
|
2783
|
+
const xy = line.slice(0, 2);
|
|
2784
|
+
if (xy[0] !== ' ' && xy[0] !== '?') staged++;
|
|
2785
|
+
if (xy[1] === 'M') modified++;
|
|
2786
|
+
if (xy[1] === 'D') deleted++;
|
|
2787
|
+
if (xy[0] === 'R' || xy[1] === 'R') renamed++;
|
|
2788
|
+
if (xy === '??') untracked++;
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
status.git = {
|
|
2792
|
+
workspace: node.workspace,
|
|
2793
|
+
repoRoot: node.workspace,
|
|
2794
|
+
isGitRepo: true,
|
|
2795
|
+
branch: branch || null,
|
|
2796
|
+
headCommit,
|
|
2797
|
+
headMessage,
|
|
2798
|
+
upstream,
|
|
2799
|
+
ahead,
|
|
2800
|
+
behind,
|
|
2801
|
+
staged,
|
|
2802
|
+
modified,
|
|
2803
|
+
untracked,
|
|
2804
|
+
deleted,
|
|
2805
|
+
renamed,
|
|
2806
|
+
hasConflicts: false,
|
|
2807
|
+
conflictFiles: [],
|
|
2808
|
+
stashCount: stashCount ? stashCount.split('\n').filter(Boolean).length : 0,
|
|
2809
|
+
lastCheckedAt: Date.now(),
|
|
2810
|
+
};
|
|
2811
|
+
status.health = branch ? (dirty ? 'dirty' : 'online') : 'degraded';
|
|
2812
|
+
} catch {
|
|
2813
|
+
status.health = 'degraded';
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
nodeStatuses.push(status);
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
return {
|
|
2820
|
+
success: true,
|
|
2821
|
+
meshId: mesh.id,
|
|
2822
|
+
meshName: mesh.name,
|
|
2823
|
+
repoIdentity: mesh.repoIdentity,
|
|
2824
|
+
defaultBranch: mesh.defaultBranch,
|
|
2825
|
+
nodes: nodeStatuses,
|
|
2826
|
+
queue: { tasks: queue, summary: queueSummary },
|
|
2827
|
+
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
2828
|
+
};
|
|
2829
|
+
} catch (e: any) {
|
|
2830
|
+
return { success: false, error: e.message };
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
|
|
1896
2834
|
default:
|
|
1897
2835
|
break;
|
|
1898
2836
|
}
|