@adhdev/daemon-core 0.9.82-rc.59 → 0.9.82-rc.60
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/router.d.ts +11 -0
- package/dist/index.js +339 -207
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +339 -207
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/commands/router.ts +405 -223
package/package.json
CHANGED
package/src/commands/router.ts
CHANGED
|
@@ -38,7 +38,7 @@ import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../l
|
|
|
38
38
|
import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
|
|
39
39
|
import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
|
|
40
40
|
import { buildSessionEntries } from '../status/builders.js';
|
|
41
|
-
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents } from '../mesh/mesh-events.js';
|
|
41
|
+
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
42
42
|
import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
43
43
|
import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
|
|
44
44
|
import {
|
|
@@ -909,6 +909,35 @@ type MeshRefinePatchEquivalenceSummary = {
|
|
|
909
909
|
stderr?: string;
|
|
910
910
|
};
|
|
911
911
|
|
|
912
|
+
type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
|
|
913
|
+
|
|
914
|
+
type MeshRefineJobHandle = {
|
|
915
|
+
success: true;
|
|
916
|
+
async: true;
|
|
917
|
+
status: MeshRefineAsyncJobStatus;
|
|
918
|
+
jobId: string;
|
|
919
|
+
interactionId: string;
|
|
920
|
+
meshId: string;
|
|
921
|
+
nodeId: string;
|
|
922
|
+
targetNodeId: string;
|
|
923
|
+
targetDaemonId?: string;
|
|
924
|
+
workspace?: string;
|
|
925
|
+
startedAt: string;
|
|
926
|
+
completedAt?: string;
|
|
927
|
+
duplicate?: boolean;
|
|
928
|
+
eventDelivery: {
|
|
929
|
+
pendingEvents: true;
|
|
930
|
+
ledger: true;
|
|
931
|
+
};
|
|
932
|
+
evidence: {
|
|
933
|
+
pendingEventsCommand: 'get_pending_mesh_events';
|
|
934
|
+
ledgerCommand: 'get_mesh_ledger_slice';
|
|
935
|
+
taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
|
|
936
|
+
};
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
type MeshRefineTerminalJob = MeshRefineJobHandle & { result?: Record<string, unknown> };
|
|
940
|
+
|
|
912
941
|
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
913
942
|
const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
914
943
|
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
@@ -1395,6 +1424,10 @@ export class DaemonCommandRouter {
|
|
|
1395
1424
|
private inlineMeshCache = new Map<string, any>();
|
|
1396
1425
|
/** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
|
|
1397
1426
|
private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any }>();
|
|
1427
|
+
/** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
|
|
1428
|
+
private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
|
|
1429
|
+
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
1430
|
+
private terminalRefineJobs = new Map<string, MeshRefineTerminalJob>();
|
|
1398
1431
|
|
|
1399
1432
|
constructor(deps: CommandRouterDeps) {
|
|
1400
1433
|
this.deps = deps;
|
|
@@ -2141,6 +2174,376 @@ export class DaemonCommandRouter {
|
|
|
2141
2174
|
}
|
|
2142
2175
|
}
|
|
2143
2176
|
|
|
2177
|
+
|
|
2178
|
+
private buildRefineJobKey(meshId: string, nodeId: string): string {
|
|
2179
|
+
return `${meshId}:${nodeId}`;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
private buildRefineJobHandle(args: {
|
|
2183
|
+
meshId: string;
|
|
2184
|
+
nodeId: string;
|
|
2185
|
+
node?: any;
|
|
2186
|
+
status?: MeshRefineAsyncJobStatus;
|
|
2187
|
+
startedAt?: string;
|
|
2188
|
+
completedAt?: string;
|
|
2189
|
+
jobId?: string;
|
|
2190
|
+
interactionId?: string;
|
|
2191
|
+
}): MeshRefineJobHandle {
|
|
2192
|
+
return {
|
|
2193
|
+
success: true,
|
|
2194
|
+
async: true,
|
|
2195
|
+
status: args.status || 'accepted',
|
|
2196
|
+
jobId: args.jobId || `refine_${createInteractionId()}`,
|
|
2197
|
+
interactionId: args.interactionId || createInteractionId(),
|
|
2198
|
+
meshId: args.meshId,
|
|
2199
|
+
nodeId: args.nodeId,
|
|
2200
|
+
targetNodeId: args.nodeId,
|
|
2201
|
+
targetDaemonId: readStringValue(args.node?.daemonId),
|
|
2202
|
+
workspace: readStringValue(args.node?.workspace),
|
|
2203
|
+
startedAt: args.startedAt || new Date().toISOString(),
|
|
2204
|
+
...(args.completedAt ? { completedAt: args.completedAt } : {}),
|
|
2205
|
+
eventDelivery: { pendingEvents: true, ledger: true },
|
|
2206
|
+
evidence: {
|
|
2207
|
+
pendingEventsCommand: 'get_pending_mesh_events',
|
|
2208
|
+
ledgerCommand: 'get_mesh_ledger_slice',
|
|
2209
|
+
taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
|
|
2210
|
+
},
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
private queueRefineJobEvent(event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
|
|
2215
|
+
queuePendingMeshCoordinatorEvent({
|
|
2216
|
+
event,
|
|
2217
|
+
meshId: handle.meshId,
|
|
2218
|
+
nodeLabel: handle.targetNodeId,
|
|
2219
|
+
nodeId: handle.targetNodeId,
|
|
2220
|
+
workspace: handle.workspace,
|
|
2221
|
+
metadataEvent: {
|
|
2222
|
+
source: 'refine_mesh_node_async_job',
|
|
2223
|
+
jobId: handle.jobId,
|
|
2224
|
+
interactionId: handle.interactionId,
|
|
2225
|
+
meshId: handle.meshId,
|
|
2226
|
+
nodeId: handle.targetNodeId,
|
|
2227
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2228
|
+
workspace: handle.workspace,
|
|
2229
|
+
status: handle.status,
|
|
2230
|
+
startedAt: handle.startedAt,
|
|
2231
|
+
completedAt: handle.completedAt,
|
|
2232
|
+
...(result ? { result } : {}),
|
|
2233
|
+
},
|
|
2234
|
+
queuedAt: Date.now(),
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
private async appendRefineJobLedger(kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
|
|
2239
|
+
try {
|
|
2240
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2241
|
+
appendLedgerEntry(handle.meshId, {
|
|
2242
|
+
kind,
|
|
2243
|
+
nodeId: handle.targetNodeId,
|
|
2244
|
+
payload: {
|
|
2245
|
+
source: 'refine_mesh_node_async_job',
|
|
2246
|
+
refineJob: {
|
|
2247
|
+
jobId: handle.jobId,
|
|
2248
|
+
interactionId: handle.interactionId,
|
|
2249
|
+
status: handle.status,
|
|
2250
|
+
meshId: handle.meshId,
|
|
2251
|
+
nodeId: handle.targetNodeId,
|
|
2252
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2253
|
+
workspace: handle.workspace,
|
|
2254
|
+
startedAt: handle.startedAt,
|
|
2255
|
+
completedAt: handle.completedAt,
|
|
2256
|
+
},
|
|
2257
|
+
async: true,
|
|
2258
|
+
...(result ? {
|
|
2259
|
+
success: result.success === true,
|
|
2260
|
+
result,
|
|
2261
|
+
finalBranchConvergenceState: result.finalBranchConvergenceState,
|
|
2262
|
+
} : {}),
|
|
2263
|
+
},
|
|
2264
|
+
});
|
|
2265
|
+
} catch (e: any) {
|
|
2266
|
+
LOG.warn('Mesh', `[Refinery] Failed to append async refine ledger entry: ${e?.message || e}`);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
2271
|
+
const refineStages: Array<Record<string, unknown>> = [];
|
|
2272
|
+
try {
|
|
2273
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2274
|
+
const mesh = meshRecord?.mesh;
|
|
2275
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2276
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
2277
|
+
|
|
2278
|
+
if (!node.isLocalWorktree || !node.workspace) {
|
|
2279
|
+
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
const sourceNode = node.clonedFromNodeId
|
|
2283
|
+
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2284
|
+
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2285
|
+
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2286
|
+
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
2287
|
+
|
|
2288
|
+
const { execFile } = await import('node:child_process');
|
|
2289
|
+
const { promisify } = await import('node:util');
|
|
2290
|
+
const execFileAsync = promisify(execFile);
|
|
2291
|
+
|
|
2292
|
+
const resolveStarted = Date.now();
|
|
2293
|
+
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2294
|
+
const branch = branchStdout.trim();
|
|
2295
|
+
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
2296
|
+
|
|
2297
|
+
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2298
|
+
const baseBranch = baseBranchStdout.trim();
|
|
2299
|
+
const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2300
|
+
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
2301
|
+
const baseHead = baseHeadStdout.trim();
|
|
2302
|
+
const branchHead = branchHeadStdout.trim();
|
|
2303
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
|
|
2304
|
+
|
|
2305
|
+
const validationStarted = Date.now();
|
|
2306
|
+
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2307
|
+
recordMeshRefineStage(
|
|
2308
|
+
refineStages,
|
|
2309
|
+
'validation',
|
|
2310
|
+
validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
|
|
2311
|
+
validationStarted,
|
|
2312
|
+
{ validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
|
|
2313
|
+
);
|
|
2314
|
+
if (validationSummary.status === 'failed') {
|
|
2315
|
+
return {
|
|
2316
|
+
success: false,
|
|
2317
|
+
code: 'validation_failed',
|
|
2318
|
+
convergenceStatus: 'blocked_review',
|
|
2319
|
+
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2320
|
+
branch,
|
|
2321
|
+
into: baseBranch,
|
|
2322
|
+
validationSummary,
|
|
2323
|
+
refineStages,
|
|
2324
|
+
finalBranchConvergenceState: {
|
|
2325
|
+
branch,
|
|
2326
|
+
baseBranch,
|
|
2327
|
+
merged: false,
|
|
2328
|
+
removed: false,
|
|
2329
|
+
validation: 'failed',
|
|
2330
|
+
status: 'blocked_review',
|
|
2331
|
+
},
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
if (validationSummary.status === 'skipped') {
|
|
2335
|
+
return {
|
|
2336
|
+
success: false,
|
|
2337
|
+
code: 'validation_unavailable',
|
|
2338
|
+
convergenceStatus: 'blocked_review',
|
|
2339
|
+
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2340
|
+
branch,
|
|
2341
|
+
into: baseBranch,
|
|
2342
|
+
validationSummary,
|
|
2343
|
+
refineStages,
|
|
2344
|
+
finalBranchConvergenceState: {
|
|
2345
|
+
branch,
|
|
2346
|
+
baseBranch,
|
|
2347
|
+
merged: false,
|
|
2348
|
+
removed: false,
|
|
2349
|
+
validation: 'unavailable',
|
|
2350
|
+
status: 'blocked_review',
|
|
2351
|
+
},
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
const patchEquivalenceStarted = Date.now();
|
|
2356
|
+
const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
2357
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
2358
|
+
equivalent: patchEquivalence.equivalent,
|
|
2359
|
+
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
2360
|
+
actualPatchId: patchEquivalence.actualPatchId,
|
|
2361
|
+
error: patchEquivalence.error,
|
|
2362
|
+
});
|
|
2363
|
+
if (!patchEquivalence.equivalent) {
|
|
2364
|
+
return {
|
|
2365
|
+
success: false,
|
|
2366
|
+
code: 'patch_equivalence_failed',
|
|
2367
|
+
convergenceStatus: 'blocked_review',
|
|
2368
|
+
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
2369
|
+
branch,
|
|
2370
|
+
into: baseBranch,
|
|
2371
|
+
validationSummary,
|
|
2372
|
+
patchEquivalence,
|
|
2373
|
+
refineStages,
|
|
2374
|
+
finalBranchConvergenceState: {
|
|
2375
|
+
branch,
|
|
2376
|
+
baseBranch,
|
|
2377
|
+
merged: false,
|
|
2378
|
+
removed: false,
|
|
2379
|
+
validation: 'passed',
|
|
2380
|
+
patchEquivalence: 'failed',
|
|
2381
|
+
status: 'blocked_review',
|
|
2382
|
+
},
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
let mergeResult: Record<string, unknown> | undefined;
|
|
2387
|
+
const mergeStarted = Date.now();
|
|
2388
|
+
try {
|
|
2389
|
+
const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
2390
|
+
mergeResult = {
|
|
2391
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
2392
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
2393
|
+
durationMs: Date.now() - mergeStarted,
|
|
2394
|
+
};
|
|
2395
|
+
recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
|
|
2396
|
+
} catch (e: any) {
|
|
2397
|
+
recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
|
|
2398
|
+
error: e?.message || String(e),
|
|
2399
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
2400
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
2401
|
+
});
|
|
2402
|
+
return {
|
|
2403
|
+
success: false,
|
|
2404
|
+
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2405
|
+
validationSummary,
|
|
2406
|
+
patchEquivalence,
|
|
2407
|
+
refineStages,
|
|
2408
|
+
finalBranchConvergenceState: {
|
|
2409
|
+
branch,
|
|
2410
|
+
baseBranch,
|
|
2411
|
+
merged: false,
|
|
2412
|
+
removed: false,
|
|
2413
|
+
validation: 'passed',
|
|
2414
|
+
patchEquivalence: 'passed',
|
|
2415
|
+
status: 'not_mergeable',
|
|
2416
|
+
},
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
const cleanupStarted = Date.now();
|
|
2421
|
+
const removeResult = await this.execute('remove_mesh_node', {
|
|
2422
|
+
meshId,
|
|
2423
|
+
nodeId,
|
|
2424
|
+
sessionCleanupMode: 'preserve',
|
|
2425
|
+
inlineMesh: args?.inlineMesh,
|
|
2426
|
+
});
|
|
2427
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
2428
|
+
removed: removeResult?.removed,
|
|
2429
|
+
code: removeResult?.code,
|
|
2430
|
+
error: removeResult?.error,
|
|
2431
|
+
});
|
|
2432
|
+
|
|
2433
|
+
let ledgerError: string | undefined;
|
|
2434
|
+
const ledgerStarted = Date.now();
|
|
2435
|
+
try {
|
|
2436
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2437
|
+
appendLedgerEntry(meshId, {
|
|
2438
|
+
kind: 'node_removed',
|
|
2439
|
+
nodeId,
|
|
2440
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence },
|
|
2441
|
+
});
|
|
2442
|
+
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
2443
|
+
} catch (e: any) {
|
|
2444
|
+
ledgerError = e?.message || String(e);
|
|
2445
|
+
recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
const finalBranchConvergenceState = {
|
|
2449
|
+
branch: baseBranch,
|
|
2450
|
+
mergedBranch: branch,
|
|
2451
|
+
baseBranch,
|
|
2452
|
+
merged: true,
|
|
2453
|
+
removed: removeResult?.success !== false,
|
|
2454
|
+
validation: 'passed',
|
|
2455
|
+
patchEquivalence: 'passed',
|
|
2456
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2457
|
+
};
|
|
2458
|
+
|
|
2459
|
+
if (removeResult?.success === false) {
|
|
2460
|
+
return {
|
|
2461
|
+
success: false,
|
|
2462
|
+
code: 'cleanup_failed',
|
|
2463
|
+
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
2464
|
+
merged: true,
|
|
2465
|
+
branch,
|
|
2466
|
+
into: baseBranch,
|
|
2467
|
+
removeResult,
|
|
2468
|
+
validationSummary,
|
|
2469
|
+
patchEquivalence,
|
|
2470
|
+
mergeResult,
|
|
2471
|
+
refineStages,
|
|
2472
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
2473
|
+
finalBranchConvergenceState,
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
return {
|
|
2478
|
+
success: true,
|
|
2479
|
+
merged: true,
|
|
2480
|
+
branch,
|
|
2481
|
+
into: baseBranch,
|
|
2482
|
+
removeResult,
|
|
2483
|
+
validationSummary,
|
|
2484
|
+
patchEquivalence,
|
|
2485
|
+
mergeResult,
|
|
2486
|
+
refineStages,
|
|
2487
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
2488
|
+
finalBranchConvergenceState,
|
|
2489
|
+
};
|
|
2490
|
+
} catch (e: any) {
|
|
2491
|
+
return { success: false, error: e.message, refineStages };
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
private async finishMeshRefineJob(handle: MeshRefineJobHandle, args: any): Promise<void> {
|
|
2496
|
+
const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
|
|
2497
|
+
let result: Record<string, unknown>;
|
|
2498
|
+
try {
|
|
2499
|
+
result = await this.executeMeshRefineNodeSynchronously(handle.meshId, handle.targetNodeId, args) as Record<string, unknown>;
|
|
2500
|
+
} catch (e: any) {
|
|
2501
|
+
result = { success: false, error: e?.message || String(e) };
|
|
2502
|
+
}
|
|
2503
|
+
const completedAt = new Date().toISOString();
|
|
2504
|
+
const terminalHandle = this.buildRefineJobHandle({
|
|
2505
|
+
meshId: handle.meshId,
|
|
2506
|
+
nodeId: handle.targetNodeId,
|
|
2507
|
+
status: result.success === true ? 'completed' : 'failed',
|
|
2508
|
+
startedAt: handle.startedAt,
|
|
2509
|
+
completedAt,
|
|
2510
|
+
jobId: handle.jobId,
|
|
2511
|
+
interactionId: handle.interactionId,
|
|
2512
|
+
node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
|
|
2513
|
+
});
|
|
2514
|
+
const terminal: MeshRefineTerminalJob = { ...terminalHandle, result };
|
|
2515
|
+
this.terminalRefineJobs.set(key, terminal);
|
|
2516
|
+
this.runningRefineJobs.delete(key);
|
|
2517
|
+
this.invalidateAggregateMeshStatus(handle.meshId);
|
|
2518
|
+
await this.appendRefineJobLedger(result.success === true ? 'task_completed' : 'task_failed', terminalHandle, result);
|
|
2519
|
+
this.queueRefineJobEvent(result.success === true ? 'refine:completed' : 'refine:failed', terminalHandle, result);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
2523
|
+
const key = this.buildRefineJobKey(meshId, nodeId);
|
|
2524
|
+
const running = this.runningRefineJobs.get(key);
|
|
2525
|
+
if (running) return { ...running, duplicate: true };
|
|
2526
|
+
const terminal = this.terminalRefineJobs.get(key);
|
|
2527
|
+
if (terminal) return { ...terminal, duplicate: true };
|
|
2528
|
+
|
|
2529
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2530
|
+
const mesh = meshRecord?.mesh;
|
|
2531
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2532
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
2533
|
+
if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
|
|
2534
|
+
|
|
2535
|
+
const handle = this.buildRefineJobHandle({ meshId, nodeId, node });
|
|
2536
|
+
this.runningRefineJobs.set(key, handle);
|
|
2537
|
+
await this.appendRefineJobLedger('task_dispatched', handle);
|
|
2538
|
+
this.queueRefineJobEvent('refine:accepted', handle);
|
|
2539
|
+
|
|
2540
|
+
setImmediate(() => {
|
|
2541
|
+
void this.finishMeshRefineJob(handle, args);
|
|
2542
|
+
});
|
|
2543
|
+
|
|
2544
|
+
return handle;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2144
2547
|
// ─── Daemon-level command core ───────────────────
|
|
2145
2548
|
|
|
2146
2549
|
/**
|
|
@@ -3359,228 +3762,7 @@ export class DaemonCommandRouter {
|
|
|
3359
3762
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3360
3763
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
3361
3764
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3362
|
-
|
|
3363
|
-
try {
|
|
3364
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3365
|
-
const mesh = meshRecord?.mesh;
|
|
3366
|
-
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
3367
|
-
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
3368
|
-
|
|
3369
|
-
if (!node.isLocalWorktree || !node.workspace) {
|
|
3370
|
-
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
3371
|
-
}
|
|
3372
|
-
|
|
3373
|
-
const sourceNode = node.clonedFromNodeId
|
|
3374
|
-
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
3375
|
-
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
3376
|
-
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
3377
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
3378
|
-
|
|
3379
|
-
const { execFile } = await import('node:child_process');
|
|
3380
|
-
const { promisify } = await import('node:util');
|
|
3381
|
-
const execFileAsync = promisify(execFile);
|
|
3382
|
-
|
|
3383
|
-
const resolveStarted = Date.now();
|
|
3384
|
-
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
3385
|
-
const branch = branchStdout.trim();
|
|
3386
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
3387
|
-
|
|
3388
|
-
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
3389
|
-
const baseBranch = baseBranchStdout.trim();
|
|
3390
|
-
const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
3391
|
-
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
3392
|
-
const baseHead = baseHeadStdout.trim();
|
|
3393
|
-
const branchHead = branchHeadStdout.trim();
|
|
3394
|
-
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
|
|
3395
|
-
|
|
3396
|
-
const validationStarted = Date.now();
|
|
3397
|
-
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
3398
|
-
recordMeshRefineStage(
|
|
3399
|
-
refineStages,
|
|
3400
|
-
'validation',
|
|
3401
|
-
validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
|
|
3402
|
-
validationStarted,
|
|
3403
|
-
{ validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
|
|
3404
|
-
);
|
|
3405
|
-
if (validationSummary.status === 'failed') {
|
|
3406
|
-
return {
|
|
3407
|
-
success: false,
|
|
3408
|
-
code: 'validation_failed',
|
|
3409
|
-
convergenceStatus: 'blocked_review',
|
|
3410
|
-
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
3411
|
-
branch,
|
|
3412
|
-
into: baseBranch,
|
|
3413
|
-
validationSummary,
|
|
3414
|
-
refineStages,
|
|
3415
|
-
finalBranchConvergenceState: {
|
|
3416
|
-
branch,
|
|
3417
|
-
baseBranch,
|
|
3418
|
-
merged: false,
|
|
3419
|
-
removed: false,
|
|
3420
|
-
validation: 'failed',
|
|
3421
|
-
status: 'blocked_review',
|
|
3422
|
-
},
|
|
3423
|
-
};
|
|
3424
|
-
}
|
|
3425
|
-
if (validationSummary.status === 'skipped') {
|
|
3426
|
-
return {
|
|
3427
|
-
success: false,
|
|
3428
|
-
code: 'validation_unavailable',
|
|
3429
|
-
convergenceStatus: 'blocked_review',
|
|
3430
|
-
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
3431
|
-
branch,
|
|
3432
|
-
into: baseBranch,
|
|
3433
|
-
validationSummary,
|
|
3434
|
-
refineStages,
|
|
3435
|
-
finalBranchConvergenceState: {
|
|
3436
|
-
branch,
|
|
3437
|
-
baseBranch,
|
|
3438
|
-
merged: false,
|
|
3439
|
-
removed: false,
|
|
3440
|
-
validation: 'unavailable',
|
|
3441
|
-
status: 'blocked_review',
|
|
3442
|
-
},
|
|
3443
|
-
};
|
|
3444
|
-
}
|
|
3445
|
-
|
|
3446
|
-
const patchEquivalenceStarted = Date.now();
|
|
3447
|
-
const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
3448
|
-
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
3449
|
-
equivalent: patchEquivalence.equivalent,
|
|
3450
|
-
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
3451
|
-
actualPatchId: patchEquivalence.actualPatchId,
|
|
3452
|
-
error: patchEquivalence.error,
|
|
3453
|
-
});
|
|
3454
|
-
if (!patchEquivalence.equivalent) {
|
|
3455
|
-
return {
|
|
3456
|
-
success: false,
|
|
3457
|
-
code: 'patch_equivalence_failed',
|
|
3458
|
-
convergenceStatus: 'blocked_review',
|
|
3459
|
-
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
3460
|
-
branch,
|
|
3461
|
-
into: baseBranch,
|
|
3462
|
-
validationSummary,
|
|
3463
|
-
patchEquivalence,
|
|
3464
|
-
refineStages,
|
|
3465
|
-
finalBranchConvergenceState: {
|
|
3466
|
-
branch,
|
|
3467
|
-
baseBranch,
|
|
3468
|
-
merged: false,
|
|
3469
|
-
removed: false,
|
|
3470
|
-
validation: 'passed',
|
|
3471
|
-
patchEquivalence: 'failed',
|
|
3472
|
-
status: 'blocked_review',
|
|
3473
|
-
},
|
|
3474
|
-
};
|
|
3475
|
-
}
|
|
3476
|
-
|
|
3477
|
-
let mergeResult: Record<string, unknown> | undefined;
|
|
3478
|
-
const mergeStarted = Date.now();
|
|
3479
|
-
try {
|
|
3480
|
-
const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
3481
|
-
mergeResult = {
|
|
3482
|
-
stdout: truncateValidationOutput(result.stdout),
|
|
3483
|
-
stderr: truncateValidationOutput(result.stderr),
|
|
3484
|
-
durationMs: Date.now() - mergeStarted,
|
|
3485
|
-
};
|
|
3486
|
-
recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
|
|
3487
|
-
} catch (e: any) {
|
|
3488
|
-
recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
|
|
3489
|
-
error: e?.message || String(e),
|
|
3490
|
-
stdout: truncateValidationOutput(e?.stdout),
|
|
3491
|
-
stderr: truncateValidationOutput(e?.stderr),
|
|
3492
|
-
});
|
|
3493
|
-
return {
|
|
3494
|
-
success: false,
|
|
3495
|
-
error: `Merge failed (conflicts?): ${e.message}`,
|
|
3496
|
-
validationSummary,
|
|
3497
|
-
patchEquivalence,
|
|
3498
|
-
refineStages,
|
|
3499
|
-
finalBranchConvergenceState: {
|
|
3500
|
-
branch,
|
|
3501
|
-
baseBranch,
|
|
3502
|
-
merged: false,
|
|
3503
|
-
removed: false,
|
|
3504
|
-
validation: 'passed',
|
|
3505
|
-
patchEquivalence: 'passed',
|
|
3506
|
-
status: 'not_mergeable',
|
|
3507
|
-
},
|
|
3508
|
-
};
|
|
3509
|
-
}
|
|
3510
|
-
|
|
3511
|
-
const cleanupStarted = Date.now();
|
|
3512
|
-
const removeResult = await this.execute('remove_mesh_node', {
|
|
3513
|
-
meshId,
|
|
3514
|
-
nodeId,
|
|
3515
|
-
sessionCleanupMode: 'preserve',
|
|
3516
|
-
inlineMesh: args?.inlineMesh,
|
|
3517
|
-
});
|
|
3518
|
-
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
3519
|
-
removed: removeResult?.removed,
|
|
3520
|
-
code: removeResult?.code,
|
|
3521
|
-
error: removeResult?.error,
|
|
3522
|
-
});
|
|
3523
|
-
|
|
3524
|
-
let ledgerError: string | undefined;
|
|
3525
|
-
const ledgerStarted = Date.now();
|
|
3526
|
-
try {
|
|
3527
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3528
|
-
appendLedgerEntry(meshId, {
|
|
3529
|
-
kind: 'node_removed',
|
|
3530
|
-
nodeId,
|
|
3531
|
-
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence },
|
|
3532
|
-
});
|
|
3533
|
-
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
3534
|
-
} catch (e: any) {
|
|
3535
|
-
ledgerError = e?.message || String(e);
|
|
3536
|
-
recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
|
|
3537
|
-
}
|
|
3538
|
-
|
|
3539
|
-
const finalBranchConvergenceState = {
|
|
3540
|
-
branch: baseBranch,
|
|
3541
|
-
mergedBranch: branch,
|
|
3542
|
-
baseBranch,
|
|
3543
|
-
merged: true,
|
|
3544
|
-
removed: removeResult?.success !== false,
|
|
3545
|
-
validation: 'passed',
|
|
3546
|
-
patchEquivalence: 'passed',
|
|
3547
|
-
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
3548
|
-
};
|
|
3549
|
-
|
|
3550
|
-
if (removeResult?.success === false) {
|
|
3551
|
-
return {
|
|
3552
|
-
success: false,
|
|
3553
|
-
code: 'cleanup_failed',
|
|
3554
|
-
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
3555
|
-
merged: true,
|
|
3556
|
-
branch,
|
|
3557
|
-
into: baseBranch,
|
|
3558
|
-
removeResult,
|
|
3559
|
-
validationSummary,
|
|
3560
|
-
patchEquivalence,
|
|
3561
|
-
mergeResult,
|
|
3562
|
-
refineStages,
|
|
3563
|
-
...(ledgerError ? { ledgerError } : {}),
|
|
3564
|
-
finalBranchConvergenceState,
|
|
3565
|
-
};
|
|
3566
|
-
}
|
|
3567
|
-
|
|
3568
|
-
return {
|
|
3569
|
-
success: true,
|
|
3570
|
-
merged: true,
|
|
3571
|
-
branch,
|
|
3572
|
-
into: baseBranch,
|
|
3573
|
-
removeResult,
|
|
3574
|
-
validationSummary,
|
|
3575
|
-
patchEquivalence,
|
|
3576
|
-
mergeResult,
|
|
3577
|
-
refineStages,
|
|
3578
|
-
...(ledgerError ? { ledgerError } : {}),
|
|
3579
|
-
finalBranchConvergenceState,
|
|
3580
|
-
};
|
|
3581
|
-
} catch (e: any) {
|
|
3582
|
-
return { success: false, error: e.message, refineStages };
|
|
3583
|
-
}
|
|
3765
|
+
return this.startMeshRefineJob(meshId, nodeId, args);
|
|
3584
3766
|
}
|
|
3585
3767
|
|
|
3586
3768
|
case 'remove_mesh_node': {
|