@adhdev/daemon-core 0.9.82-rc.209 → 0.9.82-rc.210

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.
Files changed (64) hide show
  1. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +2 -0
  2. package/dist/commands/router.d.ts +6 -0
  3. package/dist/git/git-commands.d.ts +2 -0
  4. package/dist/git/git-diff.d.ts +6 -0
  5. package/dist/index.d.ts +11 -5
  6. package/dist/index.js +5699 -3486
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +5679 -3483
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/mesh/coordinator-prompt.d.ts +6 -0
  11. package/dist/mesh/mesh-delivery-policy.d.ts +5 -0
  12. package/dist/mesh/mesh-events-coordinator.d.ts +151 -0
  13. package/dist/mesh/mesh-events-pending.d.ts +33 -0
  14. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  15. package/dist/mesh/mesh-events-utils.d.ts +14 -0
  16. package/dist/mesh/mesh-events.d.ts +5 -198
  17. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  18. package/dist/mesh/mesh-ledger.d.ts +19 -0
  19. package/dist/mesh/mesh-missions.d.ts +58 -0
  20. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  21. package/dist/mesh/mesh-runtime-store.d.ts +175 -0
  22. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  23. package/dist/mesh/mesh-work-queue.d.ts +82 -0
  24. package/dist/mesh/refine-config.d.ts +24 -2
  25. package/dist/mesh/worktree-bootstrap-config.d.ts +22 -0
  26. package/dist/providers/acp-provider-instance.d.ts +2 -0
  27. package/dist/providers/spec/driver.d.ts +8 -0
  28. package/dist/providers/spec/evaluator.d.ts +4 -5
  29. package/dist/providers/spec/loader.d.ts +1 -0
  30. package/dist/providers/spec/schema.gen.d.ts +1409 -6
  31. package/dist/providers/spec/types.d.ts +188 -175
  32. package/dist/repo-mesh-types.d.ts +1 -0
  33. package/package.json +1 -1
  34. package/src/boot/daemon-lifecycle.ts +3 -0
  35. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +20 -7
  36. package/src/commands/router.ts +594 -66
  37. package/src/git/git-commands.ts +5 -5
  38. package/src/git/git-diff.ts +53 -0
  39. package/src/index.ts +11 -5
  40. package/src/mesh/coordinator-prompt.ts +14 -1
  41. package/src/mesh/mesh-delivery-policy.ts +17 -0
  42. package/src/mesh/mesh-events-coordinator.ts +1404 -0
  43. package/src/mesh/mesh-events-pending.ts +371 -0
  44. package/src/mesh/mesh-events-stale.ts +283 -0
  45. package/src/mesh/mesh-events-utils.ts +161 -0
  46. package/src/mesh/mesh-events.ts +27 -2143
  47. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  48. package/src/mesh/mesh-ledger.ts +134 -2
  49. package/src/mesh/mesh-missions.ts +151 -0
  50. package/src/mesh/mesh-review-inbox.ts +307 -0
  51. package/src/mesh/mesh-runtime-store.ts +539 -3
  52. package/src/mesh/mesh-task-stats.ts +154 -0
  53. package/src/mesh/mesh-work-queue.ts +233 -17
  54. package/src/mesh/refine-config.ts +42 -5
  55. package/src/mesh/worktree-bootstrap-config.ts +79 -0
  56. package/src/providers/acp-provider-instance.ts +15 -1
  57. package/src/providers/cli-provider-instance.ts +34 -13
  58. package/src/providers/spec/driver.ts +57 -29
  59. package/src/providers/spec/evaluator.ts +302 -112
  60. package/src/providers/spec/loader.ts +226 -37
  61. package/src/providers/spec/schema.gen.ts +450 -334
  62. package/src/providers/spec/schema.json +162 -75
  63. package/src/providers/spec/types.ts +234 -183
  64. package/src/repo-mesh-types.ts +1 -0
