@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.
@@ -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: { sourceNodeId, branch: result.branch, worktreePath: result.worktreePath, submodulesInitialized: initSubmodules },
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;
@@ -42,8 +42,12 @@ export interface GitLogResult extends GitRepoIdentity {
42
42
  }
43
43
 
44
44
  export interface GitCheckpointResult extends GitRepoIdentity {
45
- commit: string;
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
- throw new GitCommandError('nothing_to_commit', 'Nothing to commit — working tree is clean.', {
459
- stdout: err?.stdout,
460
- stderr: err?.stderr,
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
+ }