@adhdev/daemon-core 0.9.82-rc.77 → 0.9.82-rc.79

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.
@@ -20,6 +20,7 @@ export interface MeshActiveWorkRecord {
20
20
  terminal?: boolean;
21
21
  terminalKind?: string;
22
22
  terminalAt?: string;
23
+ staleReason?: string;
23
24
  }
24
25
  export interface MeshActiveWorkSummary {
25
26
  totalActiveCount: number;
@@ -31,6 +32,7 @@ export interface MeshActiveWorkSummary {
31
32
  idleCount: number;
32
33
  sourceCounts: Record<MeshActiveWorkSource, number>;
33
34
  statusCounts: Record<MeshActiveWorkStatus, number>;
35
+ staleDirectCount: number;
34
36
  }
35
37
  export interface BuildMeshActiveWorkOptions {
36
38
  meshId: string;
@@ -44,5 +46,6 @@ export interface BuildMeshActiveWorkOptions {
44
46
  export declare function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]): MeshActiveWorkSummary;
45
47
  export declare function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): {
46
48
  activeWork: MeshActiveWorkRecord[];
49
+ staleDirectWork: MeshActiveWorkRecord[];
47
50
  summary: MeshActiveWorkSummary;
48
51
  };
@@ -6,6 +6,7 @@ export interface PendingMeshCoordinatorEvent {
6
6
  nodeId?: string;
7
7
  workspace?: string;
8
8
  metadataEvent: Record<string, unknown>;
9
+ coordinatorMessage?: string;
9
10
  queuedAt: number;
10
11
  }
11
12
  export declare function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean;
