@adhdev/daemon-core 0.9.82-rc.113 → 0.9.82-rc.115
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/config/mesh-config.d.ts +2 -0
- package/dist/git/git-commands.d.ts +5 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +976 -269
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +960 -260
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +13 -0
- package/dist/mesh/mesh-refine-status.d.ts +27 -0
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +17 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +115 -0
- package/dist/repo-mesh-types.d.ts +17 -0
- package/package.json +1 -1
- package/src/commands/chat-commands.ts +28 -10
- package/src/commands/router.ts +341 -5
- package/src/config/mesh-config.ts +4 -1
- package/src/git/git-commands.ts +17 -5
- package/src/index.ts +13 -2
- package/src/mesh/mesh-active-work.ts +37 -0
- package/src/mesh/mesh-refine-status.ts +145 -0
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +17 -7
- package/src/mesh/worktree-bootstrap-config.ts +234 -0
- package/src/repo-mesh-types.ts +17 -0
package/src/commands/router.ts
CHANGED
|
@@ -41,6 +41,8 @@ import { buildSessionEntries } from '../status/builders.js';
|
|
|
41
41
|
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
42
42
|
import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
43
43
|
import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
|
|
44
|
+
import { buildPreviewFreshness } from '../mesh/preview-freshness.js';
|
|
45
|
+
import { buildMeshAsyncRefineJobs } from '../mesh/mesh-refine-status.js';
|
|
44
46
|
import {
|
|
45
47
|
MESH_REFINE_CONFIG_LOCATIONS,
|
|
46
48
|
MESH_REFINE_CONFIG_SCHEMA,
|
|
@@ -50,6 +52,12 @@ import {
|
|
|
50
52
|
validateMeshRefineConfig,
|
|
51
53
|
type MeshRefineValidationCommandPlan,
|
|
52
54
|
} from '../mesh/refine-config.js';
|
|
55
|
+
import {
|
|
56
|
+
MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
|
|
57
|
+
MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
|
|
58
|
+
runMeshWorktreeBootstrap,
|
|
59
|
+
type WorktreeBootstrapState,
|
|
60
|
+
} from '../mesh/worktree-bootstrap-config.js';
|
|
53
61
|
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
54
62
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
55
63
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
@@ -58,6 +66,7 @@ import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
|
58
66
|
import { homedir, hostname as osHostname } from 'os';
|
|
59
67
|
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
60
68
|
import * as fs from 'fs';
|
|
69
|
+
import { execFileSync } from 'node:child_process';
|
|
61
70
|
|
|
62
71
|
type ReleaseChannel = 'stable' | 'preview';
|
|
63
72
|
const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
|
|
@@ -915,6 +924,17 @@ function finalizeMeshNodeStatus(args: {
|
|
|
915
924
|
if (machineStatus) status.machineStatus = machineStatus;
|
|
916
925
|
}
|
|
917
926
|
synthesizeMeshNodeFreshnessFromConnection(status);
|
|
927
|
+
const bootstrap = readObjectRecord(node?.worktreeBootstrap);
|
|
928
|
+
if (node?.isLocalWorktree && readStringValue(bootstrap.status)) {
|
|
929
|
+
status.worktreeBootstrap = bootstrap;
|
|
930
|
+
if (bootstrap.status === 'failed' && bootstrap.required !== false) {
|
|
931
|
+
status.launchReady = false;
|
|
932
|
+
status.launchBlockedReason = 'worktree_bootstrap_failed';
|
|
933
|
+
status.launchBlockedMessage = readStringValue(bootstrap.error)
|
|
934
|
+
|| 'Required worktree bootstrap failed; resolve it before launching an agent into this node.';
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
918
938
|
const connectionState = readStringValue(readObjectRecord(status.connection).state);
|
|
919
939
|
status.launchReady = !!daemonId && (
|
|
920
940
|
readStringValue(status.machineStatus) === 'online'
|
|
@@ -1121,6 +1141,48 @@ function collectLiveMeshSessionRecords(args: {
|
|
|
1121
1141
|
return matches;
|
|
1122
1142
|
}
|
|
1123
1143
|
|
|
1144
|
+
function buildHistoricalMeshSessions(args: {
|
|
1145
|
+
meshId: string;
|
|
1146
|
+
nodes: any[];
|
|
1147
|
+
liveSessionRecords: any[];
|
|
1148
|
+
}): { count: number; sessions: Record<string, unknown>[]; instruction: string } | undefined {
|
|
1149
|
+
const liveNodeIds = new Set<string>();
|
|
1150
|
+
const liveWorkspaces = new Set<string>();
|
|
1151
|
+
for (const node of args.nodes || []) {
|
|
1152
|
+
const nodeId = readStringValue(node?.id, node?.nodeId);
|
|
1153
|
+
const workspace = readStringValue(node?.workspace);
|
|
1154
|
+
if (nodeId) liveNodeIds.add(nodeId);
|
|
1155
|
+
if (workspace) liveWorkspaces.add(workspace);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const sessions: Record<string, unknown>[] = [];
|
|
1159
|
+
for (const record of args.liveSessionRecords || []) {
|
|
1160
|
+
const meta = readObjectRecord(record?.meta);
|
|
1161
|
+
const recordMeshId = readStringValue(meta.meshNodeFor, meta.meshCoordinatorFor);
|
|
1162
|
+
if (recordMeshId !== args.meshId) continue;
|
|
1163
|
+
const recordNodeId = readStringValue(meta.meshNodeId);
|
|
1164
|
+
const workspace = readStringValue(record?.workspace);
|
|
1165
|
+
const removedNode = !!recordNodeId && !liveNodeIds.has(recordNodeId);
|
|
1166
|
+
const orphanedWorkspace = !!workspace && !liveWorkspaces.has(workspace) && meta.meshCoordinatorFor !== args.meshId;
|
|
1167
|
+
if (!removedNode && !orphanedWorkspace) continue;
|
|
1168
|
+
sessions.push({
|
|
1169
|
+
...summarizeMeshSessionRecord(record),
|
|
1170
|
+
classification: removedNode ? 'removedNode' : 'orphanedSession',
|
|
1171
|
+
historical: true,
|
|
1172
|
+
meshNodeId: recordNodeId || null,
|
|
1173
|
+
reason: removedNode
|
|
1174
|
+
? 'Session is tagged to a mesh node that is no longer in live membership.'
|
|
1175
|
+
: 'Session workspace is no longer attached to a live mesh node.',
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
if (sessions.length === 0) return undefined;
|
|
1179
|
+
return {
|
|
1180
|
+
count: sessions.length,
|
|
1181
|
+
sessions: sessions.slice(0, 5),
|
|
1182
|
+
instruction: 'These sessions are separated from normal node activeSessions because their mesh node/workspace is no longer live. Use mesh_cleanup_sessions only if cleanup is intended.',
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1124
1186
|
function applyCachedInlineMeshNodeStatus(
|
|
1125
1187
|
status: Record<string, unknown>,
|
|
1126
1188
|
node: any,
|
|
@@ -1221,6 +1283,32 @@ type MeshRefinePatchEquivalenceSummary = {
|
|
|
1221
1283
|
error?: string;
|
|
1222
1284
|
stdout?: string;
|
|
1223
1285
|
stderr?: string;
|
|
1286
|
+
actionableHint?: MeshRefineSubmoduleConflictHint;
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
type MeshRefineSubmoduleConflictHint = {
|
|
1290
|
+
kind: 'submodule_conflict';
|
|
1291
|
+
message: string;
|
|
1292
|
+
conflicts: Array<{
|
|
1293
|
+
path: string;
|
|
1294
|
+
baseCommit?: string;
|
|
1295
|
+
branchCommit?: string;
|
|
1296
|
+
}>;
|
|
1297
|
+
nextSteps: string[];
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
type MeshRefineSubmoduleAlignmentSummary = {
|
|
1301
|
+
status: 'passed' | 'failed' | 'skipped';
|
|
1302
|
+
changedGitlinkPaths: string[];
|
|
1303
|
+
outOfSyncPaths: string[];
|
|
1304
|
+
updatedPaths: string[];
|
|
1305
|
+
verifiedPaths: string[];
|
|
1306
|
+
durationMs: number;
|
|
1307
|
+
reason?: string;
|
|
1308
|
+
command?: string;
|
|
1309
|
+
error?: string;
|
|
1310
|
+
stdout?: string;
|
|
1311
|
+
stderr?: string;
|
|
1224
1312
|
};
|
|
1225
1313
|
|
|
1226
1314
|
type MeshRefineSubmoduleReachabilityEntry = {
|
|
@@ -1405,6 +1493,154 @@ async function runMeshRefinePatchEquivalenceGate(
|
|
|
1405
1493
|
error: e?.message || String(e),
|
|
1406
1494
|
stdout: truncateValidationOutput(e?.stdout),
|
|
1407
1495
|
stderr: truncateValidationOutput(e?.stderr),
|
|
1496
|
+
actionableHint: buildPatchEquivalenceSubmoduleConflictHint(
|
|
1497
|
+
repoRoot,
|
|
1498
|
+
baseHead,
|
|
1499
|
+
branchHead,
|
|
1500
|
+
`${e?.message || ''}\n${e?.stdout || ''}\n${e?.stderr || ''}`,
|
|
1501
|
+
),
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function buildPatchEquivalenceSubmoduleConflictHint(
|
|
1507
|
+
repoRoot: string,
|
|
1508
|
+
baseHead: string,
|
|
1509
|
+
branchHead: string,
|
|
1510
|
+
output: string,
|
|
1511
|
+
): MeshRefineSubmoduleConflictHint | undefined {
|
|
1512
|
+
if (!/(submodule|160000)/i.test(output) || !/(conflict|failed to merge)/i.test(output)) return undefined;
|
|
1513
|
+
const conflicts = readChangedGitlinkPaths(repoRoot, baseHead, branchHead)
|
|
1514
|
+
.map(path => ({
|
|
1515
|
+
path,
|
|
1516
|
+
baseCommit: readTreeObject(repoRoot, baseHead, path),
|
|
1517
|
+
branchCommit: readTreeObject(repoRoot, branchHead, path),
|
|
1518
|
+
}));
|
|
1519
|
+
if (conflicts.length === 0) return undefined;
|
|
1520
|
+
return {
|
|
1521
|
+
kind: 'submodule_conflict',
|
|
1522
|
+
message: 'Refinery could not synthesize a safe merge tree because the branch and base point the same submodule path at different commits.',
|
|
1523
|
+
conflicts,
|
|
1524
|
+
nextSteps: [
|
|
1525
|
+
'Inspect the listed submodule path in both base and branch: baseCommit is the commit currently recorded by the base workspace, branchCommit is the commit recorded by the worktree branch.',
|
|
1526
|
+
'Resolve the submodule first by checking out or creating the intended submodule commit, then commit the chosen gitlink in the root branch.',
|
|
1527
|
+
'Ensure the chosen submodule commit is reachable from the configured submodule remote main branch, then rerun mesh_refine_node.',
|
|
1528
|
+
],
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function readChangedGitlinkPaths(repoRoot: string, fromRef: string, toRef: string): string[] {
|
|
1533
|
+
try {
|
|
1534
|
+
const output = execFileSync('git', ['diff', '--raw', '--no-abbrev', fromRef, toRef], {
|
|
1535
|
+
cwd: repoRoot,
|
|
1536
|
+
encoding: 'utf8',
|
|
1537
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1538
|
+
});
|
|
1539
|
+
const paths = new Set<string>();
|
|
1540
|
+
for (const line of output.split('\n')) {
|
|
1541
|
+
if (!line.trim()) continue;
|
|
1542
|
+
const metaAndPath = line.split('\t');
|
|
1543
|
+
const meta = metaAndPath[0] || '';
|
|
1544
|
+
const path = metaAndPath[metaAndPath.length - 1]?.trim();
|
|
1545
|
+
if (!path) continue;
|
|
1546
|
+
const parts = meta.split(/\s+/);
|
|
1547
|
+
if (parts[0]?.includes('160000') || parts[1]?.includes('160000')) {
|
|
1548
|
+
paths.add(path);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return [...paths].sort();
|
|
1552
|
+
} catch {
|
|
1553
|
+
return [];
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function readTreeObject(repoRoot: string, ref: string, path: string): string | undefined {
|
|
1558
|
+
try {
|
|
1559
|
+
const output = execFileSync('git', ['ls-tree', ref, '--', path], {
|
|
1560
|
+
cwd: repoRoot,
|
|
1561
|
+
encoding: 'utf8',
|
|
1562
|
+
maxBuffer: 1024 * 1024,
|
|
1563
|
+
}).trim();
|
|
1564
|
+
const match = output.match(/\bcommit\s+([0-9a-f]{40})\b/i);
|
|
1565
|
+
return match?.[1];
|
|
1566
|
+
} catch {
|
|
1567
|
+
return undefined;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
async function alignRefinerySubmodulesAfterMerge(
|
|
1572
|
+
repoRoot: string,
|
|
1573
|
+
previousBaseHead: string,
|
|
1574
|
+
currentHead: string,
|
|
1575
|
+
options: { submoduleIgnorePaths?: string[] } = {},
|
|
1576
|
+
): Promise<MeshRefineSubmoduleAlignmentSummary> {
|
|
1577
|
+
const startedAt = Date.now();
|
|
1578
|
+
const changedGitlinkPaths = readChangedGitlinkPaths(repoRoot, previousBaseHead, currentHead)
|
|
1579
|
+
.filter(path => !(options.submoduleIgnorePaths || []).includes(path));
|
|
1580
|
+
const preStatus = await getGitRepoStatus(repoRoot, {
|
|
1581
|
+
includeSubmodules: true,
|
|
1582
|
+
submoduleIgnorePaths: options.submoduleIgnorePaths,
|
|
1583
|
+
timeoutMs: 15_000,
|
|
1584
|
+
});
|
|
1585
|
+
const outOfSyncPaths = (preStatus.submodules || [])
|
|
1586
|
+
.filter(submodule => submodule.dirty || submodule.outOfSync || !!submodule.error)
|
|
1587
|
+
.map(submodule => submodule.path);
|
|
1588
|
+
const updatePaths = [...new Set([...changedGitlinkPaths, ...outOfSyncPaths])].sort();
|
|
1589
|
+
|
|
1590
|
+
if (updatePaths.length === 0) {
|
|
1591
|
+
return {
|
|
1592
|
+
status: 'skipped',
|
|
1593
|
+
changedGitlinkPaths,
|
|
1594
|
+
outOfSyncPaths,
|
|
1595
|
+
updatedPaths: [],
|
|
1596
|
+
verifiedPaths: [],
|
|
1597
|
+
durationMs: Date.now() - startedAt,
|
|
1598
|
+
reason: 'no_changed_or_out_of_sync_submodules',
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
const commandArgs = ['submodule', 'update', '--init', '--recursive', '--', ...updatePaths];
|
|
1603
|
+
try {
|
|
1604
|
+
const { execFile } = await import('node:child_process');
|
|
1605
|
+
const { promisify } = await import('node:util');
|
|
1606
|
+
const execFileAsync = promisify(execFile);
|
|
1607
|
+
const result = await execFileAsync('git', commandArgs, {
|
|
1608
|
+
cwd: repoRoot,
|
|
1609
|
+
encoding: 'utf8',
|
|
1610
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1611
|
+
timeout: 60_000,
|
|
1612
|
+
});
|
|
1613
|
+
const postStatus = await getGitRepoStatus(repoRoot, {
|
|
1614
|
+
includeSubmodules: true,
|
|
1615
|
+
submoduleIgnorePaths: options.submoduleIgnorePaths,
|
|
1616
|
+
timeoutMs: 15_000,
|
|
1617
|
+
});
|
|
1618
|
+
const remaining = (postStatus.submodules || [])
|
|
1619
|
+
.filter(submodule => updatePaths.includes(submodule.path) && (submodule.dirty || submodule.outOfSync || !!submodule.error));
|
|
1620
|
+
return {
|
|
1621
|
+
status: remaining.length === 0 ? 'passed' : 'failed',
|
|
1622
|
+
changedGitlinkPaths,
|
|
1623
|
+
outOfSyncPaths,
|
|
1624
|
+
updatedPaths: updatePaths,
|
|
1625
|
+
verifiedPaths: updatePaths.filter(path => !remaining.some(submodule => submodule.path === path)),
|
|
1626
|
+
durationMs: Date.now() - startedAt,
|
|
1627
|
+
command: `git ${commandArgs.join(' ')}`,
|
|
1628
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
1629
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
1630
|
+
...(remaining.length > 0 ? { error: `Submodule checkout remained out of sync after update: ${remaining.map(entry => entry.path).join(', ')}` } : {}),
|
|
1631
|
+
};
|
|
1632
|
+
} catch (e: any) {
|
|
1633
|
+
return {
|
|
1634
|
+
status: 'failed',
|
|
1635
|
+
changedGitlinkPaths,
|
|
1636
|
+
outOfSyncPaths,
|
|
1637
|
+
updatedPaths: updatePaths,
|
|
1638
|
+
verifiedPaths: [],
|
|
1639
|
+
durationMs: Date.now() - startedAt,
|
|
1640
|
+
command: `git ${commandArgs.join(' ')}`,
|
|
1641
|
+
error: e?.message || String(e),
|
|
1642
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1643
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1408
1644
|
};
|
|
1409
1645
|
}
|
|
1410
1646
|
}
|
|
@@ -1694,7 +1930,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
1694
1930
|
cwd,
|
|
1695
1931
|
encoding: 'utf8',
|
|
1696
1932
|
timeout,
|
|
1697
|
-
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1933
|
+
maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1698
1934
|
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
1699
1935
|
});
|
|
1700
1936
|
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
@@ -1734,7 +1970,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
1734
1970
|
cwd,
|
|
1735
1971
|
encoding: 'utf8',
|
|
1736
1972
|
timeout,
|
|
1737
|
-
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1973
|
+
maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1738
1974
|
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
1739
1975
|
});
|
|
1740
1976
|
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
@@ -3022,6 +3258,7 @@ export class DaemonCommandRouter {
|
|
|
3022
3258
|
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
3023
3259
|
actualPatchId: patchEquivalence.actualPatchId,
|
|
3024
3260
|
error: patchEquivalence.error,
|
|
3261
|
+
actionableHint: patchEquivalence.actionableHint,
|
|
3025
3262
|
});
|
|
3026
3263
|
if (!patchEquivalence.equivalent) {
|
|
3027
3264
|
return {
|
|
@@ -3187,6 +3424,52 @@ export class DaemonCommandRouter {
|
|
|
3187
3424
|
};
|
|
3188
3425
|
}
|
|
3189
3426
|
|
|
3427
|
+
const submoduleAlignmentStarted = Date.now();
|
|
3428
|
+
const submoduleAlignment = await alignRefinerySubmodulesAfterMerge(repoRoot, baseHead, 'HEAD', {
|
|
3429
|
+
submoduleIgnorePaths: Array.isArray(sourceNode?.policy?.submoduleIgnorePaths)
|
|
3430
|
+
? sourceNode.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
3431
|
+
: undefined,
|
|
3432
|
+
});
|
|
3433
|
+
if (submoduleAlignment.status !== 'skipped') {
|
|
3434
|
+
recordMeshRefineStage(refineStages, 'submodule_alignment', submoduleAlignment.status, submoduleAlignmentStarted, {
|
|
3435
|
+
changedGitlinkPaths: submoduleAlignment.changedGitlinkPaths,
|
|
3436
|
+
outOfSyncPaths: submoduleAlignment.outOfSyncPaths,
|
|
3437
|
+
updatedPaths: submoduleAlignment.updatedPaths,
|
|
3438
|
+
verifiedPaths: submoduleAlignment.verifiedPaths,
|
|
3439
|
+
command: submoduleAlignment.command,
|
|
3440
|
+
error: submoduleAlignment.error,
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
if (submoduleAlignment.status === 'failed') {
|
|
3444
|
+
return {
|
|
3445
|
+
success: false,
|
|
3446
|
+
code: 'post_merge_submodule_alignment_failed',
|
|
3447
|
+
error: 'Refinery merge completed but post-merge submodule checkout alignment failed; run the reported git submodule update command and re-check base workspace status.',
|
|
3448
|
+
merged: true,
|
|
3449
|
+
branch,
|
|
3450
|
+
into: baseBranch,
|
|
3451
|
+
validationSummary,
|
|
3452
|
+
patchEquivalence,
|
|
3453
|
+
submoduleReachability,
|
|
3454
|
+
submoduleAlignment,
|
|
3455
|
+
mergeResult,
|
|
3456
|
+
refineStages,
|
|
3457
|
+
finalBranchConvergenceState: {
|
|
3458
|
+
branch: baseBranch,
|
|
3459
|
+
mergedBranch: branch,
|
|
3460
|
+
baseBranch,
|
|
3461
|
+
merged: true,
|
|
3462
|
+
removed: false,
|
|
3463
|
+
validation: 'passed',
|
|
3464
|
+
patchEquivalence: 'passed',
|
|
3465
|
+
submoduleReachability: 'passed',
|
|
3466
|
+
submoduleAlignment: 'failed',
|
|
3467
|
+
status: 'post_merge_alignment_failed',
|
|
3468
|
+
nextStep: submoduleAlignment.command || 'Run git submodule update --init --recursive for the reported path(s), then re-check base workspace status.',
|
|
3469
|
+
},
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3190
3473
|
const cleanupStarted = Date.now();
|
|
3191
3474
|
const removeResult = await this.execute('remove_mesh_node', {
|
|
3192
3475
|
meshId,
|
|
@@ -3207,7 +3490,7 @@ export class DaemonCommandRouter {
|
|
|
3207
3490
|
appendLedgerEntry(meshId, {
|
|
3208
3491
|
kind: 'node_removed',
|
|
3209
3492
|
nodeId,
|
|
3210
|
-
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence, submoduleReachability },
|
|
3493
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence, submoduleReachability, submoduleAlignment },
|
|
3211
3494
|
});
|
|
3212
3495
|
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
3213
3496
|
} catch (e: any) {
|
|
@@ -3223,6 +3506,7 @@ export class DaemonCommandRouter {
|
|
|
3223
3506
|
removed: removeResult?.success !== false,
|
|
3224
3507
|
validation: 'passed',
|
|
3225
3508
|
patchEquivalence: 'passed',
|
|
3509
|
+
submoduleAlignment: submoduleAlignment.status,
|
|
3226
3510
|
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
3227
3511
|
};
|
|
3228
3512
|
|
|
@@ -3238,6 +3522,7 @@ export class DaemonCommandRouter {
|
|
|
3238
3522
|
validationSummary,
|
|
3239
3523
|
patchEquivalence,
|
|
3240
3524
|
submoduleReachability,
|
|
3525
|
+
submoduleAlignment,
|
|
3241
3526
|
mergeResult,
|
|
3242
3527
|
refineStages,
|
|
3243
3528
|
...(ledgerError ? { ledgerError } : {}),
|
|
@@ -3254,6 +3539,7 @@ export class DaemonCommandRouter {
|
|
|
3254
3539
|
validationSummary,
|
|
3255
3540
|
patchEquivalence,
|
|
3256
3541
|
submoduleReachability,
|
|
3542
|
+
submoduleAlignment,
|
|
3257
3543
|
mergeResult,
|
|
3258
3544
|
refineStages,
|
|
3259
3545
|
...(ledgerError ? { ledgerError } : {}),
|
|
@@ -4465,6 +4751,12 @@ export class DaemonCommandRouter {
|
|
|
4465
4751
|
success: true,
|
|
4466
4752
|
schema: MESH_REFINE_CONFIG_SCHEMA,
|
|
4467
4753
|
locations: MESH_REFINE_CONFIG_LOCATIONS,
|
|
4754
|
+
worktreeBootstrap: {
|
|
4755
|
+
schema: MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
|
|
4756
|
+
locations: MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
|
|
4757
|
+
sourceOfTruth: 'repo worktree bootstrap config',
|
|
4758
|
+
runBehavior: 'When present and enabled, clone_mesh_node runs commands after submodule initialization and records status on the worktree node.',
|
|
4759
|
+
},
|
|
4468
4760
|
sourceOfTruth: 'repo mesh/refine config',
|
|
4469
4761
|
heuristicRole: 'suggestions_only_not_execution_path',
|
|
4470
4762
|
};
|
|
@@ -4695,13 +4987,36 @@ export class DaemonCommandRouter {
|
|
|
4695
4987
|
}
|
|
4696
4988
|
}
|
|
4697
4989
|
|
|
4990
|
+
const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
|
|
4991
|
+
node.worktreeBootstrap = bootstrapState;
|
|
4992
|
+
if (!meshRecord.inline) {
|
|
4993
|
+
try {
|
|
4994
|
+
const { updateNode } = await import('../config/mesh-config.js');
|
|
4995
|
+
updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
|
|
4996
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4997
|
+
} catch { /* bootstrap status persistence is best-effort */ }
|
|
4998
|
+
}
|
|
4999
|
+
|
|
4698
5000
|
// Record in task ledger
|
|
4699
5001
|
try {
|
|
4700
5002
|
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
4701
5003
|
appendLedgerEntry(meshId, {
|
|
4702
5004
|
kind: 'node_cloned',
|
|
4703
5005
|
nodeId: node.id,
|
|
4704
|
-
payload: {
|
|
5006
|
+
payload: {
|
|
5007
|
+
sourceNodeId,
|
|
5008
|
+
branch: result.branch,
|
|
5009
|
+
worktreePath: result.worktreePath,
|
|
5010
|
+
submodulesInitialized: initSubmodules,
|
|
5011
|
+
worktreeBootstrap: {
|
|
5012
|
+
status: bootstrapState.status,
|
|
5013
|
+
required: bootstrapState.required,
|
|
5014
|
+
configSource: bootstrapState.configSource,
|
|
5015
|
+
configSourceType: bootstrapState.configSourceType,
|
|
5016
|
+
lastCommand: bootstrapState.lastCommand,
|
|
5017
|
+
exitCode: bootstrapState.exitCode,
|
|
5018
|
+
},
|
|
5019
|
+
},
|
|
4705
5020
|
});
|
|
4706
5021
|
} catch { /* ledger append is best-effort */ }
|
|
4707
5022
|
|
|
@@ -4710,6 +5025,7 @@ export class DaemonCommandRouter {
|
|
|
4710
5025
|
node,
|
|
4711
5026
|
worktreePath: result.worktreePath,
|
|
4712
5027
|
branch: result.branch,
|
|
5028
|
+
worktreeBootstrap: bootstrapState,
|
|
4713
5029
|
};
|
|
4714
5030
|
} catch (e: any) {
|
|
4715
5031
|
return { success: false, error: e.message };
|
|
@@ -5164,6 +5480,7 @@ export class DaemonCommandRouter {
|
|
|
5164
5480
|
|
|
5165
5481
|
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
5166
5482
|
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
5483
|
+
const asyncRefineLedgerEntries = readLedgerEntries(meshId, { tail: 100 });
|
|
5167
5484
|
const ledgerSummary = getLedgerSummary(meshId);
|
|
5168
5485
|
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
5169
5486
|
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
@@ -5473,6 +5790,22 @@ export class DaemonCommandRouter {
|
|
|
5473
5790
|
}
|
|
5474
5791
|
|
|
5475
5792
|
const pendingCoordinatorEvents = drainPendingMeshCoordinatorEvents(meshId);
|
|
5793
|
+
const previewFreshness = (() => {
|
|
5794
|
+
const localRepoRoot = nodeStatuses
|
|
5795
|
+
.map((node: any) => readStringValue(node?.git?.repoRoot, node?.repoRoot, node?.workspace))
|
|
5796
|
+
.find((candidate: string | undefined) => !!candidate && fs.existsSync(candidate));
|
|
5797
|
+
return localRepoRoot ? buildPreviewFreshness(localRepoRoot) : undefined;
|
|
5798
|
+
})();
|
|
5799
|
+
const asyncRefineJobs = buildMeshAsyncRefineJobs({
|
|
5800
|
+
meshId,
|
|
5801
|
+
ledgerEntries: asyncRefineLedgerEntries,
|
|
5802
|
+
pendingEvents: pendingCoordinatorEvents,
|
|
5803
|
+
});
|
|
5804
|
+
const historicalSessions = buildHistoricalMeshSessions({
|
|
5805
|
+
meshId,
|
|
5806
|
+
nodes: mesh.nodes || [],
|
|
5807
|
+
liveSessionRecords: liveMeshSessions,
|
|
5808
|
+
});
|
|
5476
5809
|
const statusResult = {
|
|
5477
5810
|
success: true,
|
|
5478
5811
|
meshId: mesh.id,
|
|
@@ -5508,12 +5841,15 @@ export class DaemonCommandRouter {
|
|
|
5508
5841
|
partialNodeFailures: effectiveDirectTruth.unavailableNodeIds,
|
|
5509
5842
|
},
|
|
5510
5843
|
} : {}),
|
|
5511
|
-
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary'],
|
|
5844
|
+
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary', 'historicalSessions'],
|
|
5512
5845
|
},
|
|
5513
5846
|
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
|
|
5847
|
+
...(previewFreshness ? { previewFreshness, deployFreshness: previewFreshness } : {}),
|
|
5514
5848
|
nodes: nodeStatuses,
|
|
5515
5849
|
queue: { tasks: queue, summary: queueSummary },
|
|
5516
5850
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
5851
|
+
...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
|
|
5852
|
+
...(historicalSessions ? { historicalSessions } : {}),
|
|
5517
5853
|
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
5518
5854
|
};
|
|
5519
5855
|
const { pendingCoordinatorEvents: _pendingCoordinatorEvents, ...cacheableStatusResult } = statusResult as any;
|
|
@@ -428,6 +428,7 @@ export interface AddNodeOptions {
|
|
|
428
428
|
isLocalWorktree?: boolean;
|
|
429
429
|
worktreeBranch?: string;
|
|
430
430
|
clonedFromNodeId?: string;
|
|
431
|
+
worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
|
|
431
432
|
role?: RepoMeshDaemonRole;
|
|
432
433
|
}
|
|
433
434
|
|
|
@@ -456,6 +457,7 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
|
|
|
456
457
|
isLocalWorktree: opts.isLocalWorktree,
|
|
457
458
|
worktreeBranch: opts.worktreeBranch,
|
|
458
459
|
clonedFromNodeId: opts.clonedFromNodeId,
|
|
460
|
+
worktreeBootstrap: opts.worktreeBootstrap,
|
|
459
461
|
role: opts.role,
|
|
460
462
|
};
|
|
461
463
|
|
|
@@ -482,7 +484,7 @@ export function removeNode(meshId: string, nodeId: string): boolean {
|
|
|
482
484
|
export function updateNode(
|
|
483
485
|
meshId: string,
|
|
484
486
|
nodeId: string,
|
|
485
|
-
opts: { userOverrides?: Partial<RepoMeshNodeCapabilities>; policy?: RepoMeshNodePolicy },
|
|
487
|
+
opts: { userOverrides?: Partial<RepoMeshNodeCapabilities>; policy?: RepoMeshNodePolicy; worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'] },
|
|
486
488
|
): LocalMeshNodeEntry | undefined {
|
|
487
489
|
const config = loadMeshConfig();
|
|
488
490
|
const mesh = config.meshes.find(m => m.id === meshId);
|
|
@@ -493,6 +495,7 @@ export function updateNode(
|
|
|
493
495
|
|
|
494
496
|
if (opts.userOverrides) node.userOverrides = { ...node.userOverrides, ...opts.userOverrides };
|
|
495
497
|
if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
|
|
498
|
+
if (opts.worktreeBootstrap) node.worktreeBootstrap = opts.worktreeBootstrap;
|
|
496
499
|
mesh.updatedAt = new Date().toISOString();
|
|
497
500
|
saveMeshConfig(config);
|
|
498
501
|
return node;
|
package/src/git/git-commands.ts
CHANGED
|
@@ -42,8 +42,12 @@ export interface GitLogResult extends GitRepoIdentity {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export interface GitCheckpointResult extends GitRepoIdentity {
|
|
45
|
-
commit
|
|
45
|
+
commit?: string;
|
|
46
46
|
message: string;
|
|
47
|
+
status?: 'created' | 'skipped';
|
|
48
|
+
skipped?: boolean;
|
|
49
|
+
noop?: boolean;
|
|
50
|
+
reason?: 'nothing_to_commit';
|
|
47
51
|
lastCheckedAt: number;
|
|
48
52
|
}
|
|
49
53
|
|
|
@@ -455,10 +459,17 @@ async function gitCheckpoint(
|
|
|
455
459
|
} catch (err: any) {
|
|
456
460
|
const output = (err?.stdout || '') + (err?.stderr || '');
|
|
457
461
|
if (/nothing to commit/i.test(output)) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
+
return {
|
|
463
|
+
workspace: repo.workspace,
|
|
464
|
+
repoRoot,
|
|
465
|
+
isGitRepo: true,
|
|
466
|
+
message: fullMsg,
|
|
467
|
+
status: 'skipped',
|
|
468
|
+
skipped: true,
|
|
469
|
+
noop: true,
|
|
470
|
+
reason: 'nothing_to_commit',
|
|
471
|
+
lastCheckedAt: Date.now(),
|
|
472
|
+
};
|
|
462
473
|
}
|
|
463
474
|
throw err;
|
|
464
475
|
}
|
|
@@ -469,6 +480,7 @@ async function gitCheckpoint(
|
|
|
469
480
|
isGitRepo: true,
|
|
470
481
|
commit: commitSha,
|
|
471
482
|
message: fullMsg,
|
|
483
|
+
status: 'created',
|
|
472
484
|
lastCheckedAt: Date.now(),
|
|
473
485
|
};
|
|
474
486
|
}
|
package/src/index.ts
CHANGED
|
@@ -167,6 +167,15 @@ export {
|
|
|
167
167
|
suggestMeshRefineConfig,
|
|
168
168
|
validateMeshRefineConfig,
|
|
169
169
|
} from './mesh/refine-config.js';
|
|
170
|
+
export {
|
|
171
|
+
MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
|
|
172
|
+
MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
|
|
173
|
+
loadMeshWorktreeBootstrapConfig,
|
|
174
|
+
runMeshWorktreeBootstrap,
|
|
175
|
+
validateMeshWorktreeBootstrapConfig,
|
|
176
|
+
type RepoMeshWorktreeBootstrapConfig,
|
|
177
|
+
type WorktreeBootstrapState,
|
|
178
|
+
} from './mesh/worktree-bootstrap-config.js';
|
|
170
179
|
export type {
|
|
171
180
|
MeshRefineValidationCategory,
|
|
172
181
|
MeshRefineValidationCommandPlan,
|
|
@@ -188,8 +197,10 @@ export type { MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshL
|
|
|
188
197
|
// ── Mesh Work Queue (GUPP) ──
|
|
189
198
|
export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest } from './mesh/mesh-work-queue.js';
|
|
190
199
|
export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult } from './mesh/mesh-work-queue.js';
|
|
191
|
-
export { buildMeshActiveWork, buildMeshActiveWorkSummary } from './mesh/mesh-active-work.js';
|
|
192
|
-
export type { MeshActiveWorkRecord, MeshActiveWorkStatus, MeshActiveWorkSummary, MeshActiveWorkSource } from './mesh/mesh-active-work.js';
|
|
200
|
+
export { buildCompactStaleDirectWorkSummary, buildMeshActiveWork, buildMeshActiveWorkSummary } from './mesh/mesh-active-work.js';
|
|
201
|
+
export type { MeshActiveWorkRecord, MeshActiveWorkStatus, MeshActiveWorkSummary, MeshActiveWorkSource, MeshStaleDirectWorkSummary } from './mesh/mesh-active-work.js';
|
|
202
|
+
export { buildMeshAsyncRefineJobs } from './mesh/mesh-refine-status.js';
|
|
203
|
+
export type { MeshAsyncRefineJobStatus, MeshAsyncRefineJobSummary } from './mesh/mesh-refine-status.js';
|
|
193
204
|
|
|
194
205
|
// ── Mesh Host Ownership ──
|
|
195
206
|
export { buildMeshHostRequiredFailure, createDefaultMeshHostMetadata, isMeshHostOwner, normalizeMeshDaemonRole, requireMeshHostQueueOwner, resolveMeshHostStatus } from './mesh/mesh-host-ownership.js';
|
|
@@ -45,6 +45,15 @@ export interface MeshActiveWorkSummary {
|
|
|
45
45
|
staleDirectNote?: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export interface MeshStaleDirectWorkSummary {
|
|
49
|
+
count: number;
|
|
50
|
+
sampleLimit: number;
|
|
51
|
+
sample: Array<Pick<MeshActiveWorkRecord, 'taskId' | 'status' | 'nodeId' | 'sessionId' | 'taskTitle' | 'createdAt' | 'staleReason'>>;
|
|
52
|
+
reasonCounts: Record<string, number>;
|
|
53
|
+
detailHint: string;
|
|
54
|
+
note?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
export interface BuildMeshActiveWorkOptions {
|
|
49
58
|
meshId: string;
|
|
50
59
|
queue?: MeshWorkQueueEntry[];
|
|
@@ -253,3 +262,31 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
253
262
|
}
|
|
254
263
|
return { activeWork: records, staleDirectWork, staleDirectWorkNote, terminalDirectWork, summary };
|
|
255
264
|
}
|
|
265
|
+
|
|
266
|
+
export function buildCompactStaleDirectWorkSummary(
|
|
267
|
+
staleDirectWork: MeshActiveWorkRecord[],
|
|
268
|
+
opts: { sampleLimit?: number; detailHint?: string; note?: string } = {},
|
|
269
|
+
): MeshStaleDirectWorkSummary {
|
|
270
|
+
const sampleLimit = Math.max(0, Math.min(10, Math.floor(opts.sampleLimit ?? 3)));
|
|
271
|
+
const reasonCounts: Record<string, number> = {};
|
|
272
|
+
for (const entry of staleDirectWork) {
|
|
273
|
+
const reason = entry.staleReason || 'unknown';
|
|
274
|
+
reasonCounts[reason] = (reasonCounts[reason] || 0) + 1;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
count: staleDirectWork.length,
|
|
278
|
+
sampleLimit,
|
|
279
|
+
sample: staleDirectWork.slice(0, sampleLimit).map(entry => ({
|
|
280
|
+
taskId: entry.taskId,
|
|
281
|
+
status: entry.status,
|
|
282
|
+
nodeId: entry.nodeId,
|
|
283
|
+
sessionId: entry.sessionId,
|
|
284
|
+
taskTitle: entry.taskTitle,
|
|
285
|
+
createdAt: entry.createdAt,
|
|
286
|
+
staleReason: entry.staleReason,
|
|
287
|
+
})),
|
|
288
|
+
reasonCounts,
|
|
289
|
+
detailHint: opts.detailHint || 'Stale direct records are historical recovery evidence only. Use mesh_task_history for full ledger details, or request includeStaleDirectWorkDetails when supported by the caller.',
|
|
290
|
+
...(opts.note ? { note: opts.note } : {}),
|
|
291
|
+
};
|
|
292
|
+
}
|