@adhdev/daemon-core 0.9.82-rc.70 → 0.9.82-rc.72

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.70",
3
+ "version": "0.9.82-rc.72",
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",
@@ -24,6 +24,7 @@ import { filterUserFacingChatMessages, normalizeChatMessages } from '../provider
24
24
  const RECENT_SEND_WINDOW_MS = 1200;
25
25
  export const READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS = 25_000;
26
26
  const HERMES_CLI_STARTING_SEND_SETTLE_MS = 2_000;
27
+ const CODEX_NATIVE_HISTORY_FRESH_MS = 5 * 60_000;
27
28
  const recentSendByTarget = new Map<string, number>();
28
29
 
29
30
  interface ApprovalSelectableInstance extends ProviderInstance {
@@ -221,6 +222,114 @@ function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[]
221
222
  return normalizeChatMessages(messages);
222
223
  }
223
224
 
225
+ function getMessageNewestReceivedAt(messages: Array<{ receivedAt?: unknown; timestamp?: unknown }>): number {
226
+ let newest = 0;
227
+ for (const message of messages) {
228
+ const receivedAt = Number(message?.receivedAt ?? message?.timestamp ?? 0);
229
+ if (Number.isFinite(receivedAt) && receivedAt > newest) newest = receivedAt;
230
+ }
231
+ return newest;
232
+ }
233
+
234
+ function buildCliMessageSourceProvenance(args: {
235
+ selected: 'native-history' | 'pty-parser';
236
+ provider: string;
237
+ nativeHandle?: string;
238
+ fallbackReason?: string;
239
+ nativeSource?: string;
240
+ sourcePath?: string;
241
+ sourceMtimeMs?: number;
242
+ nativeMessages?: ChatMessage[];
243
+ ptyMessages?: ChatMessage[];
244
+ returnedMessages?: ChatMessage[];
245
+ safeMapping?: boolean;
246
+ freshEnough?: boolean;
247
+ ptyStatusApprovalOnly?: boolean;
248
+ }): Record<string, unknown> {
249
+ const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
250
+ const sourceMtimeAgeMs = sourceMtimeMs > 0 ? Math.max(0, Date.now() - sourceMtimeMs) : undefined;
251
+ const nativeMessages = args.nativeMessages || [];
252
+ const ptyMessages = args.ptyMessages || [];
253
+ const returnedMessages = args.returnedMessages || [];
254
+ return {
255
+ selected: args.selected,
256
+ provider: args.provider,
257
+ ...(args.nativeHandle ? { nativeHandle: args.nativeHandle } : {}),
258
+ ...(args.fallbackReason ? { fallbackReason: args.fallbackReason } : {}),
259
+ ...(args.nativeSource ? { nativeSource: args.nativeSource } : {}),
260
+ ...(args.sourcePath ? { sourcePath: args.sourcePath } : {}),
261
+ ptyStatusApprovalOnly: args.ptyStatusApprovalOnly === true,
262
+ staleness: {
263
+ sourceMtimeMs: sourceMtimeMs || undefined,
264
+ sourceMtimeAgeMs,
265
+ nativeNewestMessageAt: getMessageNewestReceivedAt(nativeMessages),
266
+ ptyNewestMessageAt: getMessageNewestReceivedAt(ptyMessages),
267
+ freshEnough: args.freshEnough === true,
268
+ },
269
+ coverage: {
270
+ nativeMessageCount: nativeMessages.length,
271
+ ptyMessageCount: ptyMessages.length,
272
+ returnedMessageCount: returnedMessages.length,
273
+ safeMapping: args.safeMapping === true,
274
+ },
275
+ };
276
+ }
277
+
278
+ function buildNativeHistoryFallbackReason(args: {
279
+ providerType: string;
280
+ nativeSource?: string;
281
+ nativeMessageCount: number;
282
+ safeMapping: boolean;
283
+ freshEnough: boolean;
284
+ }): string {
285
+ if (args.providerType !== 'codex-cli') return 'provider_not_codex_cli';
286
+ if (args.nativeSource === 'native-unavailable') return 'native_history_unavailable';
287
+ if (args.nativeSource && args.nativeSource !== 'provider-native') return `native_history_source_${args.nativeSource}`;
288
+ if (args.nativeMessageCount <= 0) return 'native_history_empty';
289
+ if (!args.safeMapping) return 'native_history_not_safely_mapped';
290
+ if (!args.freshEnough) return 'native_history_stale';
291
+ return 'native_history_not_selected';
292
+ }
293
+
294
+ function isCodexCliProvider(providerType: string): boolean {
295
+ return providerType === 'codex-cli';
296
+ }
297
+
298
+ function hasSafeNativeHistoryMapping(args: {
299
+ historySessionId?: string;
300
+ providerSessionId?: string;
301
+ workspace?: string;
302
+ nativeMessages: ChatMessage[];
303
+ }): boolean {
304
+ const explicitSessionId = String(args.historySessionId || args.providerSessionId || '').trim();
305
+ if (explicitSessionId) {
306
+ const messageSessionIds = args.nativeMessages
307
+ .map((message: any) => typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '')
308
+ .filter(Boolean);
309
+ if (messageSessionIds.length === 0) return true;
310
+ return messageSessionIds.some((id) => id === explicitSessionId);
311
+ }
312
+ const workspace = String(args.workspace || '').trim();
313
+ if (!workspace) return false;
314
+ return args.nativeMessages.some((message: any) => String(message?.workspace || '').trim() === workspace);
315
+ }
316
+
317
+ function isNativeHistoryFreshEnough(args: {
318
+ sourceMtimeMs?: number;
319
+ nativeMessages: ChatMessage[];
320
+ ptyMessages: ChatMessage[];
321
+ }): boolean {
322
+ const nativeNewest = getMessageNewestReceivedAt(args.nativeMessages);
323
+ const ptyNewest = getMessageNewestReceivedAt(args.ptyMessages);
324
+ if (nativeNewest > 0 && nativeNewest >= ptyNewest) return true;
325
+ const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
326
+ if (sourceMtimeMs > 0 && Date.now() - sourceMtimeMs <= CODEX_NATIVE_HISTORY_FRESH_MS) return true;
327
+ return ptyNewest === 0 && nativeNewest > 0;
328
+ }
329
+
330
+ function shouldPreserveReadChatPayloadField(key: string): boolean {
331
+ return key === 'messageSource' || key === 'transcriptProvenance';
332
+ }
224
333
 