@@ -104,7 +104,7 @@ export declare class CliProviderInstance implements ProviderInstance {
104
104
  private buildCompletedFinalizationDiagnostic;
105
105
  private hasAdapterPendingResponse;
106
106
  private shouldSuppressStaleParsedBusyStatus;
107
- private getCompletedFinalizationBlockReason;
107
+ private getCompletedFinalizationBlock;
108
108
  private scheduleCompletedDebounceFlush;
109
109
  private flushCompletedDebounceIfFinalized;
110
110
  private maybeAutoApproveStatus;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.77",
3
+ "version": "0.9.82-rc.79",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -86,6 +86,50 @@ function normalizeAgentStatus(value: unknown): string {
86
86
  return typeof value === 'string' ? value.trim().toLowerCase() : '';
87
87
  }
88
88
 
89
+ function hasNonEmptyModalButtons(activeModal: unknown): boolean {
90
+ const buttons = (activeModal as any)?.buttons;
91
+ return Array.isArray(buttons) && buttons.some((button) => String(button || '').trim().length > 0);
92
+ }
93
+
94
+ function hasAdapterPendingResponse(adapter: any): boolean {
95
+ if (adapter?.isWaitingForResponse === true) return true;
96
+ if (adapter?.currentTurnScope) return true;
97
+ try {
98
+ if (typeof adapter?.isProcessing === 'function' && adapter.isProcessing()) return true;
99
+ } catch { /* defensive: send guard should not fail on diagnostics */ }
100
+ try {
101
+ const partial = typeof adapter?.getPartialResponse === 'function' ? adapter.getPartialResponse() : '';
102
+ if (typeof partial === 'string' && partial.trim()) return true;
103
+ } catch { /* defensive: missing partial means no pending evidence */ }
104
+ return false;
105
+ }
106
+
107
+ function shouldSuppressStaleParsedBusyStatus(adapterStatus: string, parsedStatus: any, adapter: any): boolean {
108
+ const parsedRawStatus = normalizeAgentStatus(parsedStatus?.status);
109
+ if (!BUSY_AGENT_STATUSES.has(parsedRawStatus)) return false;
110
+ if (adapterStatus !== 'idle') return false;
111
+ if (hasNonEmptyModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
112
+ return !hasAdapterPendingResponse(adapter);
113
+ }
114
+
115
+ function getEffectiveAgentSendStatus(adapter: any): string {
116
+ const adapterStatus = normalizeAgentStatus(adapter?.getStatus?.({ allowParse: false })?.status ?? adapter?.getStatus?.()?.status);
117
+ if (adapterStatus && adapterStatus !== 'idle') return adapterStatus;
118
+ if (adapterStatus !== 'idle') return adapterStatus;
119
+
120
+ if (typeof adapter?.getScriptParsedStatus !== 'function') return adapterStatus;
121
+ try {
122
+ const parsedStatus = adapter.getScriptParsedStatus();
123
+ const parsedRawStatus = normalizeAgentStatus(parsedStatus?.status);
124
+ if (BUSY_AGENT_STATUSES.has(parsedRawStatus) && !shouldSuppressStaleParsedBusyStatus(adapterStatus, parsedStatus, adapter)) {
125
+ return parsedRawStatus;
126
+ }
127
+ } catch {
128
+ return adapterStatus;
129
+ }
130
+ return adapterStatus;
131
+ }
132
+
89
133
  export interface CliTransportFactoryParams {
90
134
  runtimeId: string;
91
135
  providerType: string;
@@ -1079,7 +1123,7 @@ export class DaemonCliManager {
1079
1123
  const { adapter, key } = found;
1080
1124
 
1081
1125
  if (action === 'send_chat') {
1082
- const currentStatus = normalizeAgentStatus(adapter.getStatus?.()?.status);
1126
+ const currentStatus = getEffectiveAgentSendStatus(adapter);
1083
1127
  if (BUSY_AGENT_STATUSES.has(currentStatus)) {
1084
1128
  return {
1085
1129
  success: false,
@@ -55,8 +55,8 @@ import { getSessionCompletionMarker } from '../status/snapshot.js';
55
55
  import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
56
56
  import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
57
57
  import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
58
- import { homedir, tmpdir } from 'os';
59
- import { join as pathJoin, resolve as pathResolve } from 'path';
58
+ import { homedir } from 'os';
59
+ import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
60
60
  import * as fs from 'fs';
61
61
 
62
62
  type ReleaseChannel = 'stable' | 'preview';
@@ -230,6 +230,18 @@ function readGitSubmodules(value: unknown, parentRepoRoot?: string): GitSubmodul
230
230
  return submodules.length > 0 ? submodules : undefined;
231
231
  }
232
232
 
233
+ function buildMeshNodeDisplayLabel(node: Record<string, unknown>, nodeId: string, providerPriority: string[]): string {
234
+ const explicit = readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias);
235
+ if (explicit) return explicit;
236
+ const workspace = readStringValue(node.workspace, node.repoRoot, node.repo_root);
237
+ const workspaceName = workspace ? pathBasename(workspace) : undefined;
238
+ const host = readStringValue(node.hostname, node.host, node.daemonId, node.daemon_id, node.machineId, node.machine_id);
239
+ const provider = providerPriority[0] || (Array.isArray(node.providers) ? readStringValue(...node.providers) : undefined);
240
+ const parts = [workspaceName, host, provider].filter(Boolean);
241
+ if (parts.length > 0) return parts.join(' · ');
242
+ return nodeId || 'unidentified mesh node';
243
+ }
244
+
233
245
  function normalizeInlineMeshGitStatus(
234
246
  status: Record<string, unknown>,
235
247
  node: any,
@@ -1095,6 +1107,8 @@ type MeshRefineSubmoduleReachabilityEntry = {
1095
1107
  remote?: string;
1096
1108
  remoteUrl?: string;
1097
1109
  remoteReachable?: boolean;
1110
+ remoteMainBranch?: string;
1111
+ remoteMainReachable?: boolean;
1098
1112
  fetchedFromOrigin?: boolean;
1099
1113
  error?: string;
1100
1114
  };
@@ -1170,7 +1184,7 @@ function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReach
1170
1184
  const refs = entries
1171
1185
  .map(entry => `${entry.path}@${entry.commit}`)
1172
1186
  .join(', ');
1173
- return `Ask the user for explicit approval to push/publish the unreachable submodule commit(s) (${refs}) to their configured submodule remote(s), then rerun mesh_refine_node. Do not merge the root branch until every submodule gitlink commit is reachable from its configured remote.`;
1187
+ 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.`;
1174
1188
  }
1175
1189
 
1176
1190
  async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
@@ -1267,15 +1281,9 @@ async function runMeshRefineSubmoduleReachabilityGate(
1267
1281
  });
1268
1282
  return String(stdout || '');
1269
1283
  };
1270
- const verifyRemoteCommitReachable = async (remoteUrl: string, commit: string): Promise<void> => {
1271
- const probeDir = fs.mkdtempSync(pathJoin(tmpdir(), 'adhdev-submodule-reachability-'));
1272
- try {
1273
- await runGit(probeDir, ['init', '-q']);
1274
- await runGit(probeDir, ['-c', 'protocol.file.allow=always', 'fetch', '--depth=1', remoteUrl, commit]);
1275
- await runGit(probeDir, ['cat-file', '-e', `${commit}^{commit}`]);
1276
- } finally {
1277
- fs.rmSync(probeDir, { recursive: true, force: true });
1278
- }
1284
+ const verifyRemoteMainContainsCommit = async (submodulePath: string, commit: string, branch = 'main'): Promise<void> => {
1285
+ await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
1286
+ await runGit(submodulePath, ['merge-base', '--is-ancestor', commit, `refs/remotes/origin/${branch}`]);
1279
1287
  };
1280
1288
 
1281
1289
  const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
@@ -1325,15 +1333,18 @@ async function runMeshRefineSubmoduleReachabilityGate(
1325
1333
  entries.push(entry);
1326
1334
  continue;
1327
1335
  }
1328
- await verifyRemoteCommitReachable(remoteUrl, gitlink.commit);
1336
+ entry.remoteMainBranch = 'main';
1337
+ await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
1329
1338
  entry.fetchedFromOrigin = true;
1330
1339
  entry.remoteReachable = true;
1340
+ entry.remoteMainReachable = true;
1331
1341
  entry.reachable = true;
1332
1342
  } catch (e: any) {
1333
1343
  entry.remoteReachable = false;
1344
+ entry.remoteMainReachable = false;
1334
1345
  entry.publishRequired = true;
1335
1346
  const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
1336
- entry.error = `Submodule remote reachability check failed for origin: ${details}`;
1347
+ entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
1337
1348
  }
1338
1349
  } catch (e: any) {
1339
1350
  entry.error = truncateValidationOutput(e?.message || String(e));
@@ -2540,28 +2551,51 @@ export class DaemonCommandRouter {
2540
2551
  }
2541
2552
 
2542
2553
  private queueRefineJobEvent(event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
2543
- queuePendingMeshCoordinatorEvent({
2554
+ const metadataEvent = {
2555
+ source: 'refine_mesh_node_async_job',
2556
+ jobId: handle.jobId,
2557
+ interactionId: handle.interactionId,
2558
+ meshId: handle.meshId,
2559
+ nodeId: handle.targetNodeId,
2560
+ targetDaemonId: handle.targetDaemonId,
2561
+ workspace: handle.workspace,
2562
+ status: handle.status,
2563
+ startedAt: handle.startedAt,
2564
+ completedAt: handle.completedAt,
2565
+ retryOfJobId: handle.retryOfJobId,
2566
+ ...(result ? { result } : {}),
2567
+ };
2568
+ const eventPayload = {
2544
2569
  event,
2545
2570
  meshId: handle.meshId,
2546
2571
  nodeLabel: handle.targetNodeId,
2547
2572
  nodeId: handle.targetNodeId,
2548
2573
  workspace: handle.workspace,
2549
- metadataEvent: {
2550
- source: 'refine_mesh_node_async_job',
2551
- jobId: handle.jobId,
2552
- interactionId: handle.interactionId,
2553
- meshId: handle.meshId,
2554
- nodeId: handle.targetNodeId,
2555
- targetDaemonId: handle.targetDaemonId,
2556
- workspace: handle.workspace,
2557
- status: handle.status,
2558
- startedAt: handle.startedAt,
2559
- completedAt: handle.completedAt,
2560
- retryOfJobId: handle.retryOfJobId,
2561
- ...(result ? { result } : {}),
2562
- },
2574
+ metadataEvent,
2563
2575
  queuedAt: Date.now(),
2564
- });
2576
+ };
2577
+ if (typeof this.deps.instanceManager?.getByCategory === 'function') {
2578
+ const forwarded = handleMeshForwardEvent(
2579
+ { instanceManager: this.deps.instanceManager } as any,
2580
+ {
2581
+ event,
2582
+ meshId: handle.meshId,
2583
+ nodeId: handle.targetNodeId,
2584
+ workspace: handle.workspace,
2585
+ jobId: handle.jobId,
2586
+ interactionId: handle.interactionId,
2587
+ status: handle.status,
2588
+ targetDaemonId: handle.targetDaemonId,
2589
+ startedAt: handle.startedAt,
2590
+ completedAt: handle.completedAt,
2591
+ retryOfJobId: handle.retryOfJobId,
2592
+ ...(result ? { result } : {}),
2593
+ },
2594
+ );
2595
+ if (forwarded?.success === true) return;
2596
+ LOG.warn('Mesh', `[Refinery] Failed to forward async refine event ${event}: ${forwarded?.error || 'unknown error'}`);
2597
+ }
2598
+ queuePendingMeshCoordinatorEvent(eventPayload);
2565
2599
  }
2566
2600
 
2567
2601
  private async appendRefineJobLedger(kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
@@ -2725,6 +2759,8 @@ export class DaemonCommandRouter {
2725
2759
  remote: entry.remote,
2726
2760
  remoteUrl: entry.remoteUrl,
2727
2761
  remoteReachable: entry.remoteReachable,
2762
+ remoteMainBranch: entry.remoteMainBranch,
2763
+ remoteMainReachable: entry.remoteMainReachable,
2728
2764
  error: entry.error,
2729
2765
  })),
2730
2766
  error: submoduleReachability.error,
@@ -2737,13 +2773,13 @@ export class DaemonCommandRouter {
2737
2773
  convergenceStatus: 'blocked_review',
2738
2774
  publishRequired: true,
2739
2775
  blockedReason: 'submodule_publish_required',
2740
- error: 'Refinery submodule reachability preflight failed because one or more submodule gitlink commits are not reachable from their configured remote; merge/refine cleanup was not attempted.',
2776
+ error: 'Refinery submodule reachability preflight failed because one or more submodule gitlink commits are not reachable from their configured remote main branch; merge/refine cleanup was not attempted.',
2741
2777
  nextStep,
2742
2778
  nextSteps: [
2743
2779
  'Ask the user for explicit approval before pushing or publishing any submodule commit.',
2744
- 'Push/publish each unreachable submodule commit to the configured submodule remote shown in the evidence.',
2780
+ 'Push/publish each unreachable submodule commit to the configured submodule remote main branch shown in the evidence.',
2745
2781
  'Rerun mesh_refine_node after remote reachability is confirmed.',
2746
- 'Do not merge the root branch until every submodule gitlink commit is reachable from its configured remote.',
2782
+ 'Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.',
2747
2783
  ],
2748
2784
  unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
2749
2785
  path: entry.path,
@@ -2751,6 +2787,8 @@ export class DaemonCommandRouter {
2751
2787
  remote: entry.remote,
2752
2788
  remoteUrl: entry.remoteUrl,
2753
2789
  remoteReachable: entry.remoteReachable,
2790
+ remoteMainBranch: entry.remoteMainBranch,
2791
+ remoteMainReachable: entry.remoteMainReachable,
2754
2792
  error: entry.error,
2755
2793
  })),
2756
2794
  branch,
@@ -4869,7 +4907,10 @@ export class DaemonCommandRouter {
4869
4907
  ) || Boolean(meshRecord?.inline && nodeIndex === 0);
4870
4908
  const status: Record<string, unknown> = {
4871
4909
  nodeId,
4872
- machineLabel: node.machineLabel || node.id || node.nodeId,
4910
+ machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
4911
+ labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
4912
+ ? 'explicit_metadata'
4913
+ : 'workspace_host_provider_context',
4873
4914
  workspace: node.workspace,
4874
4915
  repoRoot: node.repoRoot,
4875
4916
  isLocalWorktree: node.isLocalWorktree,
@@ -76,7 +76,12 @@ Repository: \`${mesh.repoIdentity}\`${mesh.defaultBranch ? `\nDefault branch: \`
76
76
  // ─── Section Builders ───────────────────────────
77
77
 
78
78
  function buildNodeStatusSection(nodes: RepoMeshNodeStatus[]): string {
79
- const lines = ['## Current Node Status', ''];
79
+ const lines = [
80
+ '## Current Node Status',
81
+ '',
82
+ 'Node labels are display context, not aliases. Use exact `nodeId` values in mesh tool calls; do not invent shorthand names such as M1/M2 unless they are explicitly configured labels.',
83
+ '',
84
+ ];
80
85
  for (const n of nodes) {
81
86
  const healthIcon = n.health === 'online' ? '🟢' :
82
87
  n.health === 'dirty' ? '🟡' :
@@ -85,21 +90,33 @@ function buildNodeStatusSection(nodes: RepoMeshNodeStatus[]): string {
85
90
  ? `sessions: ${n.activeSessions.join(', ')}`
86
91
  : 'no active sessions';
87
92
  const branch = n.git?.branch ? `branch: \`${n.git.branch}\`` : '';
88
- lines.push(`- ${healthIcon} **${n.machineLabel}** (${n.nodeId})`);
89
- lines.push(` workspace: \`${n.workspace}\` | ${branch} | ${sessions}`);
93
+ const context = [
94
+ n.daemonId ? `daemon: \`${n.daemonId}\`` : '',
95
+ n.providers?.length ? `providers: ${n.providers.join(', ')}` : '',
96
+ ].filter(Boolean).join(' | ');
97
+ lines.push(`- ${healthIcon} **${n.machineLabel}** (nodeId: \`${n.nodeId}\`)`);
98
+ lines.push(` workspace: \`${n.workspace}\`${context ? ` | ${context}` : ''} | ${branch} | ${sessions}`);
90
99
  if (n.error) lines.push(` ⚠️ ${n.error}`);
91
100
  }
92
101
  return lines.join('\n');
93
102
  }
94
103
 
95
104
  function buildNodeConfigSection(mesh: LocalMeshEntry): string {
96
- const lines = ['## Configured Nodes', ''];
105
+ const lines = [
106
+ '## Configured Nodes',
107
+ '',
108
+ 'Node labels are display context, not aliases. Use exact `nodeId` values in mesh tool calls; do not invent shorthand names such as M1/M2 unless they are explicitly configured labels.',
109
+ '',
110
+ ];
97
111
  for (const n of mesh.nodes) {
98
112
  const labels: string[] = [];
99
113
  if (n.isLocalWorktree) labels.push('worktree');
100
114
  if (n.policy?.readOnly) labels.push('read-only');
101
115
  const suffix = labels.length ? ` [${labels.join(', ')}]` : '';
102
- lines.push(`- **${n.workspace}** (${n.id})${suffix}`);
116
+ const explicitMachineLabel = typeof (n as any).machineLabel === 'string' ? (n as any).machineLabel : '';
117
+ const explicitLabel = explicitMachineLabel ? ` label: **${explicitMachineLabel}** |` : '';
118
+ const providerPriority = n.policy?.providerPriority?.length ? ` | providers: ${n.policy.providerPriority.join(', ')}` : '';
119
+ lines.push(`- ${explicitLabel} nodeId: \`${n.id}\` | workspace: \`${n.workspace}\`${n.daemonId ? ` | daemon: \`${n.daemonId}\`` : ''}${providerPriority}${suffix}`);
103
120
  }
104
121
  lines.push('', '_Use `mesh_status` to probe live health before delegating work._');
105
122
  return lines.join('\n');
@@ -165,7 +182,7 @@ const WORKFLOW_SECTION = `## Orchestration Workflow
165
182
  4. **Monitor** — Prefer event-driven completion/status notifications. Do **not** poll \`mesh_read_chat\` repeatedly. Use \`mesh_view_queue\` to see the status of all pending, assigned, completed, and failed tasks. Do not call \`mesh_read_chat\` again within a few seconds for the same generating session. Use at most one compact \`mesh_read_chat\` check after a completion/approval signal. Handle approvals via \`mesh_approve\`.
166
183
  5. **Verify** — When a task reports completion or git work is visible, call \`mesh_git_status\` to verify changes were made.
167
184
  6. **Checkpoint** — Call \`mesh_checkpoint\` to save the work.
168
- 7. **Converge branches** — Before marking any task complete, classify every touched node/branch into exactly one final state: \`merged_to_main\`, \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\`. Use \`mesh_status\` branchConvergenceSummary. For obvious clean branch catch-up (ahead 0, behind > 0, upstream fresh, no dirty/stash/submodule issues), use \`mesh_fast_forward_node\` dry-run first and execute only when explicitly safe/approved; this avoids consuming an agent session. Use \`mesh_refine_node\` for clean worktree branches when safe. Before/refine merging root commits that contain submodule gitlink changes, require each submodule commit to be reachable from its configured remote. If \`mesh_refine_node\` returns \`submodule_reachability_failed\` or publish-required evidence, keep the public convergence bucket as \`blocked_review\`, ask the user for explicit approval to push/publish the unreachable submodule commit(s), then rerun \`mesh_refine_node\`; do not merge the root branch until the submodule commit(s) are reachable. A task that remains on a non-main branch is not fully complete unless the final report names the follow-up state and next step.
185
+ 7. **Converge branches** — Before marking any task complete, classify every touched node/branch into exactly one final state: \`merged_to_main\`, \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\`. Use \`mesh_status\` branchConvergenceSummary. For obvious clean branch catch-up (ahead 0, behind > 0, upstream fresh, no dirty/stash/submodule issues), use \`mesh_fast_forward_node\` dry-run first and execute only when explicitly safe/approved; this avoids consuming an agent session. Use \`mesh_refine_node\` for clean worktree branches when safe. Before/refine merging root commits that contain submodule gitlink changes, require each submodule commit to be reachable from the configured submodule remote main branch, not merely present on a feature ref or local checkout. If \`mesh_refine_node\` returns \`submodule_reachability_failed\` or publish-required evidence, keep the public convergence bucket as \`blocked_review\`, ask the user for explicit approval to push/publish the unreachable submodule commit(s) to submodule main, then rerun \`mesh_refine_node\`; do not merge the root branch until the submodule commit(s) are reachable from submodule origin/main. A task that remains on a non-main branch is not fully complete unless the final report names the follow-up state and next step.
169
186
  8. **Clean up** — Remove worktree nodes via \`mesh_remove_node\` after their work is merged or no longer needed.
170
187
  9. **Report** — Summarize what was done, what changed, any issues, and the branch convergence state.
171
188
 
@@ -204,6 +221,6 @@ function buildRulesSection(coordinatorCliType?: string): string {
204
221
  - **Clean up worktree nodes.** After a worktree task completes and its changes are merged or checkpointed, call \`mesh_remove_node\` to free resources.
205
222
  - **Do not strand completed branches.** A checkpointed or clean feature/worktree branch is not done by itself. Merge/refine it to the mesh default branch, fast-forward obvious clean behind-only branches with \`mesh_fast_forward_node\`, or explicitly report one of \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\` with the next action.
206
223
  - **Keep Refinery validation project-configurable.** \`mesh_refine_node\` must execute validation from repo mesh/refine config (for example \`.adhdev/refine.{json,yaml,yml}\`, \`.adhdev/repo-mesh-refine.*\`, or \`repo-mesh.refine.*\`). Heuristics are suggestions/scaffolding only, not the execution path.
207
- - **Treat submodule reachability as publish-needed.** A \`submodule_reachability_failed\` refine result means the root gitlink points at a submodule commit that is not reachable from the configured submodule remote. Do not retry validation blindly or start code review first. Classify it as \`blocked_review\`, request user approval to push/publish the submodule commit, then rerun \`mesh_refine_node\`.
224
+ - **Treat submodule main reachability as publish-needed.** A \`submodule_reachability_failed\` refine result means the root gitlink points at a submodule commit that is not reachable from the configured submodule remote main branch. Do not treat feature-branch reachability as complete, retry validation blindly, or start code review first. Classify it as \`blocked_review\`, request user approval to push/publish the submodule commit to submodule main, then rerun \`mesh_refine_node\`.
208
225
  - **Name worktree branches meaningfully.** Use descriptive names like \`feat/auth-refactor\` or \`fix/build-123\`.${coordinatorNote}`;
209
226
  }
@@ -22,6 +22,7 @@ export interface MeshActiveWorkRecord {
22
22
  terminal?: boolean;
23
23
  terminalKind?: string;
24
24
  terminalAt?: string;
25
+ staleReason?: string;
25
26
  }
26
27
 
27
28
  export interface MeshActiveWorkSummary {
@@ -34,6 +35,7 @@ export interface MeshActiveWorkSummary {
34
35
  idleCount: number;
35
36
  sourceCounts: Record<MeshActiveWorkSource, number>;
36
37
  statusCounts: Record<MeshActiveWorkStatus, number>;
38
+ staleDirectCount: number;
37
39
  }
38
40
 
39
41
  export interface BuildMeshActiveWorkOptions {
@@ -64,10 +66,12 @@ function elapsedSince(value: string | undefined, now: number): number {
64
66
  return Number.isFinite(started) ? Math.max(0, now - started) : 0;
65
67
  }
66
68
 
67
- function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string, sessionId?: string): MeshActiveWorkStatus | undefined {
68
- if (!nodeId || !sessionId || !Array.isArray(nodes)) return undefined;
69
+ function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string, sessionId?: string): { status?: MeshActiveWorkStatus; staleReason?: string } {
70
+ if (!Array.isArray(nodes)) return {};
71
+ if (!nodeId) return { staleReason: 'direct task has no node id' };
69
72
  const node = nodes.find(item => readString(item?.id) === nodeId || readString(item?.nodeId) === nodeId || readString(item?.node_id) === nodeId);
70
- if (!node) return undefined;
73
+ if (!node) return { staleReason: 'direct task node is no longer in the live mesh' };
74
+ if (!sessionId) return {};
71
75
  const candidates: any[] = [];
72
76
  for (const value of [node.sessions, node.activeSessions, node.active_sessions, node.lastProbe?.sessions, node.last_probe?.sessions, node.lastProbe?.status?.sessions, node.last_probe?.status?.sessions]) {
73
77
  if (Array.isArray(value)) candidates.push(...value);
@@ -76,16 +80,18 @@ function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string, sessi
76
80
  if (value && typeof value === 'object') candidates.push(value);
77
81
  }
78
82
  const session = candidates.find(item => {
83
+ if (typeof item === 'string') return item === sessionId;
79
84
  const id = readString(item?.id) || readString(item?.sessionId) || readString(item?.session_id) || readString(item?.runtimeSessionId) || readString(item?.instanceId);
80
85
  return id === sessionId;
81
86
  });
82
- if (!session) return undefined;
87
+ if (!session) return { staleReason: 'direct task session is not present in live session records' };
88
+ if (typeof session === 'string') return {};
83
89
  const raw = `${readString(session.status) || ''} ${readString(session.lifecycle) || ''} ${readString(session.state) || ''} ${readString(session.activeChat?.status) || ''}`.toLowerCase();
84
- if (raw.includes('approval')) return 'awaiting_approval';
85
- if (raw.includes('generating') || raw.includes('running') || raw.includes('busy')) return 'generating';
86
- if (raw.includes('failed') || raw.includes('stopped') || raw.includes('terminated') || raw.includes('exited')) return 'failed';
87
- if (raw.includes('idle') || raw.includes('waiting_input') || raw.includes('ready')) return 'idle';
88
- return undefined;
90
+ if (raw.includes('approval')) return { status: 'awaiting_approval' };
91
+ if (raw.includes('generating') || raw.includes('running') || raw.includes('busy')) return { status: 'generating' };
92
+ if (raw.includes('failed') || raw.includes('stopped') || raw.includes('terminated') || raw.includes('exited')) return { status: 'failed' };
93
+ if (raw.includes('idle') || raw.includes('waiting_input') || raw.includes('ready')) return { status: 'idle' };
94
+ return {};
89
95
  }
90
96
 
91
97
  function isDirectDispatch(entry: MeshLedgerEntry): boolean {
@@ -138,12 +144,14 @@ export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]):
138
144
  idleCount: statusCounts.idle,
139
145
  sourceCounts,
140
146
  statusCounts,
147
+ staleDirectCount: activeWork.filter(item => item.source === 'direct' && item.staleReason).length,
141
148
  };
142
149
  }
143
150
 
144
- export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeWork: MeshActiveWorkRecord[]; summary: MeshActiveWorkSummary } {
151
+ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeWork: MeshActiveWorkRecord[]; staleDirectWork: MeshActiveWorkRecord[]; summary: MeshActiveWorkSummary } {
145
152
  const now = opts.now ?? Date.now();
146
153
  const records: MeshActiveWorkRecord[] = [];
154
+ const staleDirectWork: MeshActiveWorkRecord[] = [];
147
155
 
148
156
  for (const task of opts.queue || []) {
149
157
  if (task.status !== 'pending' && task.status !== 'assigned') continue;
@@ -173,13 +181,13 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
173
181
  .filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
174
182
  .find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
175
183
  const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
176
- const liveStatus = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
177
- const status = terminalStatus || liveStatus || 'assigned';
184
+ const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
185
+ const status = terminalStatus || live.status || 'assigned';
178
186
  const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
179
187
  if (terminalRow && opts.includeTerminalDirect !== true) continue;
180
188
  const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
181
189
  const { title, summary } = summarizeMessage(message);
182
- records.push({
190
+ const record: MeshActiveWorkRecord = {
183
191
  taskId,
184
192
  source: 'direct',
185
193
  status,
@@ -197,9 +205,18 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
197
205
  terminal: terminalRow,
198
206
  terminalKind: terminal?.kind,
199
207
  terminalAt: terminal?.timestamp,
200
- });
208
+ staleReason: live.staleReason,
209
+ };
210
+ if (live.staleReason && !terminalRow) {
211
+ staleDirectWork.push(record);
212
+ continue;
213
+ }
214
+ records.push(record);
201
215
  }
202
216
 
203
217
  records.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
204
- return { activeWork: records, summary: buildMeshActiveWorkSummary(records) };
218
+ staleDirectWork.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
219
+ const summary = buildMeshActiveWorkSummary(records);
220
+ summary.staleDirectCount = staleDirectWork.length;
221
+ return { activeWork: records, staleDirectWork, summary };
205
222
  }
@@ -55,6 +55,7 @@ export interface PendingMeshCoordinatorEvent {
55
55
  nodeId?: string;
56
56
  workspace?: string;
57
57
  metadataEvent: Record<string, unknown>;
58
+ coordinatorMessage?: string;
58
59
  queuedAt: number;
59
60
  }
60
61
 
@@ -750,23 +751,62 @@ function buildMeshSystemMessage(args: {
750
751
  const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
751
752
  const result = readRecord(args.metadataEvent.result);
752
753
  const validationSummary = readRecord(result?.validationSummary);
754
+ const patchEquivalence = readRecord(result?.patchEquivalence);
755
+ const finalConvergence = readRecord(result?.finalBranchConvergenceState);
753
756
  const validationStatus = readNonEmptyString(validationSummary?.status);
757
+ const patchStatus = readNonEmptyString(patchEquivalence?.status)
758
+ || (patchEquivalence?.equivalent === true ? 'passed' : '');
754
759
  const into = readNonEmptyString(result?.into);
755
760
  const branch = readNonEmptyString(result?.branch);
761
+ const mergeStatus = result?.merged === true ? 'merged' : readNonEmptyString(finalConvergence?.status);
762
+ const convergenceStatus = readNonEmptyString(finalConvergence?.status);
763
+ const nextStep = readNonEmptyString(result?.nextStep)
764
+ || readNonEmptyString(finalConvergence?.nextStep)
765
+ || 'Continue from the updated mesh state.';
756
766
  const details = [
757
767
  jobId ? `job_id=${jobId}` : '',
758
768
  branch && into ? `${branch}→${into}` : '',
759
769
  validationStatus ? `validation=${validationStatus}` : '',
770
+ patchStatus ? `patch_equivalence=${patchStatus}` : '',
771
+ mergeStatus ? `merge=${mergeStatus}` : '',
772
+ convergenceStatus ? `final_convergence=${convergenceStatus}` : '',
760
773
  ].filter(Boolean).join('; ');
761
- return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}. The worktree was merged and cleanup completed; continue from the updated mesh state.`;
774
+ return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}.\nNext step: ${nextStep}`;
762
775
  }
763
776
  if (args.event === 'refine:failed') {
764
777
  const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
765
778
  const result = readRecord(args.metadataEvent.result);
779
+ const validationSummary = readRecord(result?.validationSummary);
780
+ const patchEquivalence = readRecord(result?.patchEquivalence);
781
+ const finalConvergence = readRecord(result?.finalBranchConvergenceState);
766
782
  const code = readNonEmptyString(result?.code);
767
783
  const error = readNonEmptyString(result?.error);
768
- const details = [jobId ? `job_id=${jobId}` : '', code ? `code=${code}` : ''].filter(Boolean).join('; ');
769
- return `[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'} Review the terminal refine event/ledger before retrying.`;
784
+ const validationStatus = readNonEmptyString(validationSummary?.status);
785
+ const patchStatus = readNonEmptyString(patchEquivalence?.status)
786
+ || (patchEquivalence?.equivalent === true ? 'passed' : '');
787
+ const mergeStatus = result?.merged === true
788
+ ? 'merged'
789
+ : finalConvergence?.merged === false
790
+ ? 'not_merged'
791
+ : '';
792
+ const convergenceStatus = readNonEmptyString(result?.convergenceStatus)
793
+ || readNonEmptyString(finalConvergence?.status);
794
+ const blockedReason = readNonEmptyString(result?.blockedReason);
795
+ const nextStep = readNonEmptyString(result?.nextStep) || readNonEmptyString(finalConvergence?.nextStep);
796
+ const details = [
797
+ jobId ? `job_id=${jobId}` : '',
798
+ code ? `code=${code}` : '',
799
+ validationStatus ? `validation=${validationStatus}` : '',
800
+ patchStatus ? `patch_equivalence=${patchStatus}` : '',
801
+ mergeStatus ? `merge=${mergeStatus}` : '',
802
+ convergenceStatus ? `convergence=${convergenceStatus}` : '',
803
+ blockedReason ? `reason=${blockedReason}` : '',
804
+ ].filter(Boolean).join('; ');
805
+ const parts = [
806
+ `[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'}`,
807
+ nextStep ? `Next step: ${nextStep}` : 'Review the terminal refine event/ledger before retrying.',
808
+ ];
809
+ return parts.join('\n');
770
810
  }
771
811
  return '';
772
812
  }
@@ -904,7 +944,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
904
944
  remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
905
945
  }