@@ -56,6 +56,7 @@ import {
56
56
  import {
57
57
  MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
58
58
  MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
59
+ evaluateWorktreeBootstrapState,
59
60
  loadMeshWorktreeBootstrapConfig,
60
61
  runMeshWorktreeBootstrap,
61
62
  type WorktreeBootstrapState,
@@ -940,6 +941,7 @@ function finalizeMeshNodeStatus(args: {
940
941
  status.launchBlockedReason = 'worktree_bootstrap_failed';
941
942
  status.launchBlockedMessage = readStringValue(bootstrap.error)
942
943
  || 'Required worktree bootstrap failed; resolve it before launching an agent into this node.';
944
+ status.recoveryHint = 'Run retry_mesh_node_bootstrap to retry';
943
945
  return;
944
946
  }
945
947
  if (bootstrap.status === 'running' && bootstrap.required !== false) {
@@ -1306,6 +1308,27 @@ type MeshRefineValidationSummary = {
1306
1308
  configSourceType?: string;
1307
1309
  suggestions?: unknown[];
1308
1310
  suggestedConfig?: unknown;
1311
+ /**
1312
+ * M2-3: the bootstrap stage recorded separately from validation so review
1313
+ * surfaces can distinguish environment failures from validation failures.
1314
+ * cached — worktree_bootstrap was 'ready' (staleInputs unchanged), skipped
1315
+ * ran — worktree_bootstrap was stale/never-ran and re-ran successfully
1316
+ * failed — bootstrap run failed (refine stops before validation)
1317
+ * skipped — refine config validation.bootstrap === 'skip'
1318
+ * legacy — deprecated validation.bootstrapCommands path was used
1319
+ * not_configured — no bootstrap definition anywhere
1320
+ */
1321
+ bootstrap?: {
1322
+ stage: 'cached' | 'ran' | 'failed' | 'skipped' | 'legacy' | 'not_configured';
1323
+ status?: string;
1324
+ skipped?: boolean;
1325
+ configSource?: string;
1326
+ staleReason?: string;
1327
+ error?: string;
1328
+ commandsRun?: Array<Record<string, unknown>>;
1329
+ };
1330
+ /** M2-2: deprecation notices from the refine config (e.g. bootstrapCommands). */
1331
+ deprecationWarnings?: string[];
1309
1332
  };
1310
1333
 
1311
1334
  type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
@@ -1404,6 +1427,12 @@ type MeshRefineJobHandle = {
1404
1427
  completedAt?: string;
1405
1428
  duplicate?: boolean;
1406
1429
  retryOfJobId?: string;
1430
+ /**
1431
+ * The coordinator daemon ID that initiated this refine job.
1432
+ * When set, events for this job are scoped to that coordinator's
1433
+ * pending-events queue instead of the shared broadcast queue.
1434
+ */
1435
+ targetCoordinatorDaemonId?: string;
1407
1436
  eventDelivery: {
1408
1437
  pendingEvents: true;
1409
1438
  ledger: true;
@@ -1919,7 +1948,16 @@ function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<str
1919
1948
  };
1920
1949
  }
1921
1950
 
1922
- async function runMeshRefineValidationGate(mesh: any, workspace: string): Promise<MeshRefineValidationSummary> {
1951
+ async function runMeshRefineValidationGate(
1952
+ mesh: any,
1953
+ workspace: string,
1954
+ opts?: {
1955
+ /** M2-2: persisted node bootstrap state for staleness evaluation. */
1956
+ persistedBootstrapState?: WorktreeBootstrapState | null;
1957
+ /** M2-2: called after an inherit-mode bootstrap run so the caller can persist the new state. */
1958
+ onBootstrapStateChange?: (state: WorktreeBootstrapState) => void;
1959
+ },
1960
+ ): Promise<MeshRefineValidationSummary> {
1923
1961
  const { execFile } = await import('node:child_process');
1924
1962
  const { promisify } = await import('node:util');
1925
1963
  const execFileAsync = promisify(execFile);
@@ -1937,6 +1975,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
1937
1975
  configSourceType: selection.sourceType,
1938
1976
  suggestions: selection.suggestions,
1939
1977
  suggestedConfig: selection.suggestedConfig,
1978
+ ...(selection.deprecationWarnings.length > 0 ? { deprecationWarnings: selection.deprecationWarnings } : {}),
1940
1979
  };
1941
1980
 
1942
1981
  if (!selection.commands.length) {
@@ -1944,6 +1983,52 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
1944
1983
  return summary;
1945
1984
  }
1946
1985
 
1986
+ // ── M2-2: Bootstrap stage — refine consumes the worktree_bootstrap config
1987
+ // instead of defining its own. Legacy validation.bootstrapCommands run
1988
+ // only when no worktree_bootstrap config exists (deprecation path).
1989
+ let runLegacyBootstrapCommands = selection.bootstrapCommands.length > 0;
1990
+ if (selection.bootstrapMode === 'skip') {
1991
+ summary.bootstrap = { stage: 'skipped', skipped: true };
1992
+ runLegacyBootstrapCommands = false;
1993
+ } else {
1994
+ const wbLoad = loadMeshWorktreeBootstrapConfig(mesh, workspace);
1995
+ const wbUsable = !!wbLoad.config && wbLoad.sourceType !== 'invalid'
1996
+ && wbLoad.config.enabled !== false && wbLoad.config.runOnClone !== false;
1997
+ if (wbUsable) {
1998
+ runLegacyBootstrapCommands = false; // worktree_bootstrap wins over deprecated bootstrapCommands
1999
+ const evaluated = evaluateWorktreeBootstrapState(mesh, workspace, opts?.persistedBootstrapState);
2000
+ if (evaluated.status === 'ready') {
2001
+ summary.bootstrap = { stage: 'cached', status: 'ready', skipped: true, configSource: evaluated.configSource };
2002
+ } else {
2003
+ const ran = await runMeshWorktreeBootstrap(mesh, workspace);
2004
+ try { opts?.onBootstrapStateChange?.(ran); } catch { /* persistence is best-effort */ }
2005
+ if (ran.status === 'ready') {
2006
+ summary.bootstrap = {
2007
+ stage: 'ran',
2008
+ status: 'ready',
2009
+ configSource: ran.configSource,
2010
+ ...(evaluated.staleReason ? { staleReason: evaluated.staleReason } : {}),
2011
+ commandsRun: ran.commandsRun,
2012
+ };
2013
+ } else {
2014
+ summary.bootstrap = {
2015
+ stage: 'failed',
2016
+ status: ran.status,
2017
+ configSource: ran.configSource,
2018
+ error: ran.error,
2019
+ commandsRun: ran.commandsRun,
2020
+ };
2021
+ summary.status = 'failed';
2022
+ summary.failureKind = 'dependency_bootstrap_failed';
2023
+ summary.failureCode = 'dependency_bootstrap_failed';
2024
+ return summary;
2025
+ }
2026
+ }
2027
+ } else if (!runLegacyBootstrapCommands) {
2028
+ summary.bootstrap = { stage: 'not_configured' };
2029
+ }
2030
+ }
2031
+
1947
2032
  const commandRecord = (candidate: MeshRefineValidationCommand, cwd: string, startedAt: number, result: any, passed: boolean, extras: Record<string, unknown> = {}) => ({
1948
2033
  command: candidate.command,
1949
2034
  args: candidate.args,
@@ -1969,30 +2054,34 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
1969
2054
  .some(lock => fs.existsSync(pathJoin(cwd, lock)));
1970
2055
  };
1971
2056
 
1972
- for (const candidate of selection.bootstrapCommands) {
1973
- const startedAt = Date.now();
1974
- const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
1975
- const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
1976
- try {
1977
- const result = await execFileAsync(candidate.command, candidate.args, {
1978
- cwd,
1979
- encoding: 'utf8',
1980
- timeout,
1981
- maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
1982
- env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
1983
- });
1984
- summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
1985
- } catch (error: any) {
1986
- summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
1987
- exitCode: typeof error?.code === 'number' ? error.code : null,
1988
- signal: typeof error?.signal === 'string' ? error.signal : null,
1989
- timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
1990
- failureKind: 'dependency_bootstrap_failed',
1991
- }));
1992
- summary.status = 'failed';
1993
- summary.failureKind = 'dependency_bootstrap_failed';
1994
- summary.failureCode = 'dependency_bootstrap_failed';
1995
- return summary;
2057
+ if (runLegacyBootstrapCommands) {
2058
+ summary.bootstrap = { stage: 'legacy' };
2059
+ for (const candidate of selection.bootstrapCommands) {
2060
+ const startedAt = Date.now();
2061
+ const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
2062
+ const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
2063
+ try {
2064
+ const result = await execFileAsync(candidate.command, candidate.args, {
2065
+ cwd,
2066
+ encoding: 'utf8',
2067
+ timeout,
2068
+ maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
2069
+ env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
2070
+ });
2071
+ summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
2072
+ } catch (error: any) {
2073
+ summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
2074
+ exitCode: typeof error?.code === 'number' ? error.code : null,
2075
+ signal: typeof error?.signal === 'string' ? error.signal : null,
2076
+ timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
2077
+ failureKind: 'dependency_bootstrap_failed',
2078
+ }));
2079
+ summary.bootstrap = { stage: 'failed', error: String(error?.message || error) };
2080
+ summary.status = 'failed';
2081
+ summary.failureKind = 'dependency_bootstrap_failed';
2082
+ summary.failureCode = 'dependency_bootstrap_failed';
2083
+ return summary;
2084
+ }
1996
2085
  }
1997
2086
  }
1998
2087
 
@@ -2000,7 +2089,8 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
2000
2089
  const startedAt = Date.now();
2001
2090
  const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
2002
2091
  const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
2003
- if (selection.bootstrapCommands.length === 0 && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
2092
+ const bootstrapProvidedDependencies = summary.bootstrap?.stage === 'cached' || summary.bootstrap?.stage === 'ran' || summary.bootstrap?.stage === 'legacy';
2093
+ if (!bootstrapProvidedDependencies && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
2004
2094
  summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
2005
2095
  stderr: 'Dependencies appear to be missing: package.json and a lockfile are present, but node_modules is absent. Configure validation.bootstrapCommands in repo mesh/refine config if Refinery should install/bootstrap before validation.',
2006
2096
  }, false, {
@@ -2599,6 +2689,7 @@ export class DaemonCommandRouter {
2599
2689
  mesh: any;
2600
2690
  node: any;
2601
2691
  nodeId: string;
2692
+ force?: boolean;
2602
2693
  }): Promise<{ success: true; skipped?: boolean; removedPath?: string; repoRoot?: string; reason?: string; fallback?: string; forced?: boolean; convergence?: Record<string, unknown> } | { success: false; code: string; error: string; recoveryHint: string; convergence?: Record<string, unknown> }> {
2603
2694
  const workspace = typeof args.node?.workspace === 'string' ? args.node.workspace.trim() : '';
2604
2695
  if (!workspace) {
@@ -2675,15 +2766,13 @@ export class DaemonCommandRouter {
2675
2766
  };
2676
2767
  }
2677
2768
 
2678
- const forceFallbackConvergence = await this.getWorktreeForceCleanupConvergence({
2679
- repoRoot,
2680
- workspace,
2681
- node: args.node,
2682
- });
2769
+ const forceFallbackConvergence = args.force
2770
+ ? { allow: true, status: 'force_override', source: 'caller_force_flag' }
2771
+ : await this.getWorktreeForceCleanupConvergence({ repoRoot, workspace, node: args.node });
2683
2772
 
2684
2773
  try {
2685
2774
  const result = await removeWorktree(repoRoot, workspace, {
2686
- requireClean: true,
2775
+ requireClean: !args.force,
2687
2776
  allowSubmoduleForceFallback: forceFallbackConvergence.allow,
2688
2777
  });
2689
2778
  return {
@@ -2714,7 +2803,7 @@ export class DaemonCommandRouter {
2714
2803
  recoveryHint: dirty
2715
2804
  ? 'Commit, stash, or intentionally discard the worktree changes before retrying mesh_remove_node. The mesh registry entry is preserved until cleanup is safe.'
2716
2805
  : submoduleForceBlocked
2717
- ? 'Verify the worktree branch is merged/contained in the source default branch (for example origin/main) or mark the node with a safe branchConvergence final state before retrying. The mesh registry entry is preserved.'
2806
+ ? 'Verify the worktree branch is merged/contained in the source default branch (for example origin/main) or mark the node with a safe branchConvergence final state, or pass force:true if content is confirmed already in main.'
2718
2807
  : 'Inspect git worktree status/list from the source repo and retry after resolving the reported cleanup failure.',
2719
2808
  ...(submoduleForceBlocked ? { convergence: forceFallbackConvergence } : {}),
2720
2809
  };
@@ -3117,6 +3206,7 @@ export class DaemonCommandRouter {
3117
3206
  jobId?: string;
3118
3207
  interactionId?: string;
3119
3208
  retryOfJobId?: string;
3209
+ coordinatorDaemonId?: string;
3120
3210
  }): MeshRefineJobHandle {
3121
3211
  return {
3122
3212
  success: true,
@@ -3132,6 +3222,7 @@ export class DaemonCommandRouter {
3132
3222
  startedAt: args.startedAt || new Date().toISOString(),
3133
3223
  ...(args.completedAt ? { completedAt: args.completedAt } : {}),
3134
3224
  ...(args.retryOfJobId ? { retryOfJobId: args.retryOfJobId } : {}),
3225
+ ...(args.coordinatorDaemonId ? { targetCoordinatorDaemonId: args.coordinatorDaemonId } : {}),
3135
3226
  eventDelivery: { pendingEvents: true, ledger: true },
3136
3227
  evidence: {
3137
3228
  pendingEventsCommand: 'get_pending_mesh_events',
@@ -3164,6 +3255,7 @@ export class DaemonCommandRouter {
3164
3255
  workspace: handle.workspace,
3165
3256
  metadataEvent,
3166
3257
  queuedAt: Date.now(),
3258
+ ...(handle.targetCoordinatorDaemonId ? { targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId } : {}),
3167
3259
  };
3168
3260
  if (typeof this.deps.instanceManager?.getByCategory === 'function') {
3169
3261
  const forwarded = handleMeshForwardEvent(
@@ -3204,6 +3296,7 @@ export class DaemonCommandRouter {
3204
3296
  meshId: handle.meshId,
3205
3297
  nodeId: handle.targetNodeId,
3206
3298
  targetDaemonId: handle.targetDaemonId,
3299
+ targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId,
3207
3300
  workspace: handle.workspace,
3208
3301
  startedAt: handle.startedAt,
3209
3302
  completedAt: handle.completedAt,
@@ -3223,6 +3316,47 @@ export class DaemonCommandRouter {
3223
3316
  }
3224
3317
  }
3225
3318
 
3319
+ /**
3320
+ * On daemon restart, scan all mesh ledgers for refine jobs that were dispatched
3321
+ * but never completed/failed (i.e. the daemon died mid-job). Re-queue each one
3322
+ * so the job runs to completion automatically without coordinator intervention.
3323
+ */
3324
+ async resumePendingRefineJobsOnStartup(): Promise<void> {
3325
+ try {
3326
+ const { listMeshes } = await import('../config/mesh-config.js');
3327
+ const { readLedgerEntries } = await import('../mesh/mesh-ledger.js');
3328
+ const meshIds: string[] = listMeshes().map(m => m.id).filter(Boolean) as string[];
3329
+ for (const meshId of meshIds) {
3330
+ const entries = readLedgerEntries(meshId, { kind: ['task_dispatched', 'task_completed', 'task_failed'] });
3331
+ // Build set of nodeIds that already have a terminal entry.
3332
+ const terminal = new Set<string>();
3333
+ for (const e of entries) {
3334
+ if ((e.kind === 'task_completed' || e.kind === 'task_failed') && e.nodeId) {
3335
+ const jobId = (e.payload as any)?.refineJob?.jobId;
3336
+ if (jobId) terminal.add(`${e.nodeId}:${jobId}`);
3337
+ }
3338
+ }
3339
+ // Re-dispatch dispatched jobs with no matching terminal entry.
3340
+ for (const e of entries) {
3341
+ if (e.kind !== 'task_dispatched' || !e.nodeId) continue;
3342
+ const source = (e.payload as any)?.source;
3343
+ if (source !== 'refine_mesh_node_async_job') continue;
3344
+ const jobId = (e.payload as any)?.refineJob?.jobId;
3345
+ if (!jobId || terminal.has(`${e.nodeId}:${jobId}`)) continue;
3346
+ const key = this.buildRefineJobKey(meshId, e.nodeId);
3347
+ if (this.runningRefineJobs.has(key)) continue;
3348
+ const coordinatorDaemonId = (e.payload as any)?.refineJob?.targetCoordinatorDaemonId;
3349
+ LOG.info('Mesh', `[Refinery] Auto-resuming interrupted refine job for node ${e.nodeId} (jobId=${jobId})`);
3350
+ void this.startMeshRefineJob(meshId, e.nodeId, {
3351
+ coordinatorDaemonId,
3352
+ });
3353
+ }
3354
+ }
3355
+ } catch (e: any) {
3356
+ LOG.warn('Mesh', `[Refinery] resumePendingRefineJobsOnStartup failed: ${e?.message || e}`);
3357
+ }
3358
+ }
3359
+
3226
3360
  private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
3227
3361
  const refineStages: Array<Record<string, unknown>> = [];
3228
3362
  try {
@@ -3255,11 +3389,20 @@ export class DaemonCommandRouter {
3255
3389
  const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
3256
3390
  const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
3257
3391
  const baseHead = baseHeadStdout.trim();
3258
- const branchHead = branchHeadStdout.trim();
3392
+ let branchHead = branchHeadStdout.trim();
3259
3393
  recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
3260
3394
 
3261
3395
  const validationStarted = Date.now();
3262
- const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
3396
+ const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace, {
3397
+ // M2-2: consume the node's persisted bootstrap state; persist re-runs.
3398
+ persistedBootstrapState: (node as any).worktreeBootstrap as WorktreeBootstrapState | undefined,
3399
+ onBootstrapStateChange: (state) => {
3400
+ (node as any).worktreeBootstrap = state;
3401
+ void import('../config/mesh-config.js')
3402
+ .then(({ updateNode }) => updateNode(mesh.id, node.id, { worktreeBootstrap: state } as any))
3403
+ .catch(() => { /* persistence is best-effort */ });
3404
+ },
3405
+ });
3263
3406
  recordMeshRefineStage(
3264
3407
  refineStages,
3265
3408
  'validation',
@@ -3313,7 +3456,7 @@ export class DaemonCommandRouter {
3313
3456
  }
3314
3457
 
3315
3458
  const patchEquivalenceStarted = Date.now();
3316
- const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
3459
+ let patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
3317
3460
  recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
3318
3461
  equivalent: patchEquivalence.equivalent,
3319
3462
  expectedPatchId: patchEquivalence.expectedPatchId,
@@ -3322,26 +3465,171 @@ export class DaemonCommandRouter {
3322
3465
  actionableHint: patchEquivalence.actionableHint,
3323
3466
  });
3324
3467
  if (!patchEquivalence.equivalent) {
3325
- return {
3326
- success: false,
3327
- code: 'patch_equivalence_failed',
3328
- convergenceStatus: 'blocked_review',
3329
- error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
3330
- branch,
3331
- into: baseBranch,
3332
- validationSummary,
3333
- patchEquivalence,
3334
- refineStages,
3335
- finalBranchConvergenceState: {
3336
- branch,
3337
- baseBranch,
3338
- merged: false,
3339
- removed: false,
3340
- validation: 'passed',
3341
- patchEquivalence: 'failed',
3342
- status: 'blocked_review',
3343
- },
3344
- };
3468
+ // Auto-rebase: if branch is simply behind base, attempt rebase automatically before failing.
3469
+ let didAutoRebase = false;
3470
+ let isBehindBase = false;
3471
+ try {
3472
+ execFileSync('git', ['merge-base', '--is-ancestor', branchHead, baseHead], {
3473
+ cwd: node.workspace,
3474
+ stdio: 'ignore',
3475
+ });
3476
+ isBehindBase = true;
3477
+ } catch { /* non-zero exit means branchHead is not an ancestor of baseHead */ }
3478
+
3479
+ if (isBehindBase) {
3480
+ const autoRebaseStarted = Date.now();
3481
+ try {
3482
+ execFileSync('git', ['rebase', baseHead], {
3483
+ cwd: node.workspace,
3484
+ stdio: ['ignore', 'pipe', 'pipe'],
3485
+ });
3486
+ const { stdout: rebasedHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: node.workspace, encoding: 'utf8' });
3487
+ branchHead = rebasedHeadStdout.trim();
3488
+ const rebasedPatchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
3489
+ recordMeshRefineStage(refineStages, 'patch_equivalence_after_auto_rebase', rebasedPatchEquivalence.status, autoRebaseStarted, {
3490
+ equivalent: rebasedPatchEquivalence.equivalent,
3491
+ expectedPatchId: rebasedPatchEquivalence.expectedPatchId,
3492
+ actualPatchId: rebasedPatchEquivalence.actualPatchId,
3493
+ error: rebasedPatchEquivalence.error,
3494
+ rebasedBranchHead: branchHead,
3495
+ });
3496
+ if (rebasedPatchEquivalence.equivalent) {
3497
+ patchEquivalence = rebasedPatchEquivalence;
3498
+ didAutoRebase = true;
3499
+ } else {
3500
+ return {
3501
+ success: false,
3502
+ code: 'needs_rebase',
3503
+ convergenceStatus: 'blocked_review',
3504
+ error: 'Branch was rebased onto base but patch equivalence still failed; manual intervention required.',
3505
+ branch,
3506
+ into: baseBranch,
3507
+ validationSummary,
3508
+ patchEquivalence: rebasedPatchEquivalence,
3509
+ refineStages,
3510
+ finalBranchConvergenceState: {
3511
+ branch,
3512
+ baseBranch,
3513
+ merged: false,
3514
+ removed: false,
3515
+ validation: 'passed',
3516
+ patchEquivalence: 'failed',
3517
+ status: 'blocked_review',
3518
+ },
3519
+ };
3520
+ }
3521
+ } catch (rebaseErr: any) {
3522
+ try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
3523
+ recordMeshRefineStage(refineStages, 'patch_equivalence_after_auto_rebase', 'failed', autoRebaseStarted, {
3524
+ error: rebaseErr?.message || String(rebaseErr),
3525
+ });
3526
+ return {
3527
+ success: false,
3528
+ code: 'needs_rebase_with_conflicts',
3529
+ convergenceStatus: 'blocked_review',
3530
+ error: 'Branch is behind base and auto-rebase failed due to conflicts; resolve conflicts manually and retry.',
3531
+ branch,
3532
+ into: baseBranch,
3533
+ validationSummary,
3534
+ patchEquivalence,
3535
+ refineStages,
3536
+ finalBranchConvergenceState: {
3537
+ branch,
3538
+ baseBranch,
3539
+ merged: false,
3540
+ removed: false,
3541
+ validation: 'passed',
3542
+ patchEquivalence: 'failed',
3543
+ status: 'blocked_review',
3544
+ },
3545
+ };
3546
+ }
3547
+ }
3548
+
3549
+ // If the actual patch-id is empty, the merge-tree produces no diff vs base —
3550
+ // meaning the branch content is already present in base (landed via a different
3551
+ // path, e.g. cherry-pick or direct commit). Treat this as "already merged":
3552
+ // skip the merge step but still run cleanup so the worktree node is removed.
3553
+ // "already merged via another path": the branch has real changes
3554
+ // (expectedPatchId non-empty) but the merge-tree produces no diff
3555
+ // against base (actualPatchId empty) — meaning every change in the
3556
+ // branch is already present in base via a cherry-pick or direct commit.
3557
+ // If both patch-ids are empty, the branch itself has no changes; that
3558
+ // is a degenerate worktree case, not an "already merged" scenario.
3559
+ const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
3560
+ if (!didAutoRebase && !alreadyMergedViaOtherPath) {
3561
+ return {
3562
+ success: false,
3563
+ code: 'patch_equivalence_failed',
3564
+ convergenceStatus: 'blocked_review',
3565
+ error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
3566
+ branch,
3567
+ into: baseBranch,
3568
+ validationSummary,
3569
+ patchEquivalence,
3570
+ refineStages,
3571
+ finalBranchConvergenceState: {
3572
+ branch,
3573
+ baseBranch,
3574
+ merged: false,
3575
+ removed: false,
3576
+ validation: 'passed',
3577
+ patchEquivalence: 'failed',
3578
+ status: 'blocked_review',
3579
+ },
3580
+ };
3581
+ }
3582
+
3583
+ if (!didAutoRebase && alreadyMergedViaOtherPath) {
3584
+ // Content already in base — skip merge, go straight to cleanup.
3585
+ recordMeshRefineStage(refineStages, 'merge', 'skipped', Date.now(), {
3586
+ reason: 'already_merged_via_other_path',
3587
+ note: 'actualPatchId is empty; branch content is already present in base via a different commit path',
3588
+ });
3589
+ const cleanupStarted = Date.now();
3590
+ const removeResult = await this.execute('remove_mesh_node', {
3591
+ meshId,
3592
+ nodeId,
3593
+ sessionCleanupMode: 'preserve',
3594
+ inlineMesh: args?.inlineMesh,
3595
+ });
3596
+ recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
3597
+ removed: removeResult?.removed,
3598
+ code: removeResult?.code,
3599
+ error: removeResult?.error,
3600
+ });
3601
+ try {
3602
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
3603
+ appendLedgerEntry(meshId, {
3604
+ kind: 'node_removed',
3605
+ nodeId,
3606
+ payload: { alreadyMergedViaOtherPath: true, branch, into: baseBranch, validationSummary, patchEquivalence },
3607
+ });
3608
+ } catch { /* ledger append is best-effort */ }
3609
+ return {
3610
+ success: removeResult?.success !== false,
3611
+ code: 'already_merged',
3612
+ merged: false,
3613
+ alreadyMergedViaOtherPath: true,
3614
+ branch,
3615
+ into: baseBranch,
3616
+ removeResult,
3617
+ validationSummary,
3618
+ patchEquivalence,
3619
+ refineStages,
3620
+ finalBranchConvergenceState: {
3621
+ branch: baseBranch,
3622
+ mergedBranch: branch,
3623
+ baseBranch,
3624
+ merged: false,
3625
+ alreadyMergedViaOtherPath: true,
3626
+ removed: removeResult?.success !== false,
3627
+ validation: 'passed',
3628
+ patchEquivalence: 'already_merged',
3629
+ status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged_to_main',
3630
+ },
3631
+ };
3632
+ }
3345
3633
  }
3346
3634
 
3347
3635
  const submoduleReachabilityStarted = Date.now();
@@ -3620,23 +3908,61 @@ export class DaemonCommandRouter {
3620
3908
  result = { success: false, error: e?.message || String(e) };
3621
3909
  }
3622
3910
  const completedAt = new Date().toISOString();
3911
+
3912
+ // B1: Discriminated terminal status — do not rely solely on result.success.
3913
+ // Map known failure codes to structured terminal kinds.
3914
+ type RefineTerminalKind = 'completed' | 'blocked_review' | 'validation_failed' | 'submodule_reachability_failed' | 'merge_failed' | 'cleanup_failed';
3915
+ const refineCode = typeof result.code === 'string' ? result.code : '';
3916
+ const refineTerminalKind: RefineTerminalKind = result.success === true
3917
+ ? 'completed'
3918
+ : refineCode === 'blocked_review'
3919
+ ? 'blocked_review'
3920
+ : refineCode === 'validation_failed' || refineCode === 'validation_dependencies_missing'
3921
+ ? 'validation_failed'
3922
+ : refineCode === 'submodule_reachability_failed'
3923
+ ? 'submodule_reachability_failed'
3924
+ : refineCode === 'merge_failed' || refineCode === 'patch_equivalence_failed' || refineCode === 'needs_rebase' || refineCode === 'needs_rebase_with_conflicts'
3925
+ ? 'merge_failed'
3926
+ : refineCode === 'cleanup_failed'
3927
+ ? 'cleanup_failed'
3928
+ : 'merge_failed'; // fallback for unclassified failures
3929
+ const isTerminalSuccess = refineTerminalKind === 'completed';
3930
+ const normalizedResult = {
3931
+ ...result,
3932
+ terminalKind: refineTerminalKind,
3933
+ ...(result.nextStep === undefined && !isTerminalSuccess ? {
3934
+ nextStep: refineTerminalKind === 'blocked_review'
3935
+ ? 'Request user review/approval before attempting to merge again.'
3936
+ : refineTerminalKind === 'validation_failed'
3937
+ ? 'Fix failing tests or configure validation.bootstrapCommands and retry mesh_refine_node.'
3938
+ : refineTerminalKind === 'submodule_reachability_failed'
3939
+ ? 'Push unreachable submodule commits to origin/main, then retry mesh_refine_node.'
3940
+ : refineTerminalKind === 'merge_failed'
3941
+ ? 'Resolve merge conflicts or patch equivalence issues, then retry mesh_refine_node.'
3942
+ : refineTerminalKind === 'cleanup_failed'
3943
+ ? 'Manually remove the worktree and retry or use mesh_remove_node.'
3944
+ : 'Inspect refineStages for the failing stage and retry.',
3945
+ } : {}),
3946
+ };
3947
+
3623
3948
  const terminalHandle = this.buildRefineJobHandle({
3624
3949
  meshId: handle.meshId,
3625
3950
  nodeId: handle.targetNodeId,
3626
- status: result.success === true ? 'completed' : 'failed',
3951
+ status: isTerminalSuccess ? 'completed' : 'failed',
3627
3952
  startedAt: handle.startedAt,
3628
3953
  completedAt,
3629
3954
  jobId: handle.jobId,
3630
3955
  interactionId: handle.interactionId,
3631
3956
  retryOfJobId: handle.retryOfJobId,
3632
3957
  node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
3958
+ coordinatorDaemonId: handle.targetCoordinatorDaemonId,
3633
3959
  });
3634
- const terminal: MeshRefineTerminalJob = { ...terminalHandle, result };
3960
+ const terminal: MeshRefineTerminalJob = { ...terminalHandle, result: normalizedResult };
3635
3961
  this.terminalRefineJobs.set(key, terminal);
3636
3962
  this.runningRefineJobs.delete(key);
3637
3963
  this.invalidateAggregateMeshStatus(handle.meshId);
3638
- await this.appendRefineJobLedger(result.success === true ? 'task_completed' : 'task_failed', terminalHandle, result);
3639
- this.queueRefineJobEvent(result.success === true ? 'refine:completed' : 'refine:failed', terminalHandle, result);
3964
+ await this.appendRefineJobLedger(isTerminalSuccess ? 'task_completed' : 'task_failed', terminalHandle, normalizedResult);
3965
+ this.queueRefineJobEvent(isTerminalSuccess ? 'refine:completed' : 'refine:failed', terminalHandle, normalizedResult);
3640
3966
  }
3641
3967
 
3642
3968
  private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
@@ -3651,7 +3977,12 @@ export class DaemonCommandRouter {
3651
3977
  if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
3652
3978
  if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
3653
3979
 
3654
- const handle = this.buildRefineJobHandle({ meshId, nodeId, node, retryOfJobId: terminal?.jobId });
3980
+ // Capture the caller's coordinator daemon ID so completed/failed events are
3981
+ // scoped to that coordinator's pending-events queue and survive daemon restarts.
3982
+ const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
3983
+ ? args.coordinatorDaemonId.trim()
3984
+ : (this.deps.statusInstanceId || undefined);
3985
+ const handle = this.buildRefineJobHandle({ meshId, nodeId, node, retryOfJobId: terminal?.jobId, coordinatorDaemonId });
3655
3986
  this.runningRefineJobs.set(key, handle);
3656
3987
  await this.appendRefineJobLedger('task_dispatched', handle);
3657
3988
  this.queueRefineJobEvent('refine:accepted', handle);
@@ -4815,11 +5146,17 @@ export class DaemonCommandRouter {
4815
5146
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
4816
5147
  if (!meshId) return { success: false, error: 'meshId required' };
4817
5148
  try {
4818
- const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
5149
+ const { getMeshQueueStats, getQueue, describeTaskDependencyState } = await import('../mesh/mesh-work-queue.js');
4819
5150
  const status = Array.isArray(args?.status)
4820
5151
  ? args.status.map((s: any) => typeof s === 'string' ? s.trim() : '').filter(Boolean)
4821
5152
  : undefined;
4822
- const queue = getQueue(meshId, { status: status as any });
5153
+ const rawQueue = getQueue(meshId, { status: status as any });
5154
+ // M1: annotate dependency state at view time (waitingOn / dependenciesSatisfied).
5155
+ const statusById = new Map(getQueue(meshId).map(task => [task.id, task.status]));
5156
+ const queue = rawQueue.map(task =>
5157
+ Array.isArray(task.dependsOn) && task.dependsOn.length > 0
5158
+ ? { ...task, ...describeTaskDependencyState(task, statusById) }
5159
+ : task);
4823
5160
  const summary = getMeshQueueStats(meshId);
4824
5161
  return {
4825
5162
  success: true,
@@ -5105,7 +5442,7 @@ export class DaemonCommandRouter {
5105
5442
 
5106
5443
  let worktreeCleanup: Record<string, unknown> | undefined;
5107
5444
  if (node?.isLocalWorktree) {
5108
- const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId });
5445
+ const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId, force: args?.force === true });
5109
5446
  if (cleanupResult.success === false) {
5110
5447
  return {
5111
5448
  success: false,
@@ -5281,6 +5618,40 @@ export class DaemonCommandRouter {
5281
5618
  { timeoutMs: 120000 },
5282
5619
  );
5283
5620
  submodulesInitialized = true;
5621
+
5622
+ // Sync oss submodule to source node HEAD (best-effort)
5623
+ const sourceWorkspace = sourceNode.repoRoot || sourceNode.workspace;
5624
+ if (sourceWorkspace) {
5625
+ try {
5626
+ const { runGit: rg } = await import('../git/git-executor.js');
5627
+ const sourceCtx = { workspace: sourceWorkspace, repoRoot: sourceWorkspace, isGitRepo: true };
5628
+ const worktreeCtx = { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true };
5629
+
5630
+ // Read source node's oss submodule SHA
5631
+ const sourceStatusOut = await rg(sourceCtx, ['submodule', 'status', 'oss'], { timeoutMs: 10000 });
5632
+ const sourceStatusLine = (typeof sourceStatusOut === 'string' ? sourceStatusOut : (sourceStatusOut as any)?.stdout ?? '').trim();
5633
+ const sourceShaMatch = sourceStatusLine.match(/^[+\- ]?([0-9a-f]{40})/);
5634
+ const sourceSha = sourceShaMatch?.[1];
5635
+
5636
+ if (sourceSha) {
5637
+ // Read worktree's current oss HEAD
5638
+ const ossCtx = { workspace: `${result.worktreePath}/oss`, repoRoot: `${result.worktreePath}/oss`, isGitRepo: true };
5639
+ const worktreeOssHeadOut = await rg(ossCtx, ['rev-parse', 'HEAD'], { timeoutMs: 10000 });
5640
+ const worktreeOssSha = (typeof worktreeOssHeadOut === 'string' ? worktreeOssHeadOut : (worktreeOssHeadOut as any)?.stdout ?? '').trim();
5641
+
5642
+ if (worktreeOssSha !== sourceSha) {
5643
+ // Fetch target SHA from source node's oss directory
5644
+ await rg(ossCtx, ['fetch', `${sourceWorkspace}/oss`, 'HEAD'], { timeoutMs: 60000 });
5645
+ await rg(ossCtx, ['checkout', sourceSha], { timeoutMs: 10000 });
5646
+ await rg(worktreeCtx, ['add', 'oss'], { timeoutMs: 10000 });
5647
+ await rg(worktreeCtx, ['commit', '-m', 'chore: sync oss to source node HEAD on clone'], { timeoutMs: 10000 });
5648
+ console.log(`[mesh] Synced oss submodule to source HEAD ${sourceSha.slice(0, 8)} in worktree`);
5649
+ }
5650
+ }
5651
+ } catch (ossErr: any) {
5652
+ console.warn('[mesh] oss submodule sync to source HEAD failed (best-effort):', ossErr.message);
5653
+ }
5654
+ }
5284
5655
  } catch (subErr: any) {
5285
5656
  // Submodule init is best-effort; don't fail the clone
5286
5657
  console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
@@ -5342,6 +5713,63 @@ export class DaemonCommandRouter {
5342
5713
  return { success: false, error: e.message };
5343
5714
  }
5344
5715
  }
5716
+ case 'retry_mesh_node_bootstrap': {
5717
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
5718
+ const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
5719
+ if (!meshId) return { success: false, error: 'meshId required' };
5720
+ if (!nodeId) return { success: false, error: 'nodeId required' };
5721
+ const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'bootstrap retry');
5722
+ if (ownerFailure) return ownerFailure;
5723
+
5724
+ try {
5725
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
5726
+ const mesh = meshRecord?.mesh;
5727
+ if (!mesh) return { success: false, error: 'Mesh not found' };
5728
+
5729
+ const node = mesh.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
5730
+ if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
5731
+ if (!node.isLocalWorktree) return { success: false, error: 'Node is not a local worktree node' };
5732
+
5733
+ const currentBootstrap = node.worktreeBootstrap as WorktreeBootstrapState | undefined;
5734
+ if (currentBootstrap?.status === 'running') {
5735
+ return { success: false, error: 'Bootstrap is already running for this node' };
5736
+ }
5737
+
5738
+ const worktreePath: string = node.workspace || node.repoRoot;
5739
+ if (!worktreePath) return { success: false, error: 'Node has no workspace path' };
5740
+
5741
+ const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, worktreePath);
5742
+ const runningState: WorktreeBootstrapState = {
5743
+ status: 'running',
5744
+ required: loadedBootstrap.config?.required !== false,
5745
+ configSource: loadedBootstrap.path || loadedBootstrap.source,
5746
+ configSourceType: loadedBootstrap.sourceType,
5747
+ startedAt: new Date().toISOString(),
5748
+ };
5749
+
5750
+ const persistState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
5751
+ node.worktreeBootstrap = bootstrapState;
5752
+ if (meshRecord.inline) {
5753
+ this.updateInlineMeshNode(meshId, mesh, node);
5754
+ return;
5755
+ }
5756
+ try {
5757
+ const { updateNode } = await import('../config/mesh-config.js');
5758
+ updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
5759
+ this.invalidateAggregateMeshStatus(meshId);
5760
+ } catch { /* best-effort */ }
5761
+ };
5762
+
5763
+ await persistState(runningState);
5764
+ const bootstrapState = await runMeshWorktreeBootstrap(mesh, worktreePath);
5765
+ await persistState(bootstrapState);
5766
+
5767
+ return { success: true, bootstrapState };
5768
+ } catch (e: any) {
5769
+ return { success: false, error: e.message };
5770
+ }
5771
+ }
5772
+
5345
5773
  case 'trigger_mesh_queue': {
5346
5774
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
5347
5775
  if (!meshId) return { success: false, error: 'meshId required' };
@@ -5377,6 +5805,12 @@ export class DaemonCommandRouter {
5377
5805
 
5378
5806
  try {
5379
5807
  const { buildCoordinatorSystemPrompt } = await import('../mesh/coordinator-prompt.js');
5808
+ const { buildMissionPromptSection } = await import('../mesh/mesh-missions.js');
5809
+ // M3-3: inject the active mission summary into the coordinator prompt.
5810
+ // Best-effort — a store failure must not block coordinator launch.
5811
+ const buildMissionSectionBestEffort = (id: string): string => {
5812
+ try { return buildMissionPromptSection(id); } catch { return ''; }
5813
+ };
5380
5814
 
5381
5815
  // Support inline mesh data from cloud (bypasses local meshes.json lookup)
5382
5816
  let mesh: any;
@@ -5483,7 +5917,7 @@ export class DaemonCommandRouter {
5483
5917
  // Build coordinator prompt first — fail closed on errors.
5484
5918
  let cliCmdSystemPrompt = '';
5485
5919
  try {
5486
- cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
5920
+ cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
5487
5921
  } catch (error: any) {
5488
5922
  const message = error?.message || String(error);
5489
5923
  LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
@@ -5701,7 +6135,7 @@ export class DaemonCommandRouter {
5701
6135
  // broken mesh state is visible instead of silently launching with weaker rules.
5702
6136
  let systemPrompt = '';
5703
6137
  try {
5704
- systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
6138
+ systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
5705
6139
  } catch (error: any) {
5706
6140
  const message = error?.message || String(error);
5707
6141
  LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
@@ -6346,6 +6780,16 @@ export class DaemonCommandRouter {
6346
6780
  ...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
6347
6781
  ...(historicalSessions ? { historicalSessions } : {}),
6348
6782
  ...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
6783
+ activeRefineJobs: Array.from(this.runningRefineJobs.values())
6784
+ .filter(job => job.meshId === meshId)
6785
+ .map(job => ({
6786
+ jobId: job.jobId,
6787
+ nodeId: job.targetNodeId,
6788
+ workspace: job.workspace,
6789
+ startedAt: job.startedAt,
6790
+ status: job.status,
6791
+ targetCoordinatorDaemonId: job.targetCoordinatorDaemonId,
6792
+ })),
6349
6793
  };
6350
6794
  const { pendingCoordinatorEvents: _pendingCoordinatorEvents, ...cacheableStatusResult } = statusResult as any;
6351
6795
  const rememberedStatus = this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
@@ -6367,6 +6811,90 @@ export class DaemonCommandRouter {
6367
6811
  }
6368
6812
  }
6369
6813
 
6814
+ case 'get_mesh_review_inbox': {
6815
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
6816
+ if (!meshId) return { success: false, error: 'meshId required' };
6817
+ try {
6818
+ const { deriveMeshReviewInboxItems } = await import('../mesh/mesh-review-inbox.js');
6819
+ const { readLedgerEntries } = await import('../mesh/mesh-ledger.js');
6820
+ const { getGitDiffSummary } = await import('../git/git-diff.js');
6821
+ const { existsSync } = await import('node:fs');
6822
+
6823
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
6824
+ const mesh = meshRecord?.mesh;
6825
+ if (!mesh) return { success: false, error: 'Mesh not found' };
6826
+
6827
+ // Ensure we have a fresh aggregate status so nodeStatuses carry
6828
+ // computed fields (connection.state, branchConvergence, isLocalWorktree)
6829
+ // that the raw mesh.nodes config objects don't have.
6830
+ // When the caller provides an inlineMesh, prefer its nodes directly
6831
+ // (they already carry the computed fields from the coordinator).
6832
+ const inlineNodes = args?.inlineMesh && Array.isArray((args.inlineMesh as any)?.nodes)
6833
+ ? (args.inlineMesh as any).nodes as Record<string, unknown>[]
6834
+ : null;
6835
+ let cachedStatus = !inlineNodes ? this.getCachedAggregateMeshStatus(meshId, mesh, {}) : null;
6836
+ if (!cachedStatus && !inlineNodes) {
6837
+ const freshStatus = await this.execute('mesh_status', {
6838
+ meshId,
6839
+ inlineMesh: args?.inlineMesh,
6840
+ refresh: true,
6841
+ }, 'get_mesh_review_inbox');
6842
+ cachedStatus = (freshStatus?.success !== false) ? freshStatus : null;
6843
+ }
6844
+ const nodeStatuses: Record<string, unknown>[] = inlineNodes
6845
+ ? inlineNodes
6846
+ : Array.isArray(cachedStatus?.nodes)
6847
+ ? cachedStatus.nodes as Record<string, unknown>[]
6848
+ : Array.isArray(mesh.nodes)
6849
+ ? mesh.nodes as Record<string, unknown>[]
6850
+ : [];
6851
+
6852
+ const ledgerEntries = readLedgerEntries(meshId, { tail: 300 });
6853
+ const derivation = deriveMeshReviewInboxItems({ nodes: nodeStatuses, ledgerEntries });
6854
+
6855
+ for (const item of derivation.items) {
6856
+ const workspace = item.workspace;
6857
+ if (!workspace || !existsSync(workspace)) continue;
6858
+ const baseRef = item.defaultBranch
6859
+ ? `origin/${item.defaultBranch}`
6860
+ : 'origin/main';
6861
+ try {
6862
+ const diffResult = await getGitDiffSummary(workspace, { baseRef, maxFiles: 100 });
6863
+ if (diffResult.isGitRepo) {
6864
+ item.diffSummary = {
6865
+ baseRef,
6866
+ files: diffResult.files.map(f => ({
6867
+ path: f.path,
6868
+ status: f.status,
6869
+ insertions: f.insertions,
6870
+ deletions: f.deletions,
6871
+ binary: f.binary,
6872
+ oldPath: f.oldPath,
6873
+ })),
6874
+ totalFiles: diffResult.files.length,
6875
+ totalInsertions: diffResult.totalInsertions,
6876
+ totalDeletions: diffResult.totalDeletions,
6877
+ truncated: diffResult.truncated,
6878
+ ...(diffResult.error ? { error: diffResult.error } : {}),
6879
+ };
6880
+ }
6881
+ } catch {
6882
+ item.diffSummary = null;
6883
+ }
6884
+ }
6885
+
6886
+ return {
6887
+ success: true,
6888
+ meshId,
6889
+ inbox: derivation.items,
6890
+ remoteNodesExcluded: derivation.remoteNodesExcluded,
6891
+ excludedRemoteNodeIds: derivation.excludedRemoteNodeIds,
6892
+ };
6893
+ } catch (e: any) {
6894
+ return { success: false, error: e.message };
6895
+ }
6896
+ }
6897
+
6370
6898
  default:
6371
6899
  break;
6372
6900
  }