225
334
  function deriveHistoryDedupKey(message: ChatMessage & { _unitKey?: string; _turnKey?: string }): string | undefined {
226
335
  const unitKey = typeof message._unitKey === 'string' ? message._unitKey.trim() : '';
@@ -356,6 +465,7 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
356
465
  return {
357
466
  success: true,
358
467
  ...validatedPayload,
468
+ ...Object.fromEntries(Object.entries(payload).filter(([key]) => shouldPreserveReadChatPayloadField(key))),
359
469
  messages: sync.messages,
360
470
  totalMessages: sync.totalMessages,
361
471
  ...(returnedDebugReadChat ? { debugReadChat: returnedDebugReadChat } : {}),
@@ -584,6 +694,8 @@ function buildChatDebugBundleSummary(bundle: Record<string, unknown>): Record<st
584
694
  adapterStatus: debugReadChat.adapterStatus,
585
695
  parsedStatus: debugReadChat.parsedStatus,
586
696
  returnedStatus: debugReadChat.returnedStatus,
697
+ selectedMessageSource: debugReadChat.selectedMessageSource,
698
+ messageSource: debugReadChat.messageSource,
587
699
  parsedMsgCount: debugReadChat.parsedMsgCount,
588
700
  returnedMsgCount: debugReadChat.returnedMsgCount,
589
701
  shouldPreferAdapterMessages: debugReadChat.shouldPreferAdapterMessages,
@@ -646,6 +758,8 @@ export async function handleGetChatDebugBundle(h: CommandHelpers, args: any): Pr
646
758
  providerSessionId: readResult.providerSessionId,
647
759
  transcriptAuthority: readResult.transcriptAuthority,
648
760
  coverage: readResult.coverage,
761
+ messageSource: readResult.messageSource,
762
+ transcriptProvenance: readResult.transcriptProvenance,
649
763
  activeModal: readResult.activeModal,
650
764
  messagesTail: Array.isArray(readResult.messages) ? readResult.messages.slice(-20) : [],
651
765
  debugReadChat: readResult.debugReadChat,
@@ -882,25 +996,146 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
882
996
  && typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
883
997
  ? runtimeMessageMerger.mergeRuntimeChatMessages(parsedMessages)
884
998
  : parsedMessages;
999
+ const providerType = provider?.type || adapter.cliType;
1000
+ let selectedMessages = returnedMessages;
1001
+ let selectedTitle = title;
1002
+ let selectedProviderSessionId = providerSessionId;
1003
+ let selectedTranscriptAuthority = transcriptAuthority;
1004
+ let selectedCoverage = coverage;
1005
+ let messageSource = buildCliMessageSourceProvenance({
1006
+ selected: 'pty-parser',
1007
+ provider: adapter.cliType,
1008
+ fallbackReason: isCodexCliProvider(providerType) ? 'native_history_not_checked' : 'provider_not_codex_cli',
1009
+ ptyMessages: returnedMessages,
1010
+ returnedMessages,
1011
+ ptyStatusApprovalOnly: false,
1012
+ });
1013
+
1014
+ if (isCodexCliProvider(providerType)) {
1015
+ const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h, adapter.cliType);
1016
+ const workspace = typeof args?.workspace === 'string'
1017
+ ? args.workspace
1018
+ : typeof (h.currentSession as any)?.workspace === 'string'
1019
+ ? (h.currentSession as any).workspace
1020
+ : typeof adapter.workingDir === 'string'
1021
+ ? adapter.workingDir
1022
+ : undefined;
1023
+ const nativeHistoryLimit = Math.max(
1024
+ normalizeReadChatTailLimit(args) || 0,
1025
+ returnedMessages.length,
1026
+ 200,
1027
+ );
1028
+ let nativeHistory: ReturnType<typeof readProviderChatHistory> | null = null;
1029
+ try {
1030
+ nativeHistory = readProviderChatHistory(agentStr, {
1031
+ canonicalHistory: provider?.canonicalHistory,
1032
+ historySessionId,
1033
+ workspace,
1034
+ offset: 0,
1035
+ limit: nativeHistoryLimit,
1036
+ excludeRecentCount: 0,
1037
+ historyBehavior: provider?.historyBehavior,
1038
+ scripts: provider?.scripts as any,
1039
+ });
1040
+ } catch (error: any) {
1041
+ const fallbackReason = `native_history_error:${error?.message || String(error)}`;
1042
+ messageSource = buildCliMessageSourceProvenance({
1043
+ selected: 'pty-parser',
1044
+ provider: adapter.cliType,
1045
+ fallbackReason,
1046
+ ptyMessages: returnedMessages,
1047
+ returnedMessages,
1048
+ ptyStatusApprovalOnly: false,
1049
+ });
1050
+ nativeHistory = null;
1051
+ }
1052
+
1053
+ if (nativeHistory) {
1054
+ const nativeMessages = Array.isArray((nativeHistory as any).messages)
1055
+ ? normalizeChatMessages((nativeHistory as any).messages as ChatMessage[])
1056
+ : [];
1057
+ const historyProviderSessionId = typeof (nativeHistory as any)?.providerSessionId === 'string'
1058
+ ? (nativeHistory as any).providerSessionId
1059
+ : historySessionId;
1060
+ const safeMapping = hasSafeNativeHistoryMapping({
1061
+ historySessionId,
1062
+ providerSessionId,
1063
+ workspace,
1064
+ nativeMessages,
1065
+ });
1066
+ const freshEnough = isNativeHistoryFreshEnough({
1067
+ sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1068
+ nativeMessages,
1069
+ ptyMessages: returnedMessages,
1070
+ });
1071
+ if ((nativeHistory as any).source === 'provider-native' && nativeMessages.length > 0 && safeMapping && freshEnough) {
1072
+ selectedMessages = finalizeStreamingMessagesWhenIdle(nativeMessages, returnedStatus);
1073
+ selectedProviderSessionId = historyProviderSessionId || providerSessionId;
1074
+ selectedTranscriptAuthority = 'provider';
1075
+ selectedCoverage = (nativeHistory as any).hasMore ? 'tail' : 'full';
1076
+ messageSource = buildCliMessageSourceProvenance({
1077
+ selected: 'native-history',
1078
+ provider: adapter.cliType,
1079
+ nativeHandle: selectedProviderSessionId || historySessionId,
1080
+ nativeSource: (nativeHistory as any).source,
1081
+ sourcePath: (nativeHistory as any).sourcePath,
1082
+ sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1083
+ nativeMessages,
1084
+ ptyMessages: returnedMessages,
1085
+ returnedMessages: selectedMessages,
1086
+ safeMapping,
1087
+ freshEnough,
1088
+ ptyStatusApprovalOnly: true,
1089
+ });
1090
+ } else {
1091
+ const fallbackReason = buildNativeHistoryFallbackReason({
1092
+ providerType,
1093
+ nativeSource: (nativeHistory as any).source,
1094
+ nativeMessageCount: nativeMessages.length,
1095
+ safeMapping,
1096
+ freshEnough,
1097
+ });
1098
+ messageSource = buildCliMessageSourceProvenance({
1099
+ selected: 'pty-parser',
1100
+ provider: adapter.cliType,
1101
+ nativeHandle: historyProviderSessionId || historySessionId,
1102
+ fallbackReason,
1103
+ nativeSource: (nativeHistory as any).source,
1104
+ sourcePath: (nativeHistory as any).sourcePath,
1105
+ sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1106
+ nativeMessages,
1107
+ ptyMessages: returnedMessages,
1108
+ returnedMessages,
1109
+ safeMapping,
1110
+ freshEnough,
1111
+ ptyStatusApprovalOnly: false,
1112
+ });
1113
+ }
1114
+ }
1115
+ }
885
1116
  LOG.debug('Command', `[read_chat] cli-like parsed provider=${adapter.cliType} target=${String(args?.targetSessionId || '')} adapterStatus=${String(adapterStatus.status || '')} parsedStatus=${String(parsedRecord.status || '')} parsedMsgCount=${parsedRecord.messages.length} returnedMsgCount=${returnedMessages.length}`);
886
1117
  return buildReadChatCommandResult({
887
- messages: returnedMessages,
1118
+ messages: selectedMessages,
888
1119
  status: returnedStatus,
889
1120
  activeModal,
1121
+ messageSource,
1122
+ transcriptProvenance: messageSource,
890
1123
  debugReadChat: {
891
1124
  provider: adapter.cliType,
892
1125
  targetSessionId: String(args?.targetSessionId || ''),
893
1126
  adapterStatus: String(adapterStatus.status || ''),
894
1127
  parsedStatus: String(parsedRecord.status || ''),
895
1128
  returnedStatus: String(returnedStatus || ''),
896
- shouldPreferAdapterMessages: false,
1129
+ selectedMessageSource: (messageSource as any).selected,
1130
+ messageSource,
1131
+ shouldPreferAdapterMessages: (messageSource as any).selected !== 'native-history',
897
1132
  parsedMsgCount: parsedRecord.messages.length,
898
- returnedMsgCount: returnedMessages.length,
1133
+ returnedMsgCount: selectedMessages.length,
899
1134
  },
900
- ...(title ? { title } : {}),
901
- ...(providerSessionId ? { providerSessionId } : {}),
902
- ...(transcriptAuthority ? { transcriptAuthority } : {}),
903
- ...(coverage ? { coverage } : {}),
1135
+ ...(selectedTitle ? { title: selectedTitle } : {}),
1136
+ ...(selectedProviderSessionId ? { providerSessionId: selectedProviderSessionId } : {}),
1137
+ ...(selectedTranscriptAuthority ? { transcriptAuthority: selectedTranscriptAuthority } : {}),
1138
+ ...(selectedCoverage ? { coverage: selectedCoverage } : {}),
904
1139
  }, args);
905
1140
  }