906
946
  if (sessionId) {
907
- updateSessionTaskStatus(args.meshId, sessionId, 'failed');
947
+ const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
948
+ completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
908
949
  }
909
950
  }
910
951
 
@@ -1015,6 +1056,14 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1015
1056
  }
1016
1057
  }
1017
1058
 
1059
+ const messageText = buildMeshSystemMessage({
1060
+ event: args.event,
1061
+ nodeLabel: args.nodeLabel,
1062
+ metadataEvent: args.metadataEvent,
1063
+ recoveryContext,
1064
+ });
1065
+ if (!messageText) return { success: false, error: 'unsupported mesh event' };
1066
+
1018
1067
  const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
1019
1068
  const instState = inst.getState();
1020
1069
  if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
@@ -1034,6 +1083,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1034
1083
  ...args.metadataEvent,
1035
1084
  ...(recoveryContext ? { recoveryContext } : {}),
1036
1085
  },
1086
+ coordinatorMessage: messageText,
1037
1087
  queuedAt: Date.now(),
1038
1088
  })) {
1039
1089
  LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
@@ -1041,14 +1091,6 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1041
1091
  return { success: true, forwarded: 0 };
1042
1092
  }
1043
1093
 
1044
- const messageText = buildMeshSystemMessage({
1045
- event: args.event,
1046
- nodeLabel: args.nodeLabel,
1047
- metadataEvent: args.metadataEvent,
1048
- recoveryContext,
1049
- });
1050
- if (!messageText) return { success: false, error: 'unsupported mesh event' };
1051
-
1052
1094
  for (const coord of coordinatorInstances) {
1053
1095
  const coordState = coord.getState();
1054
1096
  LOG.info('MeshEvents', `Forwarding mesh event to coordinator ${coordState.instanceId}`);