@adhdev/daemon-core 0.9.82-rc.365 → 0.9.82-rc.366
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/high-family/index.d.ts +3 -0
- package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
- package/dist/commands/high-family/mesh-events.d.ts +2 -0
- package/dist/commands/high-family/mesh-status.d.ts +2 -0
- package/dist/commands/high-family/types.d.ts +60 -0
- package/dist/commands/router.d.ts +208 -0
- package/dist/index.js +1937 -1755
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1922 -1740
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-coordinator.d.ts +8 -0
- package/package.json +2 -2
- package/src/commands/cli-manager.ts +28 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +592 -0
- package/src/commands/high-family/mesh-events.ts +47 -0
- package/src/commands/high-family/mesh-status.ts +639 -0
- package/src/commands/high-family/types.ts +76 -0
- package/src/commands/router.ts +272 -1246
- package/src/mesh/mesh-events-coordinator.ts +35 -1
package/src/commands/router.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { lowFamilyRegistry } from './low-family/index.js';
|
|
|
16
16
|
import { medFamilyRegistry } from './med-family/index.js';
|
|
17
17
|
import { launchIde } from './med-family/ide.js';
|
|
18
18
|
import type { MedFamilyContext } from './med-family/index.js';
|
|
19
|
+
import { highFamilyRegistry } from './high-family/index.js';
|
|
20
|
+
import type { HighFamilyContext } from './high-family/index.js';
|
|
19
21
|
import { DaemonCliManager } from './cli-manager.js';
|
|
20
22
|
import { supportsExplicitSessionResume } from './cli-manager.js';
|
|
21
23
|
import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
|
|
@@ -52,17 +54,12 @@ import { logCommand } from '../logging/command-log.js';
|
|
|
52
54
|
import type { CommandLogEntry } from '../logging/command-log.js';
|
|
53
55
|
import * as yaml from 'js-yaml';
|
|
54
56
|
import { createInteractionId, recordDebugTrace } from '../logging/debug-trace.js';
|
|
55
|
-
import { getSessionHostSurfaceKind
|
|
56
|
-
import {
|
|
57
|
-
import { registerMeshCoordinator } from '../mesh/coordinator-registry.js';
|
|
58
|
-
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, type PendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
59
|
-
import { getRecentUnroutableDeliveries } from '../mesh/mesh-routing.js';
|
|
57
|
+
import { getSessionHostSurfaceKind } from '../session-host/runtime-surface.js';
|
|
58
|
+
import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
60
59
|
import { buildMeshWorkerRelayStamp } from '../mesh/mesh-events-utils.js';
|
|
61
|
-
import { buildMeshHostRequiredFailure,
|
|
60
|
+
import { buildMeshHostRequiredFailure, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
62
61
|
import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
|
|
63
62
|
import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../mesh/mesh-refine-batch.js';
|
|
64
|
-
import { buildPreviewFreshness } from '../mesh/preview-freshness.js';
|
|
65
|
-
import { buildMeshAsyncRefineJobs } from '../mesh/mesh-refine-status.js';
|
|
66
63
|
import {
|
|
67
64
|
MESH_REFINE_CONFIG_LOCATIONS,
|
|
68
65
|
MESH_REFINE_CONFIG_SCHEMA,
|
|
@@ -88,11 +85,10 @@ import { homedir, hostname as osHostname } from 'os';
|
|
|
88
85
|
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
89
86
|
import * as fs from 'fs';
|
|
90
87
|
import { execFileSync } from 'node:child_process';
|
|
91
|
-
import { normalizeInteractivePromptResponse } from '../providers/types/interactive-prompt.js';
|
|
92
88
|
import { workingDirBasename } from '../providers/working-dir.js';
|
|
93
89
|
import { resolveWin32Executable } from '../cli-adapters/resolve-executable.js';
|
|
94
90
|
|
|
95
|
-
function readProviderPriorityFromPolicy(policy: unknown): string[] {
|
|
91
|
+
export function readProviderPriorityFromPolicy(policy: unknown): string[] {
|
|
96
92
|
const record = policy && typeof policy === 'object' && !Array.isArray(policy)
|
|
97
93
|
? policy as Record<string, unknown>
|
|
98
94
|
: {};
|
|
@@ -153,7 +149,7 @@ function readNumberValue(...values: unknown[]): number | undefined {
|
|
|
153
149
|
return undefined;
|
|
154
150
|
}
|
|
155
151
|
|
|
156
|
-
function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
152
|
+
export function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
157
153
|
for (const value of values) {
|
|
158
154
|
if (typeof value === 'boolean') return value;
|
|
159
155
|
}
|
|
@@ -163,7 +159,7 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
|
163
159
|
// summarizeRepoMeshDebugGit was a hand-synced copy of the cloud git-shape
|
|
164
160
|
// summarizer; both now call shared summarizeGitShape (@adhdev/mesh-shared).
|
|
165
161
|
|
|
166
|
-
function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
162
|
+
export function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
167
163
|
const nodes = Array.isArray(status?.nodes) ? status.nodes : [];
|
|
168
164
|
return {
|
|
169
165
|
success: status?.success,
|
|
@@ -195,7 +191,7 @@ function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
|
195
191
|
};
|
|
196
192
|
}
|
|
197
193
|
|
|
198
|
-
function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
|
|
194
|
+
export function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
|
|
199
195
|
try {
|
|
200
196
|
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${JSON.stringify({ event, ...fields })}`);
|
|
201
197
|
} catch {
|
|
@@ -257,7 +253,7 @@ function readMeshNodeDaemonId(node: Record<string, unknown>): string | undefined
|
|
|
257
253
|
);
|
|
258
254
|
}
|
|
259
255
|
|
|
260
|
-
function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
|
|
256
|
+
export function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
|
|
261
257
|
return readStringValue(
|
|
262
258
|
node.hostname,
|
|
263
259
|
node.host,
|
|
@@ -297,7 +293,7 @@ function compactMeshIdentityEvidence(value: string | undefined): string | undefi
|
|
|
297
293
|
return value.length > 24 ? `${value.slice(0, 12)}…${value.slice(-8)}` : value;
|
|
298
294
|
}
|
|
299
295
|
|
|
300
|
-
function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
|
|
296
|
+
export function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
|
|
301
297
|
localMachineId?: string;
|
|
302
298
|
localDaemonId?: string;
|
|
303
299
|
coordinatorHostname?: string;
|
|
@@ -357,7 +353,7 @@ function normalizeInlineMeshGitStatus(
|
|
|
357
353
|
return sharedNormalizeGitStatus(status, readObjectRecord(node), options) as Record<string, unknown> | undefined;
|
|
358
354
|
}
|
|
359
355
|
|
|
360
|
-
function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
356
|
+
export function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
361
357
|
return sharedPickBestTransitGitStatus(readObjectRecord(node), { lastCheckedAt: Date.now() }) as Record<string, unknown> | undefined;
|
|
362
358
|
}
|
|
363
359
|
|
|
@@ -370,7 +366,7 @@ function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDi
|
|
|
370
366
|
});
|
|
371
367
|
}
|
|
372
368
|
|
|
373
|
-
function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
369
|
+
export function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
374
370
|
const source = readStringValue(connection.source);
|
|
375
371
|
const transport = readStringValue(connection.transport);
|
|
376
372
|
return {
|
|
@@ -385,7 +381,7 @@ function buildLivePeerGitConnection(connection: Record<string, unknown>, timesta
|
|
|
385
381
|
};
|
|
386
382
|
}
|
|
387
383
|
|
|
388
|
-
function recordInlineMeshDirectGitTruth(
|
|
384
|
+
export function recordInlineMeshDirectGitTruth(
|
|
389
385
|
node: any,
|
|
390
386
|
git: Record<string, unknown>,
|
|
391
387
|
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
@@ -462,7 +458,7 @@ function stampNodeReporterPlatform(node: any, platform: string | null, arch: str
|
|
|
462
458
|
* Fire-and-forget (same pattern as the worktreeBootstrap writer) — a persistence
|
|
463
459
|
* failure must never block the status response.
|
|
464
460
|
*/
|
|
465
|
-
function persistNodeReporterPlatform(
|
|
461
|
+
export function persistNodeReporterPlatform(
|
|
466
462
|
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config',
|
|
467
463
|
mesh: any,
|
|
468
464
|
nodeId: string | undefined,
|
|
@@ -736,7 +732,7 @@ function isInlineMeshAutoFastForwardEligible(git: Record<string, unknown> | null
|
|
|
736
732
|
return dirty !== true && countGitWorktreeChanges(git) === 0;
|
|
737
733
|
}
|
|
738
734
|
|
|
739
|
-
function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
|
|
735
|
+
export function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
|
|
740
736
|
if (!git || readBooleanValue(git.isGitRepo) === false) return 'degraded';
|
|
741
737
|
const branch = readStringValue(git.branch);
|
|
742
738
|
if (!branch) return 'degraded';
|
|
@@ -875,7 +871,7 @@ function buildInlineMeshBranchConvergence(args: {
|
|
|
875
871
|
};
|
|
876
872
|
}
|
|
877
873
|
|
|
878
|
-
function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
|
|
874
|
+
export function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
|
|
879
875
|
const git = readObjectRecord(status.git);
|
|
880
876
|
if (Object.keys(git).length === 0 && !status.gitProbePending) return;
|
|
881
877
|
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
@@ -890,7 +886,7 @@ function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<s
|
|
|
890
886
|
}
|
|
891
887
|
}
|
|
892
888
|
|
|
893
|
-
function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
|
|
889
|
+
export function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
|
|
894
890
|
const followUps = nodes
|
|
895
891
|
.filter(node => {
|
|
896
892
|
if (readObjectRecord(node.branchConvergence).needsConvergence !== true) return false;
|
|
@@ -1076,7 +1072,7 @@ function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknow
|
|
|
1076
1072
|
}
|
|
1077
1073
|
}
|
|
1078
1074
|
|
|
1079
|
-
function finalizeMeshNodeStatus(args: {
|
|
1075
|
+
export function finalizeMeshNodeStatus(args: {
|
|
1080
1076
|
status: Record<string, unknown>;
|
|
1081
1077
|
node: any;
|
|
1082
1078
|
daemonId?: string;
|
|
@@ -1132,8 +1128,8 @@ function readMeshTimeoutEnvMs(name: string, defaultMs: number): number {
|
|
|
1132
1128
|
// round-trip to slow (often TURN-relayed) peers, so such a node was permanently
|
|
1133
1129
|
// marked unavailable and blocked the whole mesh graph. Default raised to 25s
|
|
1134
1130
|
// (still under the P2P REQUEST_TIMEOUT of 30s) and made env-overridable.
|
|
1135
|
-
const MESH_DIRECT_PROBE_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_TIMEOUT_MS', 25_000);
|
|
1136
|
-
const MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS', 25_000);
|
|
1131
|
+
export const MESH_DIRECT_PROBE_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_TIMEOUT_MS', 25_000);
|
|
1132
|
+
export const MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS', 25_000);
|
|
1137
1133
|
// How long a successful per-peer git_status probe stays fresh enough to be
|
|
1138
1134
|
// reused instead of issuing another blocking `refreshUpstream:true` fan-out.
|
|
1139
1135
|
// A slow (TURN-relayed) peer's probe can take 9-23s, and the dashboard's
|
|
@@ -1266,7 +1262,7 @@ function isMeshConnectionDefinitivelyDown(
|
|
|
1266
1262
|
* attempt; a non-`connected` state short-circuits the retry loop (the very first
|
|
1267
1263
|
* attempt always runs so a missing connection getter still gets one try).
|
|
1268
1264
|
*/
|
|
1269
|
-
async function probeRemoteMeshGitStatusWithRetry(args: {
|
|
1265
|
+
export async function probeRemoteMeshGitStatusWithRetry(args: {
|
|
1270
1266
|
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
1271
1267
|
daemonId: string;
|
|
1272
1268
|
workspace: string;
|
|
@@ -1483,7 +1479,7 @@ export async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1483
1479
|
};
|
|
1484
1480
|
}
|
|
1485
1481
|
|
|
1486
|
-
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
1482
|
+
export function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
1487
1483
|
const meta = readObjectRecord(record?.meta);
|
|
1488
1484
|
const isSelfCoordinator = Boolean(readStringValue(meta.meshCoordinatorFor));
|
|
1489
1485
|
const chatStatus = readStringValue(record?.chatStatus, record?.activeChat?.status, meta.chatStatus, meta.sessionStatus);
|
|
@@ -1531,7 +1527,7 @@ function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, work
|
|
|
1531
1527
|
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
1532
1528
|
}
|
|
1533
1529
|
|
|
1534
|
-
function readLiveMeshNodeWorkspace(args: {
|
|
1530
|
+
export function readLiveMeshNodeWorkspace(args: {
|
|
1535
1531
|
meshId: string;
|
|
1536
1532
|
nodeId: string;
|
|
1537
1533
|
liveSessionRecords: any[];
|
|
@@ -1558,7 +1554,7 @@ function readLiveMeshNodeWorkspace(args: {
|
|
|
1558
1554
|
return '';
|
|
1559
1555
|
}
|
|
1560
1556
|
|
|
1561
|
-
function collectLiveMeshSessionRecords(args: {
|
|
1557
|
+
export function collectLiveMeshSessionRecords(args: {
|
|
1562
1558
|
meshId: string;
|
|
1563
1559
|
node: any;
|
|
1564
1560
|
nodeId: string;
|
|
@@ -1589,7 +1585,7 @@ function collectLiveMeshSessionRecords(args: {
|
|
|
1589
1585
|
return matches;
|
|
1590
1586
|
}
|
|
1591
1587
|
|
|
1592
|
-
function buildHistoricalMeshSessions(args: {
|
|
1588
|
+
export function buildHistoricalMeshSessions(args: {
|
|
1593
1589
|
meshId: string;
|
|
1594
1590
|
nodes: any[];
|
|
1595
1591
|
liveSessionRecords: any[];
|
|
@@ -1635,7 +1631,7 @@ function buildHistoricalMeshSessions(args: {
|
|
|
1635
1631
|
};
|
|
1636
1632
|
}
|
|
1637
1633
|
|
|
1638
|
-
function applyCachedInlineMeshNodeStatus(
|
|
1634
|
+
export function applyCachedInlineMeshNodeStatus(
|
|
1639
1635
|
status: Record<string, unknown>,
|
|
1640
1636
|
node: any,
|
|
1641
1637
|
options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
|
|
@@ -1669,7 +1665,7 @@ function applyCachedInlineMeshNodeStatus(
|
|
|
1669
1665
|
return activeSessions.length > 0 || !!machineStatus || !!lastSeenAt || !!updatedAt;
|
|
1670
1666
|
}
|
|
1671
1667
|
|
|
1672
|
-
async function resolveProviderTypeFromPriority(args: {
|
|
1668
|
+
export async function resolveProviderTypeFromPriority(args: {
|
|
1673
1669
|
nodeId: string;
|
|
1674
1670
|
providerPriority: string[];
|
|
1675
1671
|
providerLoader: ProviderLoader;
|
|
@@ -1699,7 +1695,7 @@ async function resolveProviderTypeFromPriority(args: {
|
|
|
1699
1695
|
|
|
1700
1696
|
return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
|
|
1701
1697
|
}
|
|
1702
|
-
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
1698
|
+
export type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
1703
1699
|
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
1704
1700
|
type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
|
|
1705
1701
|
|
|
@@ -1849,7 +1845,7 @@ type MeshRefineSubmoduleReachabilitySummary = {
|
|
|
1849
1845
|
|
|
1850
1846
|
type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
|
|
1851
1847
|
|
|
1852
|
-
type MeshRefineJobHandle = {
|
|
1848
|
+
export type MeshRefineJobHandle = {
|
|
1853
1849
|
success: true;
|
|
1854
1850
|
async: true;
|
|
1855
1851
|
status: MeshRefineAsyncJobStatus;
|
|
@@ -1982,6 +1978,51 @@ function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReach
|
|
|
1982
1978
|
return `Ask the user for explicit approval to push/publish the unreachable submodule commit(s) (${refs}) to the configured submodule remote main branch, then rerun mesh_refine_node. Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.`;
|
|
1983
1979
|
}
|
|
1984
1980
|
|
|
1981
|
+
/**
|
|
1982
|
+
* Async git exec helper used across the synchronous-refine stage pipeline. Bound
|
|
1983
|
+
* once in the orchestrator and threaded through RefineContext so every stage runs
|
|
1984
|
+
* git the same way (execFile + promisify, utf8). Returns the child's stdout/stderr.
|
|
1985
|
+
*/
|
|
1986
|
+
type RefineExecFileAsync = (file: string, args: string[], options: { cwd: string; encoding: 'utf8' }) => Promise<{ stdout: string; stderr: string }>;
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* Accumulated state shared by the synchronous-refine stages. The orchestrator
|
|
1990
|
+
* (executeMeshRefineNodeSynchronously) seeds this in the resolve_refs stage and
|
|
1991
|
+
* each later stage reads / extends it. `branchHead` and `patchEquivalence` are the
|
|
1992
|
+
* only fields a stage mutates after creation (auto-rebase updates both), so they
|
|
1993
|
+
* are carried on the mutable context rather than re-threaded through return types.
|
|
1994
|
+
*/
|
|
1995
|
+
interface RefineContext {
|
|
1996
|
+
meshId: string;
|
|
1997
|
+
nodeId: string;
|
|
1998
|
+
args: any;
|
|
1999
|
+
refineStages: Array<Record<string, unknown>>;
|
|
2000
|
+
execFileAsync: RefineExecFileAsync;
|
|
2001
|
+
mesh: any;
|
|
2002
|
+
node: any;
|
|
2003
|
+
sourceNode: any;
|
|
2004
|
+
repoRoot: string;
|
|
2005
|
+
branch: string;
|
|
2006
|
+
baseBranch: string;
|
|
2007
|
+
baseHead: string;
|
|
2008
|
+
branchHead: string;
|
|
2009
|
+
validationSummary: Awaited<ReturnType<typeof runMeshRefineValidationGate>>;
|
|
2010
|
+
patchEquivalence: Awaited<ReturnType<typeof runMeshRefinePatchEquivalenceGate>>;
|
|
2011
|
+
submoduleReachability: Awaited<ReturnType<typeof runMeshRefineSubmoduleReachabilityGate>>;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* Stage outcome for the synchronous-refine pipeline. A stage either produces a
|
|
2016
|
+
* terminal CommandRouterResult (an early-exit gate failure, or a successful
|
|
2017
|
+
* already-merged short-circuit), in which case the orchestrator returns it
|
|
2018
|
+
* immediately, or it returns `continue` with the (possibly extended) context for
|
|
2019
|
+
* the next stage. This makes the orchestrator a flat sequence of stage calls
|
|
2020
|
+
* while preserving the original body's exact early-return control flow.
|
|
2021
|
+
*/
|
|
2022
|
+
type RefineStageOutcome =
|
|
2023
|
+
| { kind: 'terminal'; result: CommandRouterResult }
|
|
2024
|
+
| { kind: 'continue'; ctx: RefineContext };
|
|
2025
|
+
|
|
1985
2026
|
function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
|
|
1986
2027
|
if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1987
2028
|
process.stderr.write(
|
|
@@ -3285,18 +3326,18 @@ function loadYamlModule(): { load: (input: string) => any; dump: (input: any, op
|
|
|
3285
3326
|
return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
|
|
3286
3327
|
}
|
|
3287
3328
|
|
|
3288
|
-
function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
|
|
3329
|
+
export function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
|
|
3289
3330
|
return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
|
|
3290
3331
|
}
|
|
3291
3332
|
|
|
3292
|
-
function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
|
|
3333
|
+
export function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
|
|
3293
3334
|
if (!text.trim()) return {};
|
|
3294
3335
|
if (format === 'claude_mcp_json') return JSON.parse(text);
|
|
3295
3336
|
const parsed = loadYamlModule().load(text);
|
|
3296
3337
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
3297
3338
|
}
|
|
3298
3339
|
|
|
3299
|
-
function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
|
|
3340
|
+
export function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
|
|
3300
3341
|
if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
|
|
3301
3342
|
return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
|
|
3302
3343
|
}
|
|
@@ -3306,7 +3347,7 @@ function resolveHermesUserHome(): string {
|
|
|
3306
3347
|
return explicitHome || pathJoin(homedir(), '.hermes');
|
|
3307
3348
|
}
|
|
3308
3349
|
|
|
3309
|
-
function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
|
|
3350
|
+
export function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
|
|
3310
3351
|
const sourceHome = resolveHermesUserHome();
|
|
3311
3352
|
const sourceConfigPath = pathJoin(sourceHome, 'config.yaml');
|
|
3312
3353
|
if (!fs.existsSync(sourceConfigPath)) return { config: {}, sourceHome, sourceConfigPath };
|
|
@@ -3317,7 +3358,7 @@ function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Re
|
|
|
3317
3358
|
return { config: baseConfig, sourceHome, sourceConfigPath };
|
|
3318
3359
|
}
|
|
3319
3360
|
|
|
3320
|
-
function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
|
|
3361
|
+
export function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
|
|
3321
3362
|
const {
|
|
3322
3363
|
model: _model,
|
|
3323
3364
|
provider: _provider,
|
|
@@ -3346,7 +3387,7 @@ function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string,
|
|
|
3346
3387
|
return sanitized;
|
|
3347
3388
|
}
|
|
3348
3389
|
|
|
3349
|
-
function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
|
|
3390
|
+
export function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
|
|
3350
3391
|
if (pathResolve(sourceHome) === pathResolve(targetHome)) return;
|
|
3351
3392
|
for (const fileName of ['.env', 'auth.json']) {
|
|
3352
3393
|
const sourcePath = pathJoin(sourceHome, fileName);
|
|
@@ -3897,6 +3938,30 @@ export class DaemonCommandRouter {
|
|
|
3897
3938
|
return ctx;
|
|
3898
3939
|
}
|
|
3899
3940
|
|
|
3941
|
+
/**
|
|
3942
|
+
* Build the HighFamilyContext handed to RF-ROUTER HIGH family handlers. Binds
|
|
3943
|
+
* the router-private collaborators those handlers need (mesh resolution, the
|
|
3944
|
+
* aggregate-status memory cache + its bound read/write helpers, the
|
|
3945
|
+
* running-refine-job table, inline-mesh + git-probe caches, and the router's
|
|
3946
|
+
* own `execute` for the get_mesh_review_inbox mesh_status re-entry). HIGH
|
|
3947
|
+
* handlers reach more router-owned state than MED, but the binding shape is
|
|
3948
|
+
* the same: bound methods + direct field references, none reachable from
|
|
3949
|
+
* `deps`.
|
|
3950
|
+
*/
|
|
3951
|
+
private buildHighFamilyContext(): HighFamilyContext {
|
|
3952
|
+
return {
|
|
3953
|
+
deps: this.deps,
|
|
3954
|
+
getMeshForCommand: this.getMeshForCommand.bind(this),
|
|
3955
|
+
getCachedAggregateMeshStatus: this.getCachedAggregateMeshStatus.bind(this),
|
|
3956
|
+
rememberAggregateMeshStatus: this.rememberAggregateMeshStatus.bind(this),
|
|
3957
|
+
execute: this.execute.bind(this),
|
|
3958
|
+
aggregateMeshStatusCache: this.aggregateMeshStatusCache,
|
|
3959
|
+
runningRefineJobs: this.runningRefineJobs,
|
|
3960
|
+
inlineMeshCache: this.inlineMeshCache,
|
|
3961
|
+
meshGitProbeCache: this.meshGitProbeCache,
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3900
3965
|
|
|
3901
3966
|
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
3902
3967
|
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
@@ -4801,33 +4866,77 @@ export class DaemonCommandRouter {
|
|
|
4801
4866
|
}
|
|
4802
4867
|
}
|
|
4803
4868
|
|
|
4869
|
+
/**
|
|
4870
|
+
* Synchronous refinery for a single worktree node — the gate pipeline that
|
|
4871
|
+
* validates, preflights (patch-equivalence / submodule-reachability /
|
|
4872
|
+
* no-op), merges, aligns submodules, cleans up the worktree node and
|
|
4873
|
+
* (optionally) pushes. The body is a flat sequence of stage methods; each
|
|
4874
|
+
* stage either returns a terminal CommandRouterResult (gate failure or a
|
|
4875
|
+
* successful already-merged short-circuit) or `continue` with the extended
|
|
4876
|
+
* context. Behavior — stage order, every early-exit, and every result shape —
|
|
4877
|
+
* is identical to the previous single inlined body.
|
|
4878
|
+
*/
|
|
4804
4879
|
private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
4805
4880
|
const refineStages: Array<Record<string, unknown>> = [];
|
|
4806
4881
|
try {
|
|
4882
|
+
const resolved = await this.refineResolveRefsStage(meshId, nodeId, args, refineStages);
|
|
4883
|
+
if (resolved.kind === 'terminal') return resolved.result;
|
|
4884
|
+
const ctx = resolved.ctx;
|
|
4885
|
+
|
|
4886
|
+
const validation = await this.refineValidationStage(ctx);
|
|
4887
|
+
if (validation.kind === 'terminal') return validation.result;
|
|
4888
|
+
|
|
4889
|
+
const patchEquivalence = await this.refinePatchEquivalenceStage(ctx);
|
|
4890
|
+
if (patchEquivalence.kind === 'terminal') return patchEquivalence.result;
|
|
4891
|
+
|
|
4892
|
+
const submoduleReachability = await this.refineSubmoduleReachabilityStage(ctx);
|
|
4893
|
+
if (submoduleReachability.kind === 'terminal') return submoduleReachability.result;
|
|
4894
|
+
|
|
4895
|
+
const effectiveDiff = await this.refineEffectiveDiffStage(ctx);
|
|
4896
|
+
if (effectiveDiff.kind === 'terminal') return effectiveDiff.result;
|
|
4897
|
+
|
|
4898
|
+
const merge = await this.refineMergeAndFinalizeStage(ctx);
|
|
4899
|
+
return (merge as { kind: 'terminal'; result: CommandRouterResult }).result;
|
|
4900
|
+
} catch (e: any) {
|
|
4901
|
+
return { success: false, error: e.message, refineStages };
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
/**
|
|
4906
|
+
* resolve_refs stage: resolve the mesh / worktree node / source node /
|
|
4907
|
+
* repoRoot, then the worktree branch, base branch, fetched base head and
|
|
4908
|
+
* branch head. Seeds the RefineContext consumed by every later stage.
|
|
4909
|
+
*/
|
|
4910
|
+
private async refineResolveRefsStage(
|
|
4911
|
+
meshId: string,
|
|
4912
|
+
nodeId: string,
|
|
4913
|
+
args: any,
|
|
4914
|
+
refineStages: Array<Record<string, unknown>>,
|
|
4915
|
+
): Promise<RefineStageOutcome> {
|
|
4807
4916
|
// preferInline: same as startMeshRefineJob — inline-cache-only clone nodes must resolve.
|
|
4808
4917
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
4809
4918
|
const mesh = meshRecord?.mesh;
|
|
4810
4919
|
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
4811
|
-
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
4920
|
+
if (!node) return { kind: 'terminal', result: { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages } };
|
|
4812
4921
|
|
|
4813
4922
|
if (!node.isLocalWorktree || !node.workspace) {
|
|
4814
|
-
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
4923
|
+
return { kind: 'terminal', result: { success: false, error: `Refinery requires a local worktree node`, refineStages } };
|
|
4815
4924
|
}
|
|
4816
4925
|
|
|
4817
4926
|
const sourceNode = node.clonedFromNodeId
|
|
4818
4927
|
? mesh?.nodes.find((n: any) => meshNodeIdMatches(n, node.clonedFromNodeId))
|
|
4819
4928
|
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
4820
4929
|
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
4821
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
4930
|
+
if (!repoRoot) return { kind: 'terminal', result: { success: false, error: 'Source node repoRoot not found', refineStages } };
|
|
4822
4931
|
|
|
4823
4932
|
const { execFile } = await import('node:child_process');
|
|
4824
4933
|
const { promisify } = await import('node:util');
|
|
4825
|
-
const execFileAsync = promisify(execFile);
|
|
4934
|
+
const execFileAsync = promisify(execFile) as unknown as RefineExecFileAsync;
|
|
4826
4935
|
|
|
4827
4936
|
const resolveStarted = Date.now();
|
|
4828
4937
|
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
4829
4938
|
const branch = branchStdout.trim();
|
|
4830
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
4939
|
+
if (!branch) return { kind: 'terminal', result: { success: false, error: 'Could not determine branch of the worktree node', refineStages } };
|
|
4831
4940
|
|
|
4832
4941
|
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
4833
4942
|
const baseBranch = baseBranchStdout.trim();
|
|
@@ -4854,9 +4963,39 @@ export class DaemonCommandRouter {
|
|
|
4854
4963
|
|
|
4855
4964
|
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
4856
4965
|
const baseHead = baseHeadRaw;
|
|
4857
|
-
|
|
4966
|
+
const branchHead = branchHeadStdout.trim();
|
|
4858
4967
|
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead, ...(fetchWarning ? { fetchWarning } : {}) });
|
|
4859
4968
|
|
|
4969
|
+
return {
|
|
4970
|
+
kind: 'continue',
|
|
4971
|
+
ctx: {
|
|
4972
|
+
meshId,
|
|
4973
|
+
nodeId,
|
|
4974
|
+
args,
|
|
4975
|
+
refineStages,
|
|
4976
|
+
execFileAsync,
|
|
4977
|
+
mesh,
|
|
4978
|
+
node,
|
|
4979
|
+
sourceNode,
|
|
4980
|
+
repoRoot,
|
|
4981
|
+
branch,
|
|
4982
|
+
baseBranch,
|
|
4983
|
+
baseHead,
|
|
4984
|
+
branchHead,
|
|
4985
|
+
validationSummary: undefined as any,
|
|
4986
|
+
patchEquivalence: undefined as any,
|
|
4987
|
+
submoduleReachability: undefined as any,
|
|
4988
|
+
},
|
|
4989
|
+
};
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4992
|
+
/**
|
|
4993
|
+
* validation stage: run the refinery validation gate (typecheck / test /
|
|
4994
|
+
* lint / build per node config) and block on failure or when no allowlisted
|
|
4995
|
+
* command was available. On pass, stores the summary on the context.
|
|
4996
|
+
*/
|
|
4997
|
+
private async refineValidationStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
4998
|
+
const { mesh, node, branch, baseBranch, refineStages } = ctx;
|
|
4860
4999
|
const validationStarted = Date.now();
|
|
4861
5000
|
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace, {
|
|
4862
5001
|
// M2-2: consume the node's persisted bootstrap state; persist re-runs.
|
|
@@ -4868,6 +5007,7 @@ export class DaemonCommandRouter {
|
|
|
4868
5007
|
.catch(() => { /* persistence is best-effort */ });
|
|
4869
5008
|
},
|
|
4870
5009
|
});
|
|
5010
|
+
ctx.validationSummary = validationSummary;
|
|
4871
5011
|
recordMeshRefineStage(
|
|
4872
5012
|
refineStages,
|
|
4873
5013
|
'validation',
|
|
@@ -4903,7 +5043,7 @@ export class DaemonCommandRouter {
|
|
|
4903
5043
|
tail ? `Output (tail):\n${tail}` : '',
|
|
4904
5044
|
].filter(Boolean).join('\n');
|
|
4905
5045
|
};
|
|
4906
|
-
return {
|
|
5046
|
+
return { kind: 'terminal', result: {
|
|
4907
5047
|
success: false,
|
|
4908
5048
|
code: validationSummary.failureCode || 'validation_failed',
|
|
4909
5049
|
convergenceStatus: 'blocked_review',
|
|
@@ -4920,10 +5060,10 @@ export class DaemonCommandRouter {
|
|
|
4920
5060
|
validation: 'failed',
|
|
4921
5061
|
status: 'blocked_review',
|
|
4922
5062
|
},
|
|
4923
|
-
};
|
|
5063
|
+
} };
|
|
4924
5064
|
}
|
|
4925
5065
|
if (validationSummary.status === 'skipped') {
|
|
4926
|
-
return {
|
|
5066
|
+
return { kind: 'terminal', result: {
|
|
4927
5067
|
success: false,
|
|
4928
5068
|
code: 'validation_unavailable',
|
|
4929
5069
|
convergenceStatus: 'blocked_review',
|
|
@@ -4940,9 +5080,22 @@ export class DaemonCommandRouter {
|
|
|
4940
5080
|
validation: 'unavailable',
|
|
4941
5081
|
status: 'blocked_review',
|
|
4942
5082
|
},
|
|
4943
|
-
};
|
|
5083
|
+
} };
|
|
4944
5084
|
}
|
|
4945
5085
|
|
|
5086
|
+
return { kind: 'continue', ctx };
|
|
5087
|
+
}
|
|
5088
|
+
|
|
5089
|
+
/**
|
|
5090
|
+
* patch_equivalence stage: preflight that the worktree branch's cumulative
|
|
5091
|
+
* patch is equivalent to base+branch. On a "behind base" branch, auto-rebase
|
|
5092
|
+
* once and re-check; on an empty merge-tree with real branch changes, treat as
|
|
5093
|
+
* already-merged-via-another-path and short-circuit to cleanup. Mutates the
|
|
5094
|
+
* context's branchHead (after rebase) and patchEquivalence (rebased gate).
|
|
5095
|
+
*/
|
|
5096
|
+
private async refinePatchEquivalenceStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
5097
|
+
const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, validationSummary, refineStages, execFileAsync } = ctx;
|
|
5098
|
+
let branchHead = ctx.branchHead;
|
|
4946
5099
|
const patchEquivalenceStarted = Date.now();
|
|
4947
5100
|
let patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
4948
5101
|
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
@@ -4985,7 +5138,7 @@ export class DaemonCommandRouter {
|
|
|
4985
5138
|
patchEquivalence = rebasedPatchEquivalence;
|
|
4986
5139
|
didAutoRebase = true;
|
|
4987
5140
|
} else {
|
|
4988
|
-
return {
|
|
5141
|
+
return { kind: 'terminal', result: {
|
|
4989
5142
|
success: false,
|
|
4990
5143
|
code: 'needs_rebase',
|
|
4991
5144
|
convergenceStatus: 'blocked_review',
|
|
@@ -5004,14 +5157,14 @@ export class DaemonCommandRouter {
|
|
|
5004
5157
|
patchEquivalence: 'failed',
|
|
5005
5158
|
status: 'blocked_review',
|
|
5006
5159
|
},
|
|
5007
|
-
};
|
|
5160
|
+
} };
|
|
5008
5161
|
}
|
|
5009
5162
|
} catch (rebaseErr: any) {
|
|
5010
5163
|
try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
|
|
5011
5164
|
recordMeshRefineStage(refineStages, 'patch_equivalence_after_auto_rebase', 'failed', autoRebaseStarted, {
|
|
5012
5165
|
error: rebaseErr?.message || String(rebaseErr),
|
|
5013
5166
|
});
|
|
5014
|
-
return {
|
|
5167
|
+
return { kind: 'terminal', result: {
|
|
5015
5168
|
success: false,
|
|
5016
5169
|
code: 'needs_rebase_with_conflicts',
|
|
5017
5170
|
convergenceStatus: 'blocked_review',
|
|
@@ -5030,7 +5183,7 @@ export class DaemonCommandRouter {
|
|
|
5030
5183
|
patchEquivalence: 'failed',
|
|
5031
5184
|
status: 'blocked_review',
|
|
5032
5185
|
},
|
|
5033
|
-
};
|
|
5186
|
+
} };
|
|
5034
5187
|
}
|
|
5035
5188
|
}
|
|
5036
5189
|
|
|
@@ -5046,7 +5199,7 @@ export class DaemonCommandRouter {
|
|
|
5046
5199
|
// is a degenerate worktree case, not an "already merged" scenario.
|
|
5047
5200
|
const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
|
|
5048
5201
|
if (!didAutoRebase && !alreadyMergedViaOtherPath) {
|
|
5049
|
-
return {
|
|
5202
|
+
return { kind: 'terminal', result: {
|
|
5050
5203
|
success: false,
|
|
5051
5204
|
code: 'patch_equivalence_failed',
|
|
5052
5205
|
convergenceStatus: 'blocked_review',
|
|
@@ -5065,7 +5218,7 @@ export class DaemonCommandRouter {
|
|
|
5065
5218
|
patchEquivalence: 'failed',
|
|
5066
5219
|
status: 'blocked_review',
|
|
5067
5220
|
},
|
|
5068
|
-
};
|
|
5221
|
+
} };
|
|
5069
5222
|
}
|
|
5070
5223
|
|
|
5071
5224
|
if (!didAutoRebase && alreadyMergedViaOtherPath) {
|
|
@@ -5094,7 +5247,7 @@ export class DaemonCommandRouter {
|
|
|
5094
5247
|
payload: { alreadyMergedViaOtherPath: true, branch, into: baseBranch, validationSummary, patchEquivalence },
|
|
5095
5248
|
});
|
|
5096
5249
|
} catch { /* ledger append is best-effort */ }
|
|
5097
|
-
return {
|
|
5250
|
+
return { kind: 'terminal', result: {
|
|
5098
5251
|
success: removeResult?.success !== false,
|
|
5099
5252
|
code: 'already_merged',
|
|
5100
5253
|
merged: false,
|
|
@@ -5116,10 +5269,23 @@ export class DaemonCommandRouter {
|
|
|
5116
5269
|
patchEquivalence: 'already_merged',
|
|
5117
5270
|
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged_to_main',
|
|
5118
5271
|
},
|
|
5119
|
-
};
|
|
5272
|
+
} };
|
|
5120
5273
|
}
|
|
5121
5274
|
}
|
|
5122
5275
|
|
|
5276
|
+
ctx.branchHead = branchHead;
|
|
5277
|
+
ctx.patchEquivalence = patchEquivalence;
|
|
5278
|
+
return { kind: 'continue', ctx };
|
|
5279
|
+
}
|
|
5280
|
+
|
|
5281
|
+
/**
|
|
5282
|
+
* submodule_reachability stage: verify every submodule gitlink commit that
|
|
5283
|
+
* would land via the merge is reachable from its configured remote main
|
|
5284
|
+
* branch (optionally auto-publishing when policy allows). Blocks the merge
|
|
5285
|
+
* when any commit is unreachable. Stores the result on the context.
|
|
5286
|
+
*/
|
|
5287
|
+
private async refineSubmoduleReachabilityStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
5288
|
+
const { mesh, node, repoRoot, branch, baseBranch, branchHead, validationSummary, patchEquivalence, refineStages } = ctx;
|
|
5123
5289
|
const submoduleReachabilityStarted = Date.now();
|
|
5124
5290
|
const autoPublishSubmoduleMainCommits = resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace);
|
|
5125
5291
|
const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
|
|
@@ -5176,7 +5342,7 @@ export class DaemonCommandRouter {
|
|
|
5176
5342
|
});
|
|
5177
5343
|
if (submoduleReachability.status === 'failed') {
|
|
5178
5344
|
const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
|
|
5179
|
-
return {
|
|
5345
|
+
return { kind: 'terminal', result: {
|
|
5180
5346
|
success: false,
|
|
5181
5347
|
code: 'submodule_reachability_failed',
|
|
5182
5348
|
convergenceStatus: 'blocked_review',
|
|
@@ -5224,9 +5390,21 @@ export class DaemonCommandRouter {
|
|
|
5224
5390
|
reason: 'submodule_publish_required',
|
|
5225
5391
|
nextStep,
|
|
5226
5392
|
},
|
|
5227
|
-
};
|
|
5393
|
+
} };
|
|
5228
5394
|
}
|
|
5229
5395
|
|
|
5396
|
+
ctx.submoduleReachability = submoduleReachability;
|
|
5397
|
+
return { kind: 'continue', ctx };
|
|
5398
|
+
}
|
|
5399
|
+
|
|
5400
|
+
/**
|
|
5401
|
+
* effective_diff stage (no-op guard): block a silent no-op merge where the
|
|
5402
|
+
* branch produces no effective root-tree diff against base — typically a
|
|
5403
|
+
* submodule that has commits but whose root-level gitlink (pointer) bump was
|
|
5404
|
+
* never committed, so the merge would land nothing real on main.
|
|
5405
|
+
*/
|
|
5406
|
+
private async refineEffectiveDiffStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
5407
|
+
const { repoRoot, baseHead, branchHead, branch, baseBranch, validationSummary, patchEquivalence, refineStages } = ctx;
|
|
5230
5408
|
// No-op guard: block a silent no-op merge where the root tree is identical to base.
|
|
5231
5409
|
// This catches the trap where a submodule has commits but the root branch never
|
|
5232
5410
|
// committed the gitlink (oss-pointer) bump — merging would report success while the
|
|
@@ -5248,7 +5426,7 @@ export class DaemonCommandRouter {
|
|
|
5248
5426
|
hintLines.length ? `Submodules with uncommitted pointer bumps:\n${hintLines.join('\n')}` : '',
|
|
5249
5427
|
`Fix: commit the submodule pointer bump on '${branch}' (git add <submodule-path> && git commit), then re-run refine.`,
|
|
5250
5428
|
].filter(Boolean).join('\n');
|
|
5251
|
-
return {
|
|
5429
|
+
return { kind: 'terminal', result: {
|
|
5252
5430
|
success: false,
|
|
5253
5431
|
code: 'no_effective_diff',
|
|
5254
5432
|
convergenceStatus: 'blocked_review',
|
|
@@ -5271,9 +5449,20 @@ export class DaemonCommandRouter {
|
|
|
5271
5449
|
reason: 'no_effective_diff',
|
|
5272
5450
|
...(effectiveDiff.submoduleHints?.length ? { submoduleHints: effectiveDiff.submoduleHints } : {}),
|
|
5273
5451
|
},
|
|
5274
|
-
};
|
|
5452
|
+
} };
|
|
5275
5453
|
}
|
|
5276
5454
|
|
|
5455
|
+
return { kind: 'continue', ctx };
|
|
5456
|
+
}
|
|
5457
|
+
|
|
5458
|
+
/**
|
|
5459
|
+
* merge + finalize stage: perform the --no-ff merge, align submodule
|
|
5460
|
+
* checkouts after merge, clean up (remove) the worktree node per policy,
|
|
5461
|
+
* append the refinery ledger entry, and (unless approval is required) push the
|
|
5462
|
+
* base branch. Always terminal — produces the final CommandRouterResult.
|
|
5463
|
+
*/
|
|
5464
|
+
private async refineMergeAndFinalizeStage(ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
5465
|
+
const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, sourceNode, validationSummary, patchEquivalence, submoduleReachability, mesh, refineStages, execFileAsync } = ctx;
|
|
5277
5466
|
let mergeResult: Record<string, unknown> | undefined;
|
|
5278
5467
|
const mergeStarted = Date.now();
|
|
5279
5468
|
try {
|
|
@@ -5290,7 +5479,7 @@ export class DaemonCommandRouter {
|
|
|
5290
5479
|
stdout: truncateValidationOutput(e?.stdout),
|
|
5291
5480
|
stderr: truncateValidationOutput(e?.stderr),
|
|
5292
5481
|
});
|
|
5293
|
-
return {
|
|
5482
|
+
return { kind: 'terminal', result: {
|
|
5294
5483
|
success: false,
|
|
5295
5484
|
error: `Merge failed (conflicts?): ${e.message}`,
|
|
5296
5485
|
validationSummary,
|
|
@@ -5305,7 +5494,7 @@ export class DaemonCommandRouter {
|
|
|
5305
5494
|
patchEquivalence: 'passed',
|
|
5306
5495
|
status: 'not_mergeable',
|
|
5307
5496
|
},
|
|
5308
|
-
};
|
|
5497
|
+
} };
|
|
5309
5498
|
}
|
|
5310
5499
|
|
|
5311
5500
|
const submoduleAlignmentStarted = Date.now();
|
|
@@ -5325,7 +5514,7 @@ export class DaemonCommandRouter {
|
|
|
5325
5514
|
});
|
|
5326
5515
|
}
|
|
5327
5516
|
if (submoduleAlignment.status === 'failed') {
|
|
5328
|
-
return {
|
|
5517
|
+
return { kind: 'terminal', result: {
|
|
5329
5518
|
success: false,
|
|
5330
5519
|
code: 'post_merge_submodule_alignment_failed',
|
|
5331
5520
|
error: 'Refinery merge completed but post-merge submodule checkout alignment failed; run the reported git submodule update command and re-check base workspace status.',
|
|
@@ -5351,7 +5540,7 @@ export class DaemonCommandRouter {
|
|
|
5351
5540
|
status: 'post_merge_alignment_failed',
|
|
5352
5541
|
nextStep: submoduleAlignment.command || 'Run git submodule update --init --recursive for the reported path(s), then re-check base workspace status.',
|
|
5353
5542
|
},
|
|
5354
|
-
};
|
|
5543
|
+
} };
|
|
5355
5544
|
}
|
|
5356
5545
|
|
|
5357
5546
|
const cleanupStarted = Date.now();
|
|
@@ -5431,7 +5620,7 @@ export class DaemonCommandRouter {
|
|
|
5431
5620
|
};
|
|
5432
5621
|
|
|
5433
5622
|
if (removeResult?.success === false) {
|
|
5434
|
-
return {
|
|
5623
|
+
return { kind: 'terminal', result: {
|
|
5435
5624
|
success: false,
|
|
5436
5625
|
code: 'cleanup_failed',
|
|
5437
5626
|
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
@@ -5447,7 +5636,7 @@ export class DaemonCommandRouter {
|
|
|
5447
5636
|
refineStages,
|
|
5448
5637
|
...(ledgerError ? { ledgerError } : {}),
|
|
5449
5638
|
finalBranchConvergenceState,
|
|
5450
|
-
};
|
|
5639
|
+
} };
|
|
5451
5640
|
}
|
|
5452
5641
|
|
|
5453
5642
|
// Push logic: after a successful merge, either auto-push or surface push info
|
|
@@ -5474,7 +5663,7 @@ export class DaemonCommandRouter {
|
|
|
5474
5663
|
}
|
|
5475
5664
|
}
|
|
5476
5665
|
|
|
5477
|
-
return {
|
|
5666
|
+
return { kind: 'terminal', result: {
|
|
5478
5667
|
success: true,
|
|
5479
5668
|
merged: true,
|
|
5480
5669
|
branch,
|
|
@@ -5496,10 +5685,7 @@ export class DaemonCommandRouter {
|
|
|
5496
5685
|
pushCommand: `git push origin ${baseBranch}`,
|
|
5497
5686
|
pushNote: 'requireApprovalForPush is enabled — run the push command or obtain user approval before pushing.',
|
|
5498
5687
|
}),
|
|
5499
|
-
};
|
|
5500
|
-
} catch (e: any) {
|
|
5501
|
-
return { success: false, error: e.message, refineStages };
|
|
5502
|
-
}
|
|
5688
|
+
} };
|
|
5503
5689
|
}
|
|
5504
5690
|
|
|
5505
5691
|
/**
|
|
@@ -6231,1179 +6417,19 @@ export class DaemonCommandRouter {
|
|
|
6231
6417
|
return await medFamilyHandler(this.buildMedFamilyContext(), args);
|
|
6232
6418
|
}
|
|
6233
6419
|
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
: undefined;
|
|
6248
|
-
const events = drainPendingMeshCoordinatorEvents(meshId || undefined, coordinatorDaemonId);
|
|
6249
|
-
return { success: true, events };
|
|
6250
|
-
}
|
|
6251
|
-
|
|
6252
|
-
case 'interactive_prompt_response': {
|
|
6253
|
-
const sessionId = typeof args?.targetSessionId === 'string' && args.targetSessionId.trim()
|
|
6254
|
-
? args.targetSessionId.trim()
|
|
6255
|
-
: typeof args?.sessionId === 'string' && args.sessionId.trim()
|
|
6256
|
-
? args.sessionId.trim()
|
|
6257
|
-
: '';
|
|
6258
|
-
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
6259
|
-
const response = normalizeInteractivePromptResponse(args?.response ?? args);
|
|
6260
|
-
const instance = this.deps.instanceManager.getInstance(sessionId);
|
|
6261
|
-
if (!instance) return { success: false, error: `No running instance for session ${sessionId}` };
|
|
6262
|
-
this.deps.instanceManager.sendEvent(sessionId, 'interactive_prompt_response', response);
|
|
6263
|
-
return { success: true };
|
|
6264
|
-
}
|
|
6265
|
-
|
|
6266
|
-
// ─── Mesh Coordinator Launch ───
|
|
6267
|
-
case 'launch_mesh_coordinator': {
|
|
6268
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6269
|
-
let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
|
|
6270
|
-
// Optional per-launch system-prompt addition. Dashboard or API
|
|
6271
|
-
// callers (e.g. when spawning a mesh-node-specific coordinator)
|
|
6272
|
-
// can pass extra context that gets appended to the rendered
|
|
6273
|
-
// default prompt under the "## Additional Context" section.
|
|
6274
|
-
// Going through buildCoordinatorSystemPrompt's userInstruction
|
|
6275
|
-
// means user-level override files (~/.adhdev/coordinator-prompts)
|
|
6276
|
-
// and this per-launch addition compose cleanly: an override
|
|
6277
|
-
// wins outright, but if there's no override, the default
|
|
6278
|
-
// prompt + the optional append.md file + this extra context
|
|
6279
|
-
// all stack in declared order.
|
|
6280
|
-
const extraSystemPrompt = typeof args?.extraSystemPrompt === 'string'
|
|
6281
|
-
? args.extraSystemPrompt.trim()
|
|
6282
|
-
: '';
|
|
6283
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6284
|
-
|
|
6285
|
-
try {
|
|
6286
|
-
const { buildCoordinatorSystemPrompt } = await import('../mesh/coordinator-prompt.js');
|
|
6287
|
-
const { buildMissionPromptSection } = await import('../mesh/mesh-missions.js');
|
|
6288
|
-
// M3-3: inject the active mission summary into the coordinator prompt.
|
|
6289
|
-
// Best-effort — a store failure must not block coordinator launch.
|
|
6290
|
-
const buildMissionSectionBestEffort = (id: string): string => {
|
|
6291
|
-
try { return buildMissionPromptSection(id); } catch { return ''; }
|
|
6292
|
-
};
|
|
6293
|
-
|
|
6294
|
-
// Support inline mesh data from cloud (bypasses local meshes.json lookup)
|
|
6295
|
-
let mesh: any;
|
|
6296
|
-
if (args?.inlineMesh && typeof args.inlineMesh === 'object') {
|
|
6297
|
-
mesh = args.inlineMesh;
|
|
6298
|
-
// Cache cloud mesh so the MCP server can retrieve it via get_mesh
|
|
6299
|
-
this.inlineMeshCache.set(meshId, mesh);
|
|
6300
|
-
} else {
|
|
6301
|
-
const { getMesh } = await import('../config/mesh-config.js');
|
|
6302
|
-
mesh = getMesh(meshId);
|
|
6303
|
-
}
|
|
6304
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
6305
|
-
const meshHost = resolveMeshHostStatus(mesh);
|
|
6306
|
-
if (!meshHost.canOwnCoordinator) {
|
|
6307
|
-
return {
|
|
6308
|
-
success: false,
|
|
6309
|
-
...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
|
|
6310
|
-
meshId,
|
|
6311
|
-
cliType,
|
|
6312
|
-
};
|
|
6313
|
-
}
|
|
6314
|
-
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
6315
|
-
|
|
6316
|
-
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
6317
|
-
? args.coordinatorNodeId.trim()
|
|
6318
|
-
: '';
|
|
6319
|
-
const preferredCoordinatorNodeId = requestedCoordinatorNodeId
|
|
6320
|
-
|| (typeof mesh.coordinator?.preferredNodeId === 'string' ? mesh.coordinator.preferredNodeId.trim() : '');
|
|
6321
|
-
const coordinatorNode = preferredCoordinatorNodeId
|
|
6322
|
-
? mesh.nodes.find((node: any) => node?.id === preferredCoordinatorNodeId || node?.nodeId === preferredCoordinatorNodeId)
|
|
6323
|
-
: mesh.nodes[0];
|
|
6324
|
-
if (!coordinatorNode) {
|
|
6325
|
-
return {
|
|
6326
|
-
success: false,
|
|
6327
|
-
code: 'mesh_coordinator_node_not_found',
|
|
6328
|
-
error: `Coordinator node ${preferredCoordinatorNodeId} was not found in mesh`,
|
|
6329
|
-
meshId,
|
|
6330
|
-
cliType,
|
|
6331
|
-
};
|
|
6332
|
-
}
|
|
6333
|
-
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
6334
|
-
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
6335
|
-
: [];
|
|
6336
|
-
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
6337
|
-
const workspace = readLiveMeshNodeWorkspace({
|
|
6338
|
-
meshId,
|
|
6339
|
-
nodeId: String(normalizeMeshNodeId(coordinatorNode) || preferredCoordinatorNodeId || ''),
|
|
6340
|
-
liveSessionRecords: liveMeshSessions,
|
|
6341
|
-
allowCoordinatorSession: true,
|
|
6342
|
-
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
6343
|
-
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
6344
|
-
if (!cliType) {
|
|
6345
|
-
const resolved = await resolveProviderTypeFromPriority({
|
|
6346
|
-
nodeId: String(normalizeMeshNodeId(coordinatorNode) || preferredCoordinatorNodeId || 'coordinator'),
|
|
6347
|
-
providerPriority: readProviderPriorityFromPolicy(coordinatorNode.policy),
|
|
6348
|
-
providerLoader: this.deps.providerLoader,
|
|
6349
|
-
onStatusChange: this.deps.onStatusChange,
|
|
6350
|
-
});
|
|
6351
|
-
if (!resolved.providerType) {
|
|
6352
|
-
return {
|
|
6353
|
-
success: false,
|
|
6354
|
-
code: 'mesh_coordinator_provider_priority_unusable',
|
|
6355
|
-
error: resolved.error || 'No usable provider found from node providerPriority',
|
|
6356
|
-
meshId,
|
|
6357
|
-
cliType,
|
|
6358
|
-
workspace,
|
|
6359
|
-
};
|
|
6360
|
-
}
|
|
6361
|
-
cliType = resolved.providerType;
|
|
6362
|
-
}
|
|
6363
|
-
const providerMeta = this.deps.providerLoader.resolve?.(cliType) || this.deps.providerLoader.getMeta(cliType);
|
|
6364
|
-
const coordinatorSetup = resolveMeshCoordinatorSetup({
|
|
6365
|
-
provider: providerMeta,
|
|
6366
|
-
cliType,
|
|
6367
|
-
meshId,
|
|
6368
|
-
workspace,
|
|
6369
|
-
});
|
|
6370
|
-
|
|
6371
|
-
if (coordinatorSetup.kind === 'unsupported') {
|
|
6372
|
-
return {
|
|
6373
|
-
success: false,
|
|
6374
|
-
code: 'mesh_coordinator_unsupported',
|
|
6375
|
-
error: coordinatorSetup.reason,
|
|
6376
|
-
meshId,
|
|
6377
|
-
cliType,
|
|
6378
|
-
workspace,
|
|
6379
|
-
};
|
|
6380
|
-
}
|
|
6381
|
-
|
|
6382
|
-
if (coordinatorSetup.kind === 'manual') {
|
|
6383
|
-
return {
|
|
6384
|
-
success: false,
|
|
6385
|
-
code: 'mesh_coordinator_manual_mcp_setup_required',
|
|
6386
|
-
error: coordinatorSetup.instructions,
|
|
6387
|
-
meshId,
|
|
6388
|
-
cliType,
|
|
6389
|
-
workspace,
|
|
6390
|
-
meshCoordinatorSetup: coordinatorSetup,
|
|
6391
|
-
};
|
|
6392
|
-
}
|
|
6393
|
-
|
|
6394
|
-
// ─── CLI-command MCP registration (Codex, Gemini CLI) ───────────
|
|
6395
|
-
if (coordinatorSetup.kind === 'cli_command') {
|
|
6396
|
-
// Build coordinator prompt first — fail closed on errors.
|
|
6397
|
-
let cliCmdSystemPrompt = '';
|
|
6398
|
-
try {
|
|
6399
|
-
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
|
|
6400
|
-
} catch (error: any) {
|
|
6401
|
-
const message = error?.message || String(error);
|
|
6402
|
-
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
6403
|
-
return {
|
|
6404
|
-
success: false,
|
|
6405
|
-
code: 'mesh_coordinator_prompt_failed',
|
|
6406
|
-
error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
|
|
6407
|
-
meshId, cliType, workspace,
|
|
6408
|
-
};
|
|
6409
|
-
}
|
|
6410
|
-
|
|
6411
|
-
// Run the provider's MCP registration command under a
|
|
6412
|
-
// PTY. Some providers (agy, future bubbletea CLIs)
|
|
6413
|
-
// refuse to run without /dev/tty, so pipe-only
|
|
6414
|
-
// execFileSync silently no-ops the registration and
|
|
6415
|
-
// the coordinator ends up without any mcp tools. With
|
|
6416
|
-
// a real PTY the registration goes through and the
|
|
6417
|
-
// exit code tells us whether it actually persisted.
|
|
6418
|
-
let mcpRegistrationOk = false;
|
|
6419
|
-
let mcpRegistrationFailure: {
|
|
6420
|
-
command: string;
|
|
6421
|
-
output: string;
|
|
6422
|
-
exitCode: number | null;
|
|
6423
|
-
signal: number | null;
|
|
6424
|
-
timedOut: boolean;
|
|
6425
|
-
} | null = null;
|
|
6426
|
-
try {
|
|
6427
|
-
const { buildMeshCoordinatorRegistrationPlan, execUnderPty } = await import('./mesh-coordinator.js');
|
|
6428
|
-
const registrationPlan = buildMeshCoordinatorRegistrationPlan(
|
|
6429
|
-
cliType,
|
|
6430
|
-
coordinatorSetup.serverName,
|
|
6431
|
-
coordinatorSetup.command,
|
|
6432
|
-
);
|
|
6433
|
-
for (const step of registrationPlan) {
|
|
6434
|
-
const renderedCommand = [step.command, ...step.args].join(' ');
|
|
6435
|
-
LOG.info('MeshCoordinator', `Running MCP ${step.label} (pty): ${renderedCommand}`);
|
|
6436
|
-
const ptyResult = await execUnderPty(step.command, step.args, { cwd: workspace, timeoutMs: 20_000 });
|
|
6437
|
-
if (ptyResult.exitCode === 0 && !ptyResult.timedOut) {
|
|
6438
|
-
if (step.required) mcpRegistrationOk = true;
|
|
6439
|
-
continue;
|
|
6440
|
-
}
|
|
6441
|
-
LOG.warn('MeshCoordinator', `MCP ${step.label} failed exit=${ptyResult.exitCode} signal=${ptyResult.signal} timedOut=${ptyResult.timedOut} — output:\n${ptyResult.output.slice(-2000)}`);
|
|
6442
|
-
if (step.required) {
|
|
6443
|
-
mcpRegistrationFailure = {
|
|
6444
|
-
command: renderedCommand,
|
|
6445
|
-
output: ptyResult.output.slice(-2000),
|
|
6446
|
-
exitCode: ptyResult.exitCode,
|
|
6447
|
-
signal: ptyResult.signal,
|
|
6448
|
-
timedOut: ptyResult.timedOut,
|
|
6449
|
-
};
|
|
6450
|
-
break;
|
|
6451
|
-
}
|
|
6452
|
-
}
|
|
6453
|
-
} catch (error: any) {
|
|
6454
|
-
LOG.warn('MeshCoordinator', `MCP registration command failed: ${error?.message || error}`);
|
|
6455
|
-
mcpRegistrationFailure = {
|
|
6456
|
-
command: coordinatorSetup.command,
|
|
6457
|
-
output: error?.message || String(error),
|
|
6458
|
-
exitCode: null,
|
|
6459
|
-
signal: null,
|
|
6460
|
-
timedOut: false,
|
|
6461
|
-
};
|
|
6462
|
-
}
|
|
6463
|
-
|
|
6464
|
-
if (!mcpRegistrationOk) {
|
|
6465
|
-
return {
|
|
6466
|
-
success: false,
|
|
6467
|
-
code: 'mesh_coordinator_mcp_registration_failed',
|
|
6468
|
-
error: `Could not register ${coordinatorSetup.serverName}; coordinator session was not launched`,
|
|
6469
|
-
meshId,
|
|
6470
|
-
cliType,
|
|
6471
|
-
workspace,
|
|
6472
|
-
registration: mcpRegistrationFailure,
|
|
6473
|
-
};
|
|
6474
|
-
}
|
|
6475
|
-
|
|
6476
|
-
// Codex gives repo-local .mcp.json precedence over its
|
|
6477
|
-
// global `codex mcp add` registration. Refresh an
|
|
6478
|
-
// existing ADHDev entry so a stale workspace command
|
|
6479
|
-
// cannot shadow the registration we just verified.
|
|
6480
|
-
if (cliType === 'codex-cli') {
|
|
6481
|
-
const repoMcpConfigPath = pathJoin(workspace, '.mcp.json');
|
|
6482
|
-
if (fs.existsSync(repoMcpConfigPath)) {
|
|
6483
|
-
try {
|
|
6484
|
-
const repoMcpConfig = parseMeshCoordinatorMcpConfig(
|
|
6485
|
-
fs.readFileSync(repoMcpConfigPath, 'utf-8'),
|
|
6486
|
-
'claude_mcp_json',
|
|
6487
|
-
);
|
|
6488
|
-
const existingServers = repoMcpConfig.mcpServers;
|
|
6489
|
-
if (
|
|
6490
|
-
existingServers
|
|
6491
|
-
&& typeof existingServers === 'object'
|
|
6492
|
-
&& !Array.isArray(existingServers)
|
|
6493
|
-
&& existingServers[coordinatorSetup.serverName]
|
|
6494
|
-
) {
|
|
6495
|
-
fs.writeFileSync(repoMcpConfigPath, serializeMeshCoordinatorMcpConfig({
|
|
6496
|
-
...repoMcpConfig,
|
|
6497
|
-
mcpServers: {
|
|
6498
|
-
...existingServers,
|
|
6499
|
-
[coordinatorSetup.serverName]: coordinatorSetup.mcpServer,
|
|
6500
|
-
},
|
|
6501
|
-
}, 'claude_mcp_json'), 'utf-8');
|
|
6502
|
-
LOG.info('MeshCoordinator', `Refreshed repo-local ${repoMcpConfigPath} entry for ${coordinatorSetup.serverName}`);
|
|
6503
|
-
}
|
|
6504
|
-
} catch (error: any) {
|
|
6505
|
-
return {
|
|
6506
|
-
success: false,
|
|
6507
|
-
code: 'mesh_coordinator_config_write_failed',
|
|
6508
|
-
error: `Could not refresh repo-local MCP config: ${error?.message || error}`,
|
|
6509
|
-
meshId,
|
|
6510
|
-
cliType,
|
|
6511
|
-
workspace,
|
|
6512
|
-
};
|
|
6513
|
-
}
|
|
6514
|
-
}
|
|
6515
|
-
}
|
|
6516
|
-
|
|
6517
|
-
// Inject system prompt declaratively from provider.v1.json.
|
|
6518
|
-
const cliCmdArgs: string[] = [];
|
|
6519
|
-
const cliCmdEnv: Record<string, string> = {};
|
|
6520
|
-
let cliCmdContextFilePath: string | undefined;
|
|
6521
|
-
if (cliCmdSystemPrompt) {
|
|
6522
|
-
const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
|
|
6523
|
-
const effect = applyMeshCoordinatorSystemPromptInjection(
|
|
6524
|
-
cliCmdSystemPrompt,
|
|
6525
|
-
providerMeta?.meshCoordinator?.systemPromptInjection,
|
|
6526
|
-
{ cliArgs: cliCmdArgs, launchEnv: cliCmdEnv, workspace, cliType },
|
|
6527
|
-
);
|
|
6528
|
-
cliCmdContextFilePath = effect.contextFilePath;
|
|
6529
|
-
}
|
|
6530
|
-
|
|
6531
|
-
const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
6532
|
-
cliType,
|
|
6533
|
-
dir: workspace,
|
|
6534
|
-
cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
|
|
6535
|
-
env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
|
|
6536
|
-
settings: { meshCoordinatorFor: meshId },
|
|
6537
|
-
});
|
|
6538
|
-
|
|
6539
|
-
// R48 inject-then-remove. Spawn was just kicked off above; agy and
|
|
6540
|
-
// gemini-cli read AGENTS.md / GEMINI.md exactly once at startup and
|
|
6541
|
-
// cache it for the rest of the session, so we can safely strip
|
|
6542
|
-
// the wrapper from disk shortly after launch. That keeps any
|
|
6543
|
-
// worker session launched into the same workspace later from
|
|
6544
|
-
// picking up our wrapper block.
|
|
6545
|
-
if (cliCmdLaunch?.success && cliCmdContextFilePath) {
|
|
6546
|
-
const stripPath = cliCmdContextFilePath;
|
|
6547
|
-
setTimeout(() => {
|
|
6548
|
-
void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
|
|
6549
|
-
stripCoordinatorWrapperFile(stripPath);
|
|
6550
|
-
LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (cli_command)`);
|
|
6551
|
-
}).catch(() => { /* best-effort */ });
|
|
6552
|
-
}, 5000);
|
|
6553
|
-
}
|
|
6554
|
-
|
|
6555
|
-
if (!cliCmdLaunch?.success) {
|
|
6556
|
-
return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
|
|
6557
|
-
}
|
|
6558
|
-
|
|
6559
|
-
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
|
|
6560
|
-
const cliCmdSessionId = cliCmdLaunch.sessionId || cliCmdLaunch.id;
|
|
6561
|
-
if (cliCmdSessionId) {
|
|
6562
|
-
const cliCmdInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
|
|
6563
|
-
registerMeshCoordinator({
|
|
6564
|
-
meshId,
|
|
6565
|
-
sessionId: cliCmdSessionId,
|
|
6566
|
-
workspace,
|
|
6567
|
-
startedAt: Date.now(),
|
|
6568
|
-
cliType,
|
|
6569
|
-
systemPrompt: cliCmdSystemPrompt || undefined,
|
|
6570
|
-
extraSystemPrompt: extraSystemPrompt || undefined,
|
|
6571
|
-
injection: cliCmdInjectionDecl ? {
|
|
6572
|
-
mode: cliCmdInjectionDecl.mode,
|
|
6573
|
-
target: 'flag' in cliCmdInjectionDecl ? cliCmdInjectionDecl.flag
|
|
6574
|
-
: 'name' in cliCmdInjectionDecl ? cliCmdInjectionDecl.name
|
|
6575
|
-
: 'path' in cliCmdInjectionDecl ? cliCmdInjectionDecl.path
|
|
6576
|
-
: undefined,
|
|
6577
|
-
} : undefined,
|
|
6578
|
-
});
|
|
6579
|
-
}
|
|
6580
|
-
try {
|
|
6581
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
6582
|
-
appendLedgerEntry(meshId, {
|
|
6583
|
-
kind: 'coordinator_started',
|
|
6584
|
-
sessionId: cliCmdSessionId,
|
|
6585
|
-
providerType: cliType,
|
|
6586
|
-
payload: { workspace },
|
|
6587
|
-
});
|
|
6588
|
-
} catch { /* best-effort */ }
|
|
6589
|
-
|
|
6590
|
-
return {
|
|
6591
|
-
success: true,
|
|
6592
|
-
meshId,
|
|
6593
|
-
cliType,
|
|
6594
|
-
workspace,
|
|
6595
|
-
sessionId: cliCmdSessionId,
|
|
6596
|
-
mcpRegistered: mcpRegistrationOk,
|
|
6597
|
-
};
|
|
6598
|
-
}
|
|
6599
|
-
|
|
6600
|
-
const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
|
|
6601
|
-
if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
|
|
6602
|
-
return {
|
|
6603
|
-
success: false,
|
|
6604
|
-
code: 'mesh_coordinator_unsupported',
|
|
6605
|
-
error: `Unsupported auto-import MCP config format: ${String(coordinatorSetup.configFormat)}`,
|
|
6606
|
-
meshId,
|
|
6607
|
-
cliType,
|
|
6608
|
-
workspace,
|
|
6609
|
-
};
|
|
6610
|
-
}
|
|
6611
|
-
|
|
6612
|
-
// Build the coordinator prompt before mutating workspace config or launching.
|
|
6613
|
-
// Prompt generation failures are configuration/data-shape errors; fail closed so
|
|
6614
|
-
// broken mesh state is visible instead of silently launching with weaker rules.
|
|
6615
|
-
let systemPrompt = '';
|
|
6616
|
-
try {
|
|
6617
|
-
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
|
|
6618
|
-
} catch (error: any) {
|
|
6619
|
-
const message = error?.message || String(error);
|
|
6620
|
-
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
6621
|
-
return {
|
|
6622
|
-
success: false,
|
|
6623
|
-
code: 'mesh_coordinator_prompt_failed',
|
|
6624
|
-
error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
|
|
6625
|
-
meshId,
|
|
6626
|
-
cliType,
|
|
6627
|
-
workspace,
|
|
6628
|
-
};
|
|
6629
|
-
}
|
|
6630
|
-
|
|
6631
|
-
// 1. Write provider-declared MCP config for CLIs that auto-import it.
|
|
6632
|
-
const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
|
|
6633
|
-
const { dirname } = await import('path');
|
|
6634
|
-
const mcpConfigPath = coordinatorSetup.configPath;
|
|
6635
|
-
const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
|
|
6636
|
-
? createHermesManualMeshCoordinatorSetup(meshId, workspace)
|
|
6637
|
-
: null;
|
|
6638
|
-
let hermesBaseConfig: { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } | null = null;
|
|
6639
|
-
if (hermesManualFallback) {
|
|
6640
|
-
try {
|
|
6641
|
-
hermesBaseConfig = loadHermesCoordinatorBaseConfig(mcpConfigPath);
|
|
6642
|
-
} catch (error: any) {
|
|
6643
|
-
const message = `Failed to parse Hermes base config for automatic coordinator setup: ${error?.message || error}`;
|
|
6644
|
-
LOG.error('MeshCoordinator', message);
|
|
6645
|
-
return { success: false, code: 'mesh_coordinator_config_parse_failed', error: message, meshId, cliType, workspace };
|
|
6646
|
-
}
|
|
6647
|
-
}
|
|
6648
|
-
const returnManualFallback = (message: string) => ({
|
|
6649
|
-
success: false,
|
|
6650
|
-
code: 'mesh_coordinator_manual_mcp_setup_required',
|
|
6651
|
-
error: message,
|
|
6652
|
-
meshId,
|
|
6653
|
-
cliType,
|
|
6654
|
-
workspace,
|
|
6655
|
-
meshCoordinatorSetup: hermesManualFallback,
|
|
6656
|
-
});
|
|
6657
|
-
|
|
6658
|
-
// Merge ADHDev mesh server into existing config.
|
|
6659
|
-
// Pass full mesh data as env var so the MCP server can bootstrap
|
|
6660
|
-
// without depending on meshes.json or a running daemon.
|
|
6661
|
-
const mcpServerEntry: Record<string, any> = {
|
|
6662
|
-
command: coordinatorSetup.mcpServer.command,
|
|
6663
|
-
args: coordinatorSetup.mcpServer.args,
|
|
6664
|
-
};
|
|
6665
|
-
if (args?.inlineMesh) {
|
|
6666
|
-
const modeArgIndex = coordinatorSetup.mcpServer.args.findIndex((value: string) => value === '--mode');
|
|
6667
|
-
const mcpTransport = modeArgIndex >= 0 ? coordinatorSetup.mcpServer.args[modeArgIndex + 1] : 'ipc';
|
|
6668
|
-
mcpServerEntry.env = {
|
|
6669
|
-
ADHDEV_INLINE_MESH: JSON.stringify(mesh),
|
|
6670
|
-
ADHDEV_MCP_TRANSPORT: mcpTransport === 'local' ? 'local' : 'ipc',
|
|
6671
|
-
};
|
|
6672
|
-
}
|
|
6673
|
-
|
|
6674
|
-
try {
|
|
6675
|
-
mkdirSync(dirname(mcpConfigPath), { recursive: true });
|
|
6676
|
-
} catch (error: any) {
|
|
6677
|
-
const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
|
|
6678
|
-
LOG.error('MeshCoordinator', message);
|
|
6679
|
-
if (hermesManualFallback) return returnManualFallback(message);
|
|
6680
|
-
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
6681
|
-
}
|
|
6682
|
-
|
|
6683
|
-
// Backup existing MCP config if present.
|
|
6684
|
-
const hadExistingMcpConfig = existsSync(mcpConfigPath);
|
|
6685
|
-
let existingMcpConfig: Record<string, any> = hermesBaseConfig?.config || {};
|
|
6686
|
-
if (hermesBaseConfig) {
|
|
6687
|
-
copyHermesCoordinatorCredentialFiles(hermesBaseConfig.sourceHome, dirname(mcpConfigPath));
|
|
6688
|
-
}
|
|
6689
|
-
if (hadExistingMcpConfig) {
|
|
6690
|
-
try {
|
|
6691
|
-
const parsedExistingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
|
|
6692
|
-
const existingCoordinatorConfig = hermesManualFallback
|
|
6693
|
-
? stripHermesCoordinatorTempModelProviderOverrides(parsedExistingMcpConfig)
|
|
6694
|
-
: parsedExistingMcpConfig;
|
|
6695
|
-
existingMcpConfig = { ...existingMcpConfig, ...existingCoordinatorConfig };
|
|
6696
|
-
copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
|
|
6697
|
-
} catch (error: any) {
|
|
6698
|
-
LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
|
|
6699
|
-
return {
|
|
6700
|
-
success: false,
|
|
6701
|
-
code: 'mesh_coordinator_config_parse_failed',
|
|
6702
|
-
error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
|
|
6703
|
-
};
|
|
6704
|
-
}
|
|
6705
|
-
}
|
|
6706
|
-
|
|
6707
|
-
const mcpServersKey = getMcpServersKey(configFormat);
|
|
6708
|
-
const existingServers = existingMcpConfig[mcpServersKey];
|
|
6709
|
-
const mcpConfig = {
|
|
6710
|
-
...existingMcpConfig,
|
|
6711
|
-
[mcpServersKey]: {
|
|
6712
|
-
...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
|
|
6713
|
-
[coordinatorSetup.serverName]: mcpServerEntry,
|
|
6714
|
-
},
|
|
6715
|
-
};
|
|
6716
|
-
try {
|
|
6717
|
-
writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
|
|
6718
|
-
} catch (error: any) {
|
|
6719
|
-
const message = `Could not write MCP config for automatic setup: ${error?.message || error}`;
|
|
6720
|
-
LOG.error('MeshCoordinator', message);
|
|
6721
|
-
if (hermesManualFallback) return returnManualFallback(message);
|
|
6722
|
-
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
6723
|
-
}
|
|
6724
|
-
LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
|
|
6725
|
-
|
|
6726
|
-
const cliArgs: string[] = [];
|
|
6727
|
-
const launchEnv: Record<string, string> = {};
|
|
6728
|
-
if (configFormat === 'hermes_config_yaml') {
|
|
6729
|
-
launchEnv.HERMES_HOME = dirname(mcpConfigPath);
|
|
6730
|
-
launchEnv.HERMES_IGNORE_USER_CONFIG = '';
|
|
6731
|
-
}
|
|
6732
|
-
let autoImportContextFilePath: string | undefined;
|
|
6733
|
-
if (systemPrompt) {
|
|
6734
|
-
const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
|
|
6735
|
-
const effect = applyMeshCoordinatorSystemPromptInjection(
|
|
6736
|
-
systemPrompt,
|
|
6737
|
-
providerMeta?.meshCoordinator?.systemPromptInjection,
|
|
6738
|
-
{ cliArgs, launchEnv, workspace, cliType },
|
|
6739
|
-
);
|
|
6740
|
-
autoImportContextFilePath = effect.contextFilePath;
|
|
6741
|
-
}
|
|
6742
|
-
if (cliType === 'claude-cli') {
|
|
6743
|
-
cliArgs.push('--mcp-config', coordinatorSetup.configPath);
|
|
6744
|
-
}
|
|
6745
|
-
|
|
6746
|
-
// 3. Launch CLI session via existing cliManager.
|
|
6747
|
-
// Provider-specific prompt injection remains fail-closed: Claude gets
|
|
6748
|
-
// explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
|
|
6749
|
-
const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
6750
|
-
cliType,
|
|
6751
|
-
dir: workspace,
|
|
6752
|
-
cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
|
|
6753
|
-
env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
|
|
6754
|
-
settings: {
|
|
6755
|
-
meshCoordinatorFor: meshId
|
|
6756
|
-
}
|
|
6757
|
-
});
|
|
6758
|
-
|
|
6759
|
-
// R48 inject-then-remove. See the cli_command branch for context;
|
|
6760
|
-
// same idea: strip the wrapper from disk ~5s after launch so the
|
|
6761
|
-
// user's AGENTS.md / GEMINI.md is untouched the moment any
|
|
6762
|
-
// worker session opens up in the same workspace.
|
|
6763
|
-
if (launchResult?.success && autoImportContextFilePath) {
|
|
6764
|
-
const stripPath = autoImportContextFilePath;
|
|
6765
|
-
setTimeout(() => {
|
|
6766
|
-
void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
|
|
6767
|
-
stripCoordinatorWrapperFile(stripPath);
|
|
6768
|
-
LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (auto_import)`);
|
|
6769
|
-
}).catch(() => { /* best-effort */ });
|
|
6770
|
-
}, 5000);
|
|
6771
|
-
}
|
|
6772
|
-
|
|
6773
|
-
if (!launchResult?.success) {
|
|
6774
|
-
return { success: false, error: launchResult?.error || 'Failed to launch CLI session' };
|
|
6775
|
-
}
|
|
6776
|
-
|
|
6777
|
-
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
|
|
6778
|
-
const launchSessionId = launchResult.sessionId || launchResult.id;
|
|
6779
|
-
if (launchSessionId) {
|
|
6780
|
-
const autoImportInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
|
|
6781
|
-
registerMeshCoordinator({
|
|
6782
|
-
meshId,
|
|
6783
|
-
sessionId: launchSessionId,
|
|
6784
|
-
workspace,
|
|
6785
|
-
startedAt: Date.now(),
|
|
6786
|
-
cliType,
|
|
6787
|
-
systemPrompt: systemPrompt || undefined,
|
|
6788
|
-
extraSystemPrompt: extraSystemPrompt || undefined,
|
|
6789
|
-
mcpConfigPath,
|
|
6790
|
-
injection: autoImportInjectionDecl ? {
|
|
6791
|
-
mode: autoImportInjectionDecl.mode,
|
|
6792
|
-
target: 'flag' in autoImportInjectionDecl ? autoImportInjectionDecl.flag
|
|
6793
|
-
: 'name' in autoImportInjectionDecl ? autoImportInjectionDecl.name
|
|
6794
|
-
: 'path' in autoImportInjectionDecl ? autoImportInjectionDecl.path
|
|
6795
|
-
: undefined,
|
|
6796
|
-
} : undefined,
|
|
6797
|
-
});
|
|
6798
|
-
}
|
|
6799
|
-
|
|
6800
|
-
// Record coordinator launch in task ledger
|
|
6801
|
-
try {
|
|
6802
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
6803
|
-
appendLedgerEntry(meshId, {
|
|
6804
|
-
kind: 'coordinator_started',
|
|
6805
|
-
sessionId: launchSessionId,
|
|
6806
|
-
providerType: cliType,
|
|
6807
|
-
payload: { workspace },
|
|
6808
|
-
});
|
|
6809
|
-
} catch { /* ledger append is best-effort */ }
|
|
6810
|
-
|
|
6811
|
-
return {
|
|
6812
|
-
success: true,
|
|
6813
|
-
meshId,
|
|
6814
|
-
cliType,
|
|
6815
|
-
workspace,
|
|
6816
|
-
sessionId: launchSessionId,
|
|
6817
|
-
mcpConfigWritten: true,
|
|
6818
|
-
};
|
|
6819
|
-
} catch (e: any) {
|
|
6820
|
-
LOG.error('MeshCoordinator', `Failed: ${e.message}`);
|
|
6821
|
-
return { success: false, error: e.message };
|
|
6822
|
-
}
|
|
6823
|
-
}
|
|
6824
|
-
|
|
6825
|
-
case 'mesh_status': {
|
|
6826
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6827
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6828
|
-
try {
|
|
6829
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6830
|
-
const mesh = meshRecord?.mesh;
|
|
6831
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
6832
|
-
const meshHost = resolveMeshHostStatus(mesh);
|
|
6833
|
-
|
|
6834
|
-
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
6835
|
-
// Compact (default) elides each mission's full goal text from the
|
|
6836
|
-
// payload — coordinators polling node health don't need every
|
|
6837
|
-
// mission's multi-hundred-char goal repeated. verbose=true (or the
|
|
6838
|
-
// explicit compact=false) restores full goals. Verbose bypasses the
|
|
6839
|
-
// shared (compact) aggregate cache so a verbose call never poisons
|
|
6840
|
-
// the compact cache and vice versa.
|
|
6841
|
-
const verboseMissions = args?.verbose === true || args?.compact === false;
|
|
6842
|
-
// See (B3) below: scope the peek to this daemon when the
|
|
6843
|
-
// caller doesn't tell us, otherwise scoped events look
|
|
6844
|
-
// missing and we falsely return a stale cache.
|
|
6845
|
-
const peekScope = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
6846
|
-
? args.coordinatorDaemonId.trim()
|
|
6847
|
-
: (this.deps.statusInstanceId || undefined);
|
|
6848
|
-
const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId, peekScope).length;
|
|
6849
|
-
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
6850
|
-
if (!refreshRequested && !verboseMissions && pendingCoordinatorEventCount === 0) {
|
|
6851
|
-
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
6852
|
-
if (cachedStatus) {
|
|
6853
|
-
logRepoMeshStatusDebug('return_cached', {
|
|
6854
|
-
meshId,
|
|
6855
|
-
command: 'mesh_status',
|
|
6856
|
-
refreshRequested,
|
|
6857
|
-
summary: summarizeRepoMeshStatusDebug(cachedStatus),
|
|
6858
|
-
});
|
|
6859
|
-
return cachedStatus;
|
|
6860
|
-
}
|
|
6861
|
-
}
|
|
6862
|
-
const refreshReason = refreshRequested
|
|
6863
|
-
? 'explicit_refresh'
|
|
6864
|
-
: pendingCoordinatorEventCount > 0
|
|
6865
|
-
? 'pending_coordinator_events'
|
|
6866
|
-
: hadAggregateCache
|
|
6867
|
-
? 'stale_pending_cache_refresh'
|
|
6868
|
-
: 'cold_cache_miss';
|
|
6869
|
-
|
|
6870
|
-
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
6871
|
-
const queue = getQueue(meshId);
|
|
6872
|
-
const queueSummary = getMeshQueueStats(meshId);
|
|
6873
|
-
|
|
6874
|
-
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
6875
|
-
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
6876
|
-
const asyncRefineLedgerEntries = readLedgerEntries(meshId, { tail: 100 });
|
|
6877
|
-
const ledgerSummary = getLedgerSummary(meshId);
|
|
6878
|
-
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
6879
|
-
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
6880
|
-
: [];
|
|
6881
|
-
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
6882
|
-
|
|
6883
|
-
const localMachineId = loadConfig().machineId || '';
|
|
6884
|
-
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
6885
|
-
// Shared probe gate for this mesh_status call: the bootstrap
|
|
6886
|
-
// hydrate below and the per-node render loop further down both
|
|
6887
|
-
// probe the same peers — route both through this cache so they
|
|
6888
|
-
// dedup within the call and reuse recent results across calls.
|
|
6889
|
-
const meshGitProbeCache = this.meshGitProbeCache;
|
|
6890
|
-
const directTruth = requireDirectPeerTruth
|
|
6891
|
-
? await hydrateInlineMeshDirectTruth({
|
|
6892
|
-
mesh,
|
|
6893
|
-
meshSource: meshRecord.source,
|
|
6894
|
-
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
6895
|
-
getMeshPeerConnectionStatus: this.deps.getMeshPeerConnectionStatus,
|
|
6896
|
-
statusInstanceId: this.deps.statusInstanceId,
|
|
6897
|
-
localMachineId,
|
|
6898
|
-
// Standing-state model: only an explicit refresh fans
|
|
6899
|
-
// out a blocking peer git probe. Default loads return
|
|
6900
|
-
// held truth so one slow peer can't block the graph.
|
|
6901
|
-
probeRemotePeers: refreshRequested,
|
|
6902
|
-
probeCache: meshGitProbeCache,
|
|
6903
|
-
})
|
|
6904
|
-
: {
|
|
6905
|
-
directEvidenceCount: 0,
|
|
6906
|
-
localConfirmedCount: 0,
|
|
6907
|
-
peerAttemptedCount: 0,
|
|
6908
|
-
peerConfirmedCount: 0,
|
|
6909
|
-
standingEvidenceCount: 0,
|
|
6910
|
-
unavailableNodeIds: [] as string[],
|
|
6911
|
-
deadNodeIds: [] as string[],
|
|
6912
|
-
};
|
|
6913
|
-
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
6914
|
-
// a direct mesh truth failure until an explicit probe attempt actually fails.
|
|
6915
|
-
const passivePeerTruthNotAttempted = requireDirectPeerTruth
|
|
6916
|
-
&& !refreshRequested
|
|
6917
|
-
&& directTruth.directEvidenceCount > 0
|
|
6918
|
-
&& directTruth.peerAttemptedCount === 0;
|
|
6919
|
-
const effectiveDirectTruth = passivePeerTruthNotAttempted
|
|
6920
|
-
? { ...directTruth, unavailableNodeIds: [] as string[] }
|
|
6921
|
-
: directTruth;
|
|
6922
|
-
const unavailableDirectTruthNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
6923
|
-
const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
|
|
6924
|
-
&& Array.isArray(mesh.nodes)
|
|
6925
|
-
&& mesh.nodes
|
|
6926
|
-
.filter((node: any) => unavailableDirectTruthNodeIds.has(normalizeMeshNodeId(node) ?? ''))
|
|
6927
|
-
.every((node: any) => node?.isLocalWorktree === true);
|
|
6928
|
-
// Default (non-refresh) loads never hard-fail: held
|
|
6929
|
-
// standing-state truth is returned and the graph renders
|
|
6930
|
-
// immediately. The hard mesh_direct_peer_truth_unavailable
|
|
6931
|
-
// failure is reserved for an explicit refresh that actually
|
|
6932
|
-
// attempted a peer probe and could not confirm any evidence.
|
|
6933
|
-
const directTruthSatisfied = !requireDirectPeerTruth
|
|
6934
|
-
|| !refreshRequested
|
|
6935
|
-
|| (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
|
|
6936
|
-
if (requireDirectPeerTruth && refreshRequested && !directTruthSatisfied) {
|
|
6937
|
-
const failureResult = {
|
|
6938
|
-
success: false,
|
|
6939
|
-
code: 'mesh_direct_peer_truth_unavailable',
|
|
6940
|
-
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
|
|
6941
|
-
sourceOfTruth: {
|
|
6942
|
-
membership: meshRecord.source === 'inline_cache'
|
|
6943
|
-
? 'coordinator_inline_mesh_cache'
|
|
6944
|
-
: meshRecord.source === 'local_config'
|
|
6945
|
-
? 'local_mesh_config'
|
|
6946
|
-
: 'inline_bootstrap_snapshot',
|
|
6947
|
-
coordinatorOwnsLiveTruth: false,
|
|
6948
|
-
currentStatus: 'direct_peer_truth_unavailable',
|
|
6949
|
-
directPeerTruth: {
|
|
6950
|
-
required: true,
|
|
6951
|
-
satisfied: false,
|
|
6952
|
-
directEvidenceCount: directTruth.directEvidenceCount,
|
|
6953
|
-
localConfirmedCount: directTruth.localConfirmedCount,
|
|
6954
|
-
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
6955
|
-
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
6956
|
-
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
6957
|
-
},
|
|
6958
|
-
},
|
|
6959
|
-
};
|
|
6960
|
-
logRepoMeshStatusDebug('direct_truth_unavailable', {
|
|
6961
|
-
meshId,
|
|
6962
|
-
command: 'mesh_status',
|
|
6963
|
-
refreshRequested,
|
|
6964
|
-
meshSource: meshRecord.source,
|
|
6965
|
-
directTruth,
|
|
6966
|
-
});
|
|
6967
|
-
return failureResult;
|
|
6968
|
-
}
|
|
6969
|
-
const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
6970
|
-
const coordinatorHostname = osHostname();
|
|
6971
|
-
const selectedCoordinatorNodeId = readStringValue(
|
|
6972
|
-
mesh.coordinator?.preferredNodeId,
|
|
6973
|
-
normalizeMeshNodeId(mesh.nodes?.[0] as any),
|
|
6974
|
-
);
|
|
6975
|
-
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
6976
|
-
? selectedCoordinatorNodeId
|
|
6977
|
-
: undefined;
|
|
6978
|
-
const refreshedAt = new Date().toISOString();
|
|
6979
|
-
const nodeStatuses = [];
|
|
6980
|
-
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
6981
|
-
const nodeId = normalizeMeshNodeId(node) ?? '';
|
|
6982
|
-
const daemonId = readStringValue(node.daemonId);
|
|
6983
|
-
const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
|
|
6984
|
-
const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
|
|
6985
|
-
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
6986
|
-
const configuredCoordinatorNode = Boolean(
|
|
6987
|
-
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
6988
|
-
);
|
|
6989
|
-
const sparseConfiguredCoordinatorNode = configuredCoordinatorNode
|
|
6990
|
-
&& !daemonId
|
|
6991
|
-
&& !nodeMachineId
|
|
6992
|
-
&& !nodeHostname;
|
|
6993
|
-
const isSelfNode = Boolean(
|
|
6994
|
-
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
6995
|
-
) || Boolean(
|
|
6996
|
-
daemonId && (daemonIdsEquivalent(daemonId, localMachineId) || daemonIdsEquivalent(daemonId, this.deps.statusInstanceId)),
|
|
6997
|
-
) || Boolean(meshRecord?.inline && nodeIndex === 0)
|
|
6998
|
-
|| sparseConfiguredCoordinatorNode;
|
|
6999
|
-
const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
|
|
7000
|
-
localMachineId,
|
|
7001
|
-
localDaemonId: this.deps.statusInstanceId,
|
|
7002
|
-
coordinatorHostname,
|
|
7003
|
-
isSelfNode,
|
|
7004
|
-
});
|
|
7005
|
-
const status: Record<string, unknown> = {
|
|
7006
|
-
nodeId,
|
|
7007
|
-
machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
|
|
7008
|
-
labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
|
|
7009
|
-
? 'explicit_metadata'
|
|
7010
|
-
: 'workspace_host_provider_context',
|
|
7011
|
-
workspace: node.workspace,
|
|
7012
|
-
repoRoot: node.repoRoot,
|
|
7013
|
-
isLocalWorktree: node.isLocalWorktree,
|
|
7014
|
-
worktreeBranch: node.worktreeBranch,
|
|
7015
|
-
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
7016
|
-
daemonId,
|
|
7017
|
-
machineId: nodeMachineId || node.machineId,
|
|
7018
|
-
machine: machineIdentity,
|
|
7019
|
-
machineStatus: node.machineStatus,
|
|
7020
|
-
health: 'unknown',
|
|
7021
|
-
providers: node.providers || [],
|
|
7022
|
-
providerPriority,
|
|
7023
|
-
activeSessions: [],
|
|
7024
|
-
activeSessionDetails: [],
|
|
7025
|
-
launchReady: false,
|
|
7026
|
-
};
|
|
7027
|
-
if (isSelfNode) {
|
|
7028
|
-
status.connection = {
|
|
7029
|
-
perspective: 'selected_coordinator',
|
|
7030
|
-
source: 'mesh_peer_status',
|
|
7031
|
-
state: 'self',
|
|
7032
|
-
transport: 'local',
|
|
7033
|
-
reported: true,
|
|
7034
|
-
reason: 'Selected coordinator daemon',
|
|
7035
|
-
lastStateChangeAt: refreshedAt,
|
|
7036
|
-
};
|
|
7037
|
-
} else if (daemonId) {
|
|
7038
|
-
const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
7039
|
-
status.connection = connection ?? {
|
|
7040
|
-
perspective: 'selected_coordinator',
|
|
7041
|
-
source: 'not_reported',
|
|
7042
|
-
state: 'unknown',
|
|
7043
|
-
transport: 'unknown',
|
|
7044
|
-
reported: false,
|
|
7045
|
-
reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
|
|
7046
|
-
};
|
|
7047
|
-
} else {
|
|
7048
|
-
status.connection = {
|
|
7049
|
-
perspective: 'selected_coordinator',
|
|
7050
|
-
source: 'not_reported',
|
|
7051
|
-
state: 'unknown',
|
|
7052
|
-
transport: 'unknown',
|
|
7053
|
-
reported: false,
|
|
7054
|
-
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
7055
|
-
};
|
|
7056
|
-
}
|
|
7057
|
-
const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
|
|
7058
|
-
meshId,
|
|
7059
|
-
node,
|
|
7060
|
-
nodeId,
|
|
7061
|
-
liveSessionRecords: liveMeshSessions,
|
|
7062
|
-
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
7063
|
-
});
|
|
7064
|
-
const workspace = readLiveMeshNodeWorkspace({
|
|
7065
|
-
meshId,
|
|
7066
|
-
nodeId,
|
|
7067
|
-
liveSessionRecords: matchedLiveSessionRecords,
|
|
7068
|
-
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
7069
|
-
}) || (typeof node.workspace === 'string' ? node.workspace : '');
|
|
7070
|
-
status.workspace = workspace || node.workspace;
|
|
7071
|
-
if (matchedLiveSessionRecords.length > 0) {
|
|
7072
|
-
const sessionIds = matchedLiveSessionRecords
|
|
7073
|
-
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
7074
|
-
.filter(Boolean);
|
|
7075
|
-
const providerTypes = matchedLiveSessionRecords
|
|
7076
|
-
.map((record: any) => readStringValue(record?.providerType))
|
|
7077
|
-
.filter(Boolean) as string[];
|
|
7078
|
-
status.activeSessions = sessionIds;
|
|
7079
|
-
status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
|
|
7080
|
-
if (providerTypes.length > 0) {
|
|
7081
|
-
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
7082
|
-
}
|
|
7083
|
-
}
|
|
7084
|
-
if (workspace) {
|
|
7085
|
-
if (!fs.existsSync(workspace)) {
|
|
7086
|
-
// Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
|
|
7087
|
-
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
7088
|
-
let remoteProbeApplied = false;
|
|
7089
|
-
if (inlineTransitGit) {
|
|
7090
|
-
status.git = inlineTransitGit;
|
|
7091
|
-
status.health = inlineTransitGit.isGitRepo
|
|
7092
|
-
? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
|
|
7093
|
-
: 'degraded';
|
|
7094
|
-
const connection = readObjectRecord(status.connection);
|
|
7095
|
-
const connectionState = readStringValue(connection.state);
|
|
7096
|
-
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
7097
|
-
if (!connectionReported || connectionState === 'unknown') {
|
|
7098
|
-
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
7099
|
-
}
|
|
7100
|
-
remoteProbeApplied = true;
|
|
7101
|
-
} else if (refreshRequested && !isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
7102
|
-
// Only an explicit refresh fans out a blocking
|
|
7103
|
-
// per-node git probe. On the default load a peer
|
|
7104
|
-
// with no held truth falls through to
|
|
7105
|
-
// gitProbePending below — the graph still renders.
|
|
7106
|
-
// Bounded retry (shared with the bootstrap hydrate
|
|
7107
|
-
// path), gated on the peer staying connected, so a
|
|
7108
|
-
// slow TURN-relayed peer is recovered rather than
|
|
7109
|
-
// dropped after a single timeout.
|
|
7110
|
-
const runNodeProbe = () => probeRemoteMeshGitStatusWithRetry({
|
|
7111
|
-
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
7112
|
-
daemonId,
|
|
7113
|
-
workspace,
|
|
7114
|
-
timeoutMs: MESH_DIRECT_PROBE_TIMEOUT_MS,
|
|
7115
|
-
retryTimeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
|
|
7116
|
-
getConnection: this.deps.getMeshPeerConnectionStatus,
|
|
7117
|
-
onConnection: connection => { status.connection = connection; },
|
|
7118
|
-
});
|
|
7119
|
-
// Same shared cache as the bootstrap hydrate path: within one
|
|
7120
|
-
// mesh_status call this dedups the bootstrap probe against this
|
|
7121
|
-
// per-node probe for the same peer, and across calls it reuses a
|
|
7122
|
-
// recent result so the dashboard auto-retry loop can't restart a
|
|
7123
|
-
// fresh refreshUpstream probe seconds apart.
|
|
7124
|
-
const remoteGit = await meshGitProbeCache.probe(daemonId, workspace, runNodeProbe);
|
|
7125
|
-
if (remoteGit) {
|
|
7126
|
-
status.git = remoteGit;
|
|
7127
|
-
status.health = remoteGit.isGitRepo
|
|
7128
|
-
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
7129
|
-
: 'degraded';
|
|
7130
|
-
const connection = readObjectRecord(status.connection);
|
|
7131
|
-
const connectionState = readStringValue(connection.state);
|
|
7132
|
-
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
7133
|
-
if (!connectionReported || connectionState === 'unknown') {
|
|
7134
|
-
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
7135
|
-
}
|
|
7136
|
-
const reporter = recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
7137
|
-
persistNodeReporterPlatform(meshRecord.source, mesh, nodeId, reporter);
|
|
7138
|
-
remoteProbeApplied = true;
|
|
7139
|
-
}
|
|
7140
|
-
}
|
|
7141
|
-
if (!remoteProbeApplied) {
|
|
7142
|
-
const connectionState = readStringValue((status.connection as any)?.state);
|
|
7143
|
-
const pendingPeerGitProbe = !inlineTransitGit
|
|
7144
|
-
&& !isSelfNode
|
|
7145
|
-
&& !!daemonId
|
|
7146
|
-
&& (
|
|
7147
|
-
readStringValue(status.machineStatus) === 'online'
|
|
7148
|
-
|| readStringValue(status.health) === 'online'
|
|
7149
|
-
|| connectionState === 'connecting'
|
|
7150
|
-
|| connectionState === 'connected'
|
|
7151
|
-
|| connectionState === 'unknown'
|
|
7152
|
-
);
|
|
7153
|
-
if (pendingPeerGitProbe) {
|
|
7154
|
-
status.gitProbePending = true;
|
|
7155
|
-
status.health = 'unknown';
|
|
7156
|
-
}
|
|
7157
|
-
if (applyCachedInlineMeshNodeStatus(
|
|
7158
|
-
status,
|
|
7159
|
-
node,
|
|
7160
|
-
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
7161
|
-
)) {
|
|
7162
|
-
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
7163
|
-
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
7164
|
-
nodeStatuses.push(status);
|
|
7165
|
-
continue;
|
|
7166
|
-
}
|
|
7167
|
-
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
7168
|
-
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
7169
|
-
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
7170
|
-
nodeStatuses.push(status);
|
|
7171
|
-
continue;
|
|
7172
|
-
}
|
|
7173
|
-
}
|
|
7174
|
-
} else {
|
|
7175
|
-
try {
|
|
7176
|
-
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
7177
|
-
status.git = gitStatus;
|
|
7178
|
-
const reporter = recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
7179
|
-
persistNodeReporterPlatform(meshRecord.source, mesh, nodeId, reporter);
|
|
7180
|
-
if (gitStatus.isGitRepo) {
|
|
7181
|
-
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
7182
|
-
} else {
|
|
7183
|
-
status.health = 'degraded';
|
|
7184
|
-
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
7185
|
-
}
|
|
7186
|
-
} catch {
|
|
7187
|
-
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
7188
|
-
status.health = 'degraded';
|
|
7189
|
-
}
|
|
7190
|
-
}
|
|
7191
|
-
}
|
|
7192
|
-
} else {
|
|
7193
|
-
applyCachedInlineMeshNodeStatus(status, node);
|
|
7194
|
-
}
|
|
7195
|
-
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
7196
|
-
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
7197
|
-
nodeStatuses.push(status);
|
|
7198
|
-
}
|
|
7199
|
-
|
|
7200
|
-
// (B3) Resolve the coordinator daemon scope for the peek.
|
|
7201
|
-
// mesh_status is a read-only status query — it must not consume
|
|
7202
|
-
// (drain) pending events as a side effect. Coordinators that see
|
|
7203
|
-
// pendingCoordinatorEvents in the response are expected to call
|
|
7204
|
-
// get_pending_mesh_events to explicitly drain them after processing.
|
|
7205
|
-
const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
7206
|
-
? args.coordinatorDaemonId.trim()
|
|
7207
|
-
: (this.deps.statusInstanceId || undefined);
|
|
7208
|
-
const pendingCoordinatorEvents = getPendingMeshCoordinatorEvents(meshId, callerCoordinatorDaemonId);
|
|
7209
|
-
// R4: surface recent fail-loud routing drops so a coordinator/operator can see
|
|
7210
|
-
// that a worker completion was lost (envelope present, mesh unresolved) instead
|
|
7211
|
-
// of it vanishing silently. Diagnostic-only — never cached (see omit below).
|
|
7212
|
-
const unroutableDeliveries = getRecentUnroutableDeliveries();
|
|
7213
|
-
const previewFreshness = (() => {
|
|
7214
|
-
const localRepoRoot = nodeStatuses
|
|
7215
|
-
.map((node: any) => readStringValue(node?.git?.repoRoot, node?.repoRoot, node?.workspace))
|
|
7216
|
-
.find((candidate: string | undefined) => !!candidate && fs.existsSync(candidate));
|
|
7217
|
-
return localRepoRoot ? buildPreviewFreshness(localRepoRoot) : undefined;
|
|
7218
|
-
})();
|
|
7219
|
-
const asyncRefineJobs = buildMeshAsyncRefineJobs({
|
|
7220
|
-
meshId,
|
|
7221
|
-
ledgerEntries: asyncRefineLedgerEntries,
|
|
7222
|
-
pendingEvents: [...pendingCoordinatorEvents],
|
|
7223
|
-
});
|
|
7224
|
-
const historicalSessions = buildHistoricalMeshSessions({
|
|
7225
|
-
meshId,
|
|
7226
|
-
nodes: mesh.nodes || [],
|
|
7227
|
-
liveSessionRecords: liveMeshSessions,
|
|
7228
|
-
});
|
|
7229
|
-
const { getMeshStatusMissionSummaries } = await import('../mesh/mesh-missions.js');
|
|
7230
|
-
// withStats opts in to per-mission operational rollups (durations /
|
|
7231
|
-
// retries) for the dashboard mission detail. The rollup scans a
|
|
7232
|
-
// bounded ledger tail per mission, but only over the bounded set
|
|
7233
|
-
// returned here (live + capped history), so the cost stays linear
|
|
7234
|
-
// in visible missions rather than the whole mesh history.
|
|
7235
|
-
const missions = getMeshStatusMissionSummaries(meshId, { verbose: verboseMissions, withStats: true });
|
|
7236
|
-
const statusResult = {
|
|
7237
|
-
success: true,
|
|
7238
|
-
meshId: mesh.id,
|
|
7239
|
-
meshName: mesh.name,
|
|
7240
|
-
repoIdentity: mesh.repoIdentity,
|
|
7241
|
-
defaultBranch: mesh.defaultBranch,
|
|
7242
|
-
refreshedAt,
|
|
7243
|
-
meshHost,
|
|
7244
|
-
sourceOfTruth: {
|
|
7245
|
-
membership: meshRecord?.source === 'inline_cache'
|
|
7246
|
-
? 'coordinator_inline_mesh_cache'
|
|
7247
|
-
: meshRecord?.source === 'local_config'
|
|
7248
|
-
? 'local_mesh_config'
|
|
7249
|
-
: 'inline_bootstrap_snapshot',
|
|
7250
|
-
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
7251
|
-
meshHost: {
|
|
7252
|
-
owner: 'mesh_host_daemon',
|
|
7253
|
-
localRole: meshHost.role,
|
|
7254
|
-
hostDaemonId: meshHost.hostDaemonId,
|
|
7255
|
-
hostNodeId: meshHost.hostNodeId,
|
|
7256
|
-
hostAddress: meshHost.hostAddress,
|
|
7257
|
-
},
|
|
7258
|
-
...(requireDirectPeerTruth ? {
|
|
7259
|
-
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
7260
|
-
directPeerTruth: {
|
|
7261
|
-
required: true,
|
|
7262
|
-
satisfied: directTruthSatisfied,
|
|
7263
|
-
directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
|
|
7264
|
-
localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
|
|
7265
|
-
peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
|
|
7266
|
-
peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
|
|
7267
|
-
unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
|
|
7268
|
-
partialNodeFailures: effectiveDirectTruth.unavailableNodeIds,
|
|
7269
|
-
},
|
|
7270
|
-
} : {}),
|
|
7271
|
-
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary', 'historicalSessions'],
|
|
7272
|
-
},
|
|
7273
|
-
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
|
|
7274
|
-
...(previewFreshness ? { previewFreshness, deployFreshness: previewFreshness } : {}),
|
|
7275
|
-
nodes: nodeStatuses,
|
|
7276
|
-
queue: { tasks: queue, summary: queueSummary },
|
|
7277
|
-
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
7278
|
-
...(missions.length > 0 ? { missions } : {}),
|
|
7279
|
-
...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
|
|
7280
|
-
...(historicalSessions ? { historicalSessions } : {}),
|
|
7281
|
-
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
7282
|
-
...(unroutableDeliveries.length > 0 ? { unroutableDeliveries } : {}),
|
|
7283
|
-
activeRefineJobs: Array.from(this.runningRefineJobs.values())
|
|
7284
|
-
.filter(job => job.meshId === meshId)
|
|
7285
|
-
.map(job => ({
|
|
7286
|
-
jobId: job.jobId,
|
|
7287
|
-
nodeId: job.targetNodeId,
|
|
7288
|
-
workspace: job.workspace,
|
|
7289
|
-
startedAt: job.startedAt,
|
|
7290
|
-
status: job.status,
|
|
7291
|
-
targetCoordinatorDaemonId: job.targetCoordinatorDaemonId,
|
|
7292
|
-
})),
|
|
7293
|
-
};
|
|
7294
|
-
const { pendingCoordinatorEvents: _pendingCoordinatorEvents, unroutableDeliveries: _unroutableDeliveries, ...cacheableStatusResult } = statusResult as any;
|
|
7295
|
-
// Verbose carries full mission goals; never store it in the shared
|
|
7296
|
-
// (compact) aggregate cache or a later compact poll would return the
|
|
7297
|
-
// heavy goals from cache. Return it without caching.
|
|
7298
|
-
const rememberedStatus = verboseMissions
|
|
7299
|
-
? cacheableStatusResult
|
|
7300
|
-
: this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
|
|
7301
|
-
const returnedStatus = {
|
|
7302
|
-
...rememberedStatus,
|
|
7303
|
-
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
7304
|
-
...(unroutableDeliveries.length > 0 ? { unroutableDeliveries } : {}),
|
|
7305
|
-
};
|
|
7306
|
-
logRepoMeshStatusDebug('return_live', {
|
|
7307
|
-
meshId,
|
|
7308
|
-
command: 'mesh_status',
|
|
7309
|
-
refreshRequested,
|
|
7310
|
-
refreshReason,
|
|
7311
|
-
meshSource: meshRecord.source,
|
|
7312
|
-
directTruth,
|
|
7313
|
-
summary: summarizeRepoMeshStatusDebug(returnedStatus),
|
|
7314
|
-
});
|
|
7315
|
-
return returnedStatus;
|
|
7316
|
-
} catch (e: any) {
|
|
7317
|
-
return { success: false, error: e.message };
|
|
7318
|
-
}
|
|
7319
|
-
}
|
|
7320
|
-
|
|
7321
|
-
case 'get_mesh_review_inbox': {
|
|
7322
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7323
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
7324
|
-
try {
|
|
7325
|
-
const { deriveMeshReviewInboxItems } = await import('../mesh/mesh-review-inbox.js');
|
|
7326
|
-
const { readLedgerEntries } = await import('../mesh/mesh-ledger.js');
|
|
7327
|
-
const { getGitDiffSummary } = await import('../git/git-diff.js');
|
|
7328
|
-
const { existsSync } = await import('node:fs');
|
|
7329
|
-
|
|
7330
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7331
|
-
const mesh = meshRecord?.mesh;
|
|
7332
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
7333
|
-
|
|
7334
|
-
// Ensure we have a fresh aggregate status so nodeStatuses carry
|
|
7335
|
-
// computed fields (connection.state, branchConvergence, isLocalWorktree)
|
|
7336
|
-
// that the raw mesh.nodes config objects don't have.
|
|
7337
|
-
// When the caller provides an inlineMesh, prefer its nodes directly
|
|
7338
|
-
// (they already carry the computed fields from the coordinator).
|
|
7339
|
-
const inlineNodes = args?.inlineMesh && Array.isArray((args.inlineMesh as any)?.nodes)
|
|
7340
|
-
? (args.inlineMesh as any).nodes as Record<string, unknown>[]
|
|
7341
|
-
: null;
|
|
7342
|
-
let cachedStatus = !inlineNodes ? this.getCachedAggregateMeshStatus(meshId, mesh, {}) : null;
|
|
7343
|
-
if (!cachedStatus && !inlineNodes) {
|
|
7344
|
-
const freshStatus = await this.execute('mesh_status', {
|
|
7345
|
-
meshId,
|
|
7346
|
-
inlineMesh: args?.inlineMesh,
|
|
7347
|
-
refresh: true,
|
|
7348
|
-
}, 'get_mesh_review_inbox');
|
|
7349
|
-
cachedStatus = (freshStatus?.success !== false) ? freshStatus : null;
|
|
7350
|
-
}
|
|
7351
|
-
const nodeStatuses: Record<string, unknown>[] = inlineNodes
|
|
7352
|
-
? inlineNodes
|
|
7353
|
-
: Array.isArray(cachedStatus?.nodes)
|
|
7354
|
-
? cachedStatus.nodes as Record<string, unknown>[]
|
|
7355
|
-
: Array.isArray(mesh.nodes)
|
|
7356
|
-
? mesh.nodes as Record<string, unknown>[]
|
|
7357
|
-
: [];
|
|
7358
|
-
|
|
7359
|
-
const ledgerEntries = readLedgerEntries(meshId, { tail: 300 });
|
|
7360
|
-
const derivation = deriveMeshReviewInboxItems({ nodes: nodeStatuses, ledgerEntries });
|
|
7361
|
-
|
|
7362
|
-
for (const item of derivation.items) {
|
|
7363
|
-
const workspace = item.workspace;
|
|
7364
|
-
if (!workspace || !existsSync(workspace)) continue;
|
|
7365
|
-
const baseRef = item.defaultBranch
|
|
7366
|
-
? `origin/${item.defaultBranch}`
|
|
7367
|
-
: 'origin/main';
|
|
7368
|
-
try {
|
|
7369
|
-
const diffResult = await getGitDiffSummary(workspace, { baseRef, maxFiles: 100 });
|
|
7370
|
-
if (diffResult.isGitRepo) {
|
|
7371
|
-
item.diffSummary = {
|
|
7372
|
-
baseRef,
|
|
7373
|
-
files: diffResult.files.map(f => ({
|
|
7374
|
-
path: f.path,
|
|
7375
|
-
status: f.status,
|
|
7376
|
-
insertions: f.insertions,
|
|
7377
|
-
deletions: f.deletions,
|
|
7378
|
-
binary: f.binary,
|
|
7379
|
-
oldPath: f.oldPath,
|
|
7380
|
-
})),
|
|
7381
|
-
totalFiles: diffResult.files.length,
|
|
7382
|
-
totalInsertions: diffResult.totalInsertions,
|
|
7383
|
-
totalDeletions: diffResult.totalDeletions,
|
|
7384
|
-
truncated: diffResult.truncated,
|
|
7385
|
-
...(diffResult.error ? { error: diffResult.error } : {}),
|
|
7386
|
-
};
|
|
7387
|
-
}
|
|
7388
|
-
} catch {
|
|
7389
|
-
item.diffSummary = null;
|
|
7390
|
-
}
|
|
7391
|
-
}
|
|
7392
|
-
|
|
7393
|
-
return {
|
|
7394
|
-
success: true,
|
|
7395
|
-
meshId,
|
|
7396
|
-
inbox: derivation.items,
|
|
7397
|
-
remoteNodesExcluded: derivation.remoteNodesExcluded,
|
|
7398
|
-
excludedRemoteNodeIds: derivation.excludedRemoteNodeIds,
|
|
7399
|
-
};
|
|
7400
|
-
} catch (e: any) {
|
|
7401
|
-
return { success: false, error: e.message };
|
|
7402
|
-
}
|
|
7403
|
-
}
|
|
7404
|
-
|
|
7405
|
-
default:
|
|
7406
|
-
break;
|
|
6420
|
+
// RF-ROUTER HIGH family: high-coupling commands (mesh coordinator-event
|
|
6421
|
+
// relay + interactive prompt, mesh coordinator launch, mesh aggregate
|
|
6422
|
+
// status + review inbox) are handled by the registry after the LOW and
|
|
6423
|
+
// MED families and before the (now empty) switch. HIGH handlers reach the
|
|
6424
|
+
// most router-owned state — the aggregate-status memory cache and the
|
|
6425
|
+
// running-refine-job table — so the context carries those plus bound
|
|
6426
|
+
// read/write helpers and the router's own `execute` (the
|
|
6427
|
+
// get_mesh_review_inbox mesh_status re-entry). A hit returns the same
|
|
6428
|
+
// CommandRouterResult the inlined case used to; a miss falls through to
|
|
6429
|
+
// CommandHandler delegation.
|
|
6430
|
+
const highFamilyHandler = highFamilyRegistry.get(cmd);
|
|
6431
|
+
if (highFamilyHandler) {
|
|
6432
|
+
return await highFamilyHandler(this.buildHighFamilyContext(), args);
|
|
7407
6433
|
}
|
|
7408
6434
|
|
|
7409
6435
|
return null; // Not handled at this level → delegate to CommandHandler
|