906
1141
  const historyLimit = normalizeReadChatTailLimit(args);
@@ -80,6 +80,12 @@ export interface CliManagerDeps {
80
80
 
81
81
  type CommandResult = { success: boolean;[key: string]: unknown };
82
82
 
83
+ const BUSY_AGENT_STATUSES = new Set(['generating', 'running', 'streaming', 'starting', 'busy', 'waiting', 'waiting_approval', 'long_generating']);
84
+
85
+ function normalizeAgentStatus(value: unknown): string {
86
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
87
+ }
88
+
83
89
  export interface CliTransportFactoryParams {
84
90
  runtimeId: string;
85
91
  providerType: string;
@@ -1073,6 +1079,19 @@ export class DaemonCliManager {
1073
1079
  const { adapter, key } = found;
1074
1080
 
1075
1081
  if (action === 'send_chat') {
1082
+ const currentStatus = normalizeAgentStatus(adapter.getStatus?.()?.status);
1083
+ if (BUSY_AGENT_STATUSES.has(currentStatus)) {
1084
+ return {
1085
+ success: false,
1086
+ code: 'agent_runtime_busy',
1087
+ reason: 'agent_runtime_busy',
1088
+ retryable: true,
1089
+ retryRecommended: true,
1090
+ status: currentStatus,
1091
+ targetSessionId: args?.targetSessionId,
1092
+ error: `CLI agent '${agentType}' is currently ${currentStatus}; retry after the current turn finishes.`,
1093
+ };
1094
+ }
1076
1095
  const input = normalizeInputEnvelope(args?.input ? { input: args.input } : args);
1077
1096
  const provider = this.providerLoader.resolve(agentType) || this.providerLoader.getMeta(agentType);
1078
1097
  if (provider?.category === 'acp') {
@@ -55,7 +55,7 @@ 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 } from 'os';
58
+ import { homedir, tmpdir } from 'os';
59
59
  import { join as pathJoin, resolve as pathResolve } from 'path';
60
60
  import * as fs from 'fs';
61
61
 
@@ -1089,7 +1089,12 @@ type MeshRefineSubmoduleReachabilityEntry = {
1089
1089
  path: string;
1090
1090
  commit: string;
1091
1091
  reachable: boolean;
1092
+ publishRequired?: boolean;
1092
1093
  checkedLocal?: boolean;
1094
+ localReachable?: boolean;
1095
+ remote?: string;
1096
+ remoteUrl?: string;
1097
+ remoteReachable?: boolean;
1093
1098
  fetchedFromOrigin?: boolean;
1094
1099
  error?: string;
1095
1100
  };
@@ -1161,6 +1166,13 @@ function recordMeshRefineStage(
1161
1166
  });
1162
1167
  }
1163
1168
 
1169
+ function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReachabilityEntry[]): string {
1170
+ const refs = entries
1171
+ .map(entry => `${entry.path}@${entry.commit}`)
1172
+ .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.`;
1174
+ }
1175
+
1164
1176
  async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
1165
1177
  const { execFileSync } = await import('node:child_process');
1166
1178
  const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
@@ -1255,6 +1267,16 @@ async function runMeshRefineSubmoduleReachabilityGate(
1255
1267
  });
1256
1268
  return String(stdout || '');
1257
1269
  };
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
+ }
1279
+ };
1258
1280
 
1259
1281
  const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
1260
1282
  const gitlinks = treeOutput
@@ -1276,6 +1298,7 @@ async function runMeshRefineSubmoduleReachabilityGate(
1276
1298
  try {
1277
1299
  if (!fs.existsSync(submodulePath)) {
1278
1300
  entry.error = `Submodule checkout missing at ${gitlink.path}`;
1301
+ entry.publishRequired = true;
1279
1302
  entries.push(entry);
1280
1303
  continue;
1281
1304
  }
@@ -1283,23 +1306,38 @@ async function runMeshRefineSubmoduleReachabilityGate(
1283
1306
  entry.checkedLocal = true;
1284
1307
  try {
1285
1308
  await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
1286
- entry.reachable = true;
1287
- entries.push(entry);
1288
- continue;
1309
+ entry.localReachable = true;
1289
1310
  } catch {
1311
+ entry.localReachable = false;
1290
1312
  // Probe the submodule remote before allowing cleanup/completion.
1291
1313
  }
1292
1314
 
1293
1315
  try {
1294
- await runGit(submodulePath, ['fetch', 'origin', gitlink.commit]);
1316
+ entry.remote = 'origin';
1317
+ let remoteUrl = '';
1318
+ try {
1319
+ remoteUrl = (await runGit(submodulePath, ['remote', 'get-url', 'origin'])).trim();
1320
+ if (!remoteUrl) throw new Error('origin remote has no URL');
1321
+ entry.remoteUrl = remoteUrl;
1322
+ } catch {
1323
+ entry.error = 'Submodule remote reachability check failed: no configured origin remote';
1324
+ entry.publishRequired = true;
1325
+ entries.push(entry);
1326
+ continue;
1327
+ }
1328
+ await verifyRemoteCommitReachable(remoteUrl, gitlink.commit);
1295
1329
  entry.fetchedFromOrigin = true;
1296
- await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
1330
+ entry.remoteReachable = true;
1297
1331
  entry.reachable = true;
1298
1332
  } catch (e: any) {
1299
- entry.error = truncateValidationOutput(e?.stderr || e?.message || String(e));
1333
+ entry.remoteReachable = false;
1334
+ entry.publishRequired = true;
1335
+ const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
1336
+ entry.error = `Submodule remote reachability check failed for origin: ${details}`;
1300
1337
  }
1301
1338
  } catch (e: any) {
1302
1339
  entry.error = truncateValidationOutput(e?.message || String(e));
1340
+ entry.publishRequired = true;
1303
1341
  }
1304
1342
  entries.push(entry);
1305
1343
  }
@@ -1308,16 +1346,17 @@ async function runMeshRefineSubmoduleReachabilityGate(
1308
1346
  return {
1309
1347
  status: unreachable.length ? 'failed' : 'passed',
1310
1348
  checked: entries.length,
1311
- unreachable,
1312
- entries,
1349
+ unreachable: unreachable.map(entry => ({ ...entry, publishRequired: entry.publishRequired !== false })),
1350
+ entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: entry.publishRequired !== false }),
1313
1351
  durationMs: Date.now() - startedAt,
1314
1352
  };
1315
1353
  } catch (e: any) {
1354
+ const unreachable = entries.filter(entry => !entry.reachable).map(entry => ({ ...entry, publishRequired: true }));
1316
1355
  return {
1317
1356
  status: 'failed',
1318
1357
  checked: entries.length,
1319
- unreachable: entries.filter(entry => !entry.reachable),
1320
- entries,
1358
+ unreachable,
1359
+ entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: true }),
1321
1360
  durationMs: Date.now() - startedAt,
1322
1361
  error: truncateValidationOutput(e?.message || String(e)),
1323
1362
  };
@@ -2679,15 +2718,41 @@ export class DaemonCommandRouter {
2679
2718
  const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead);
2680
2719
  recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
2681
2720
  checked: submoduleReachability.checked,
2682
- unreachable: submoduleReachability.unreachable.map(entry => ({ path: entry.path, commit: entry.commit, error: entry.error })),
2721
+ unreachable: submoduleReachability.unreachable.map(entry => ({
2722
+ path: entry.path,
2723
+ commit: entry.commit,
2724
+ publishRequired: entry.publishRequired === true,
2725
+ remote: entry.remote,
2726
+ remoteUrl: entry.remoteUrl,
2727
+ remoteReachable: entry.remoteReachable,
2728
+ error: entry.error,
2729
+ })),
2683
2730
  error: submoduleReachability.error,
2684
2731
  });
2685
2732
  if (submoduleReachability.status === 'failed') {
2733
+ const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
2686
2734
  return {
2687
2735
  success: false,
2688
2736
  code: 'submodule_reachability_failed',
2689
2737
  convergenceStatus: 'blocked_review',
2690
- error: 'Refinery submodule reachability preflight failed; merge/refine cleanup was not attempted.',
2738
+ publishRequired: true,
2739
+ 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.',
2741
+ nextStep,
2742
+ nextSteps: [
2743
+ '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.',
2745
+ '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.',
2747
+ ],
2748
+ unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
2749
+ path: entry.path,
2750
+ commit: entry.commit,
2751
+ remote: entry.remote,
2752
+ remoteUrl: entry.remoteUrl,
2753
+ remoteReachable: entry.remoteReachable,
2754
+ error: entry.error,
2755
+ })),
2691
2756
  branch,
2692
2757
  into: baseBranch,
2693
2758
  validationSummary,
@@ -2703,6 +2768,8 @@ export class DaemonCommandRouter {
2703
2768
  patchEquivalence: 'passed',
2704
2769
  submoduleReachability: 'failed',
2705
2770
  status: 'blocked_review',
2771
+ reason: 'submodule_publish_required',
2772
+ nextStep,
2706
2773
  },
2707
2774
  };
2708
2775
  }
@@ -165,7 +165,7 @@ const WORKFLOW_SECTION = `## Orchestration Workflow
165
165
  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
166
  5. **Verify** — When a task reports completion or git work is visible, call \`mesh_git_status\` to verify changes were made.
167
167
  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. 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.
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.
169
169
  8. **Clean up** — Remove worktree nodes via \`mesh_remove_node\` after their work is merged or no longer needed.
170
170
  9. **Report** — Summarize what was done, what changed, any issues, and the branch convergence state.
171
171
 
@@ -204,5 +204,6 @@ function buildRulesSection(coordinatorCliType?: string): string {
204
204
  - **Clean up worktree nodes.** After a worktree task completes and its changes are merged or checkpointed, call \`mesh_remove_node\` to free resources.
205
205
  - **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
206
  - **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\`.
207
208
  - **Name worktree branches meaningfully.** Use descriptive names like \`feat/auth-refactor\` or \`fix/build-123\`.${coordinatorNote}`;
208
209
  }
@@ -891,7 +891,9 @@ export class CliProviderInstance implements ProviderInstance {
891
891
  chatTitle: pending.chatTitle,
892
892
  duration: pending.duration,
893
893
  timestamp: pending.timestamp,
894
- finalSummary: extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
894
+ finalSummary: blockReason.startsWith('parsed_status:')
895
+ ? ''
896
+ : extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
895
897
  completionDiagnostic,
896
898
  });
897
899
  this.completedDebouncePending = null;