@adhdev/daemon-core 0.9.82-rc.315 → 0.9.82-rc.317

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.
@@ -34,6 +34,28 @@ export interface AcpAdapterHandle {
34
34
  setConfigOption?(configId: string, value: string): Promise<void>;
35
35
  resolvePermission?(approved: boolean): Promise<void>;
36
36
  }
37
+ /**
38
+ * Launch metadata for a CLI session, surfaced by the dashboard Session info panel.
39
+ * Derived from the live adapter's spawn plan — the resolved binary, the full
40
+ * argument vector (provider base args + per-launch extra args), the cwd, and the
41
+ * set of per-launch extra-env KEYS (values are intentionally omitted so secrets in
42
+ * extraEnv are never sent to the dashboard). providerSessionId is the upstream
43
+ * agent's own session id once the CLI reports it.
44
+ */
45
+ export interface CliLaunchInfo {
46
+ /** Resolved executable path the PTY actually spawns. */
47
+ command?: string;
48
+ /** Full argument vector (provider spawn.args + extraArgs, {{workingDir}} expanded). */
49
+ args: string[];
50
+ /** Per-launch extra args only (subset of args), for attribution. */
51
+ extraArgs: string[];
52
+ /** Working directory the session was spawned in. */
53
+ cwd: string;
54
+ /** KEYS of per-launch extra env (values omitted — may contain secrets). */
55
+ extraEnvKeys: string[];
56
+ /** Upstream agent session id, once the CLI reports one. */
57
+ providerSessionId?: string;
58
+ }
37
59
  export interface CliAdapter {
38
60
  cliType: string;
39
61
  cliName: string;
@@ -69,6 +91,7 @@ export interface CliAdapter {
69
91
  setOnPtyData?(callback: (data: string) => void): void;
70
92
  writeRaw?(data: string): void;
71
93
  resize?(cols: number, rows: number): void;
94
+ getLaunchInfo?(): CliLaunchInfo;
72
95
  getRuntimeMetadata?(): unknown;
73
96
  updateRuntimeMeta?(meta: Record<string, unknown>): void;
74
97
  refreshProviderDefinition?(provider: unknown): void;
@@ -13,7 +13,7 @@
13
13
  * type, name, category: 'cli', binary, spawn, approvalKeys
14
14
  * compatibility: [{ ideVersion, scriptDir }] ← versioned scripts
15
15
  */
16
- import type { CliAdapter } from '../cli-adapter-types.js';
16
+ import type { CliAdapter, CliLaunchInfo } from '../cli-adapter-types.js';
17
17
  import type { InteractivePromptResponse } from '../providers/types/interactive-prompt.js';
18
18
  import { type PtyRuntimeMetadata, type PtyTransportFactory } from './pty-transport.js';
19
19
  import { type CliProviderModule, type CliScripts, type CliSessionStatus, type CliTraceEntry, type ParsedSession } from './provider-cli-shared.js';
@@ -190,6 +190,14 @@ export declare class ProviderCliAdapter implements CliAdapter {
190
190
  getPartialResponse(): string;
191
191
  getDebugSnapshot(): Record<string, unknown>;
192
192
  getRuntimeMetadata(): PtyRuntimeMetadata | null;
193
+ /**
194
+ * Launch metadata for the dashboard Session info panel. Re-derives the spawn plan
195
+ * (pure — same inputs the live PTY was spawned with) so the dashboard sees the
196
+ * resolved binary, full arg vector, and cwd without us having to persist it.
197
+ * extraEnv values are intentionally dropped (keys only) so secrets passed at
198
+ * launch time never reach the dashboard.
199
+ */
200
+ getLaunchInfo(): CliLaunchInfo;
193
201
  updateRuntimeMeta(meta: Record<string, unknown>, replace?: boolean): void;
194
202
  cancel(): void;
195
203
  saveAndStop(): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -7,8 +7,8 @@ export type { ChatBubbleState, ChatMessage, ExtensionInfo, CommandResult as Core
7
7
  export type { SessionEntry, CompactSessionEntry, CompactDaemonEntry, CloudDaemonSummaryEntry, DashboardBootstrapDaemonEntry, VersionUpdateReason, CloudStatusReportPayload, DaemonStatusEventPayload, DashboardStatusEventPayload, SessionTransport, SessionKind, SessionCapability, AgentSessionStream, ReadChatCursor, ReadChatSyncResult, TransportTopic, SessionChatTailSubscriptionParams, SessionRuntimeOutputSubscriptionParams, MachineRuntimeSubscriptionParams, SessionHostDiagnosticsSubscriptionParams, SessionModalSubscriptionParams, DaemonMetadataSubscriptionParams, WorkspaceGitSubscriptionParams, SessionChatTailUpdate, MachineRuntimeUpdate, SessionHostDiagnosticsUpdate, SessionModalUpdate, DaemonMetadataUpdate, TopicUpdateEnvelope, SubscribeRequest, UnsubscribeRequest, StandaloneWsStatusPayload, AvailableProviderInfo, AcpConfigOption, AcpMode, ProviderControlSchema, StatusReportPayload, MachineInfo, SessionHostDiagnosticsSnapshot, SessionHostRecord, SessionHostWriteOwner, SessionHostAttachedClient, SessionHostLogEntry, SessionHostRequestTrace, SessionHostRuntimeTransition, DetectedIdeInfo, WorkspaceEntry, ProviderSummaryItem, ProviderSummaryMetadata, ProviderState, ProviderStatus, ProviderErrorReason, SessionActiveChatData, ActiveChatData, IdeProviderState, CliProviderState, AcpProviderState, ExtensionProviderState, MessageInputSupport, InputMediaStrategyDescriptor, InputAttachmentStrategy, InputMediaType, } from './shared-types.js';
8
8
  export type { InteractivePrompt, InteractiveQuestion, InteractiveOption, InteractivePromptResponse, InteractiveAnswer, } from './providers/types/interactive-prompt.js';
9
9
  export { normalizeInteractivePrompt, normalizeInteractivePromptResponse, buildClaudeInteractiveToolResult, interactivePromptFromClaudeAskUserQuestion, detectClaudeAskUserQuestionPromptFromJson, } from './providers/types/interactive-prompt.js';
10
- export type { RepoMesh, RepoMeshDaemonRole, RepoMeshHostMetadata, RepoMeshHostPairingMetadata, RepoMeshHostStatus, RepoMeshNode, RepoMeshNodeHealth, RepoMeshPolicy, RepoMeshNodePolicy, RepoMeshRelatedRepo, RepoMeshNodeCapabilities, DetectedCommand, ProjectContextSnapshot, ProjectContextSource, RepoMeshCoordinatorConfig, LocalMeshConfig, LocalMeshEntry, LocalMeshNodeEntry, RepoMeshStatus, RepoMeshNodeStatus, RepoMeshPeerConnectionStatus, RepoMeshPeerConnectionState, RepoMeshPeerConnectionTransport, RepoMeshSessionStatus, RepoMeshQueueTask, RepoMeshQueueTaskStatus, RepoMeshQueueSummary, RepoMeshQueueStatus, RepoMeshLedgerEntryStatus, RepoMeshLedgerSummaryStatus, RepoMeshLedgerStatus, MeshAsyncJobLifecycle, RepoMeshSchedulingStrategy, } from './repo-mesh-types.js';
11
- export { DEFAULT_MESH_POLICY, resolveDelegatedWorkerAutoApprove, MESH_SCHEDULING_STRATEGIES, DEFAULT_MESH_SCHEDULING_STRATEGY, normalizeMeshSchedulingStrategy, resolveNodeSchedulingPriority, MESH_CONVERGE_REFINE_TAG, MESH_CONVERGE_FAST_FORWARD_TAG, resolveAutoConvergeCodeChange, } from './repo-mesh-types.js';
10
+ export type { RepoMesh, RepoMeshDaemonRole, RepoMeshHostMetadata, RepoMeshHostPairingMetadata, RepoMeshHostStatus, RepoMeshNode, RepoMeshNodeHealth, RepoMeshPolicy, RepoMeshNodePolicy, RepoMeshRelatedRepo, RepoMeshNodeCapabilities, DetectedCommand, ProjectContextSnapshot, ProjectContextSource, RepoMeshCoordinatorConfig, LocalMeshConfig, LocalMeshEntry, LocalMeshNodeEntry, RepoMeshStatus, RepoMeshNodeStatus, RepoMeshPeerConnectionStatus, RepoMeshPeerConnectionState, RepoMeshPeerConnectionTransport, RepoMeshSessionStatus, RepoMeshQueueTask, RepoMeshQueueTaskStatus, RepoMeshQueueSummary, RepoMeshQueueStatus, RepoMeshLedgerEntryStatus, RepoMeshLedgerSummaryStatus, RepoMeshLedgerStatus, MeshAsyncJobLifecycle, RepoMeshSchedulingStrategy, RepoMeshTaskAffinityPolicy, StandardMeshRole, } from './repo-mesh-types.js';
11
+ export { DEFAULT_MESH_POLICY, resolveDelegatedWorkerAutoApprove, MESH_SCHEDULING_STRATEGIES, DEFAULT_MESH_SCHEDULING_STRATEGY, normalizeMeshSchedulingStrategy, resolveNodeSchedulingPriority, MESH_CONVERGE_REFINE_TAG, MESH_CONVERGE_FAST_FORWARD_TAG, resolveAutoConvergeCodeChange, STANDARD_MESH_ROLES, DEFAULT_TASKMODE_ROLE_MAP, resolveTaskAffinityRole, resolveMeshRoleOptions, } from './repo-mesh-types.js';
12
12
  export * from './git/index.js';
13
13
  import type { RuntimeWriteOwner as _RuntimeWriteOwner } from './shared-types-extra.js';
14
14
  import type { RuntimeAttachedClient as _RuntimeAttachedClient } from './shared-types-extra.js';
@@ -49,7 +49,7 @@ export { fastForwardMeshNode } from './mesh/mesh-fast-forward.js';
49
49
  export type { MeshFastForwardNodeArgs, MeshFastForwardPlannedStep, MeshFastForwardResult } from './mesh/mesh-fast-forward.js';
50
50
  export { buildMeshLedgerReconciliationEvidence, buildMeshLedgerReplicaEvidence } from './mesh/mesh-ledger-reconciliation.js';
51
51
  export type { AnyLedgerSlice, MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshLedgerReplicaStatus } from './mesh/mesh-ledger-reconciliation.js';
52
- export { enqueueTask, recordDirectDispatchTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, normalizeMeshCapabilityTags, resolveConvergeRequiredTags, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches, deleteDirectDispatchesByTaskId, recordMeshToolCall, assertNoDependencyCycle, hasPendingDependents, describeTaskDependencyState } from './mesh/mesh-work-queue.js';
52
+ export { enqueueTask, recordDirectDispatchTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, normalizeMeshCapabilityTags, resolveConvergeRequiredTags, resolveTaskAffinityRequiredTags, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches, deleteDirectDispatchesByTaskId, recordMeshToolCall, assertNoDependencyCycle, hasPendingDependents, describeTaskDependencyState } from './mesh/mesh-work-queue.js';
53
53
  export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult, DirectDispatchRecord, MeshToolCallRateResult } from './mesh/mesh-work-queue.js';
54
54
  export { buildCompactStaleDirectWorkSummary, buildMeshActiveWork, buildMeshActiveWorkSummary, classifyStaleDirectForPrune, PRUNABLE_ORPHAN_STALE_REASONS } from './mesh/mesh-active-work.js';
55
55
  export type { StaleDirectPruneClassification } from './mesh/mesh-active-work.js';
package/dist/index.js CHANGED
@@ -40,6 +40,35 @@ function resolveNodeSchedulingPriority(nodePolicy) {
40
40
  const raw = Number(nodePolicy?.schedulingPriority);
41
41
  return Number.isFinite(raw) ? raw : 0;
42
42
  }
43
+ function resolveTaskAffinityRole(taskMode, policy) {
44
+ if (!taskMode) return null;
45
+ if (policy?.enabled === false) return null;
46
+ const override = policy?.byTaskMode && Object.prototype.hasOwnProperty.call(policy.byTaskMode, taskMode) ? policy.byTaskMode[taskMode] : void 0;
47
+ if (typeof override === "string") {
48
+ const trimmed = override.trim().toLowerCase();
49
+ return trimmed ? trimmed : null;
50
+ }
51
+ const fallback = DEFAULT_TASKMODE_ROLE_MAP[taskMode];
52
+ return fallback ?? null;
53
+ }
54
+ function resolveMeshRoleOptions(policy) {
55
+ const out = [...STANDARD_MESH_ROLES];
56
+ const seen = new Set(out);
57
+ const add = (raw) => {
58
+ if (typeof raw !== "string") return;
59
+ const role = raw.trim().toLowerCase();
60
+ if (!role || seen.has(role)) return;
61
+ seen.add(role);
62
+ out.push(role);
63
+ };
64
+ if (policy?.byTaskMode) {
65
+ for (const value of Object.values(policy.byTaskMode)) add(value);
66
+ }
67
+ if (Array.isArray(policy?.customRoles)) {
68
+ for (const value of policy.customRoles) add(value);
69
+ }
70
+ return out;
71
+ }
43
72
  function resolveAutoConvergeCodeChange(policy) {
44
73
  return policy?.autoConvergeCodeChange === true;
45
74
  }
@@ -70,7 +99,7 @@ function resolveProviderMaxParallel(nodePolicy, providerType) {
70
99
  if (!Number.isFinite(raw) || raw < 0) return void 0;
71
100
  return Math.floor(raw);
72
101
  }
73
- var MESH_SCHEDULING_STRATEGIES, DEFAULT_MESH_SCHEDULING_STRATEGY, MESH_CONVERGE_REFINE_TAG, MESH_CONVERGE_FAST_FORWARD_TAG, DEFAULT_MESH_POLICY;
102
+ var MESH_SCHEDULING_STRATEGIES, DEFAULT_MESH_SCHEDULING_STRATEGY, MESH_CONVERGE_REFINE_TAG, MESH_CONVERGE_FAST_FORWARD_TAG, STANDARD_MESH_ROLES, DEFAULT_TASKMODE_ROLE_MAP, DEFAULT_MESH_POLICY;
74
103
  var init_repo_mesh_types = __esm({
75
104
  "src/repo-mesh-types.ts"() {
76
105
  "use strict";
@@ -83,6 +112,14 @@ var init_repo_mesh_types = __esm({
83
112
  DEFAULT_MESH_SCHEDULING_STRATEGY = "first_eligible";
84
113
  MESH_CONVERGE_REFINE_TAG = "converge=refine";
85
114
  MESH_CONVERGE_FAST_FORWARD_TAG = "converge=fast_forward";
115
+ STANDARD_MESH_ROLES = ["investigator", "coder", "validator", "converger"];
116
+ DEFAULT_TASKMODE_ROLE_MAP = {
117
+ live_debug_readonly: "investigator",
118
+ code_change: "coder",
119
+ launch_app: "coder",
120
+ validation: "validator",
121
+ convergence: "converger"
122
+ };
86
123
  DEFAULT_MESH_POLICY = {
87
124
  requirePreTaskCheckpoint: false,
88
125
  requirePostTaskCheckpoint: true,
@@ -316,10 +353,10 @@ function readInjected(value) {
316
353
  }
317
354
  function getDaemonBuildInfo() {
318
355
  if (cached) return cached;
319
- const commit = readInjected(true ? "e28347641639c9d0a1e46a6141e3cfed2bc90ac2" : void 0) ?? "unknown";
320
- const commitShort = readInjected(true ? "e2834764" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
321
- const version = readInjected(true ? "0.9.82-rc.315" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
322
- const builtAt = readInjected(true ? "2026-06-18T05:47:44.462Z" : void 0);
356
+ const commit = readInjected(true ? "b4484284652748bf8096f40a00eb5cb463963576" : void 0) ?? "unknown";
357
+ const commitShort = readInjected(true ? "b4484284" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
358
+ const version = readInjected(true ? "0.9.82-rc.317" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
359
+ const builtAt = readInjected(true ? "2026-06-18T08:55:18.955Z" : void 0);
323
360
  cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
324
361
  return cached;
325
362
  }
@@ -645,14 +682,79 @@ function emptyStatus(workspace, lastCheckedAt, error) {
645
682
  async function getSubmoduleStatuses(repo, options) {
646
683
  if (!repo.repoRoot) return [];
647
684
  try {
648
- const result = await runGit(repo, ["submodule", "status"], options);
649
- const submodules = parseSubmoduleStatusOutput(result.stdout, repo.repoRoot, options.submoduleIgnorePaths);
685
+ const submodules = await deriveSubmoduleGitlinkStatuses(repo, options);
650
686
  await Promise.all(submodules.map((submodule) => enrichSubmoduleWorktreeStatus(repo, submodule, options)));
651
687
  return submodules;
652
688
  } catch {
653
689
  return [];
654
690
  }
655
691
  }
692
+ async function deriveSubmoduleGitlinkStatuses(repo, options) {
693
+ if (!repo.repoRoot) return [];
694
+ const paths = await readSubmodulePaths(repo, options);
695
+ const ignoreSet = new Set(options.submoduleIgnorePaths || []);
696
+ const lastCheckedAt = Date.now();
697
+ const entries = await Promise.all(
698
+ paths.filter((path41) => !ignoreSet.has(path41)).map(async (path41) => {
699
+ const repoPath = repo.repoRoot + "/" + path41;
700
+ const expected = await readGitlinkExpectedSha(repo, path41, options);
701
+ const actual = await readSubmoduleHeadSha(repo, repoPath, options);
702
+ const outOfSync = actual === null ? true : expected !== null && expected !== actual;
703
+ return {
704
+ path: path41,
705
+ // Prefer the recorded gitlink SHA (matches the legacy column); fall back
706
+ // to the checked-out SHA so the field is never empty when both are known.
707
+ commit: expected ?? actual ?? "",
708
+ repoPath,
709
+ dirty: false,
710
+ outOfSync,
711
+ lastCheckedAt
712
+ };
713
+ })
714
+ );
715
+ return entries;
716
+ }
717
+ async function readSubmodulePaths(repo, options) {
718
+ if (!repo.repoRoot) return [];
719
+ const gitmodulesPath = repo.repoRoot + "/.gitmodules";
720
+ try {
721
+ const result = await runGit(
722
+ repo,
723
+ ["config", "--file", gitmodulesPath, "--get-regexp", "^submodule\\..*\\.path$"],
724
+ options
725
+ );
726
+ const paths = [];
727
+ for (const line of result.stdout.split("\n")) {
728
+ const spaceIdx = line.indexOf(" ");
729
+ if (spaceIdx < 0) continue;
730
+ const value = line.slice(spaceIdx + 1).trim();
731
+ if (value) paths.push(value);
732
+ }
733
+ return paths;
734
+ } catch {
735
+ return [];
736
+ }
737
+ }
738
+ async function readGitlinkExpectedSha(repo, submodulePath, options) {
739
+ try {
740
+ const result = await runGit(repo, ["ls-tree", "HEAD", submodulePath], options);
741
+ const line = result.stdout.split("\n").find((l) => l.trim().length > 0);
742
+ if (!line) return null;
743
+ const match = line.match(/^\s*\d+\s+commit\s+([0-9a-f]{40})\b/);
744
+ return match ? match[1] : null;
745
+ } catch {
746
+ return null;
747
+ }
748
+ }
749
+ async function readSubmoduleHeadSha(repo, repoPath, options) {
750
+ try {
751
+ const result = await runGit(repo, ["rev-parse", "HEAD"], { ...options, cwd: repoPath });
752
+ const sha = result.stdout.trim();
753
+ return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
754
+ } catch {
755
+ return null;
756
+ }
757
+ }
656
758
  async function enrichSubmoduleWorktreeStatus(repo, submodule, options) {
657
759
  try {
658
760
  const result = await runGit(repo, ["status", "--porcelain=v2", "--branch"], {
@@ -667,28 +769,6 @@ async function enrichSubmoduleWorktreeStatus(repo, submodule, options) {
667
769
  submodule.error = formatGitError(error);
668
770
  }
669
771
  }
670
- function parseSubmoduleStatusOutput(output, repoRoot, ignorePaths) {
671
- const submodules = [];
672
- const ignoreSet = new Set(ignorePaths || []);
673
- for (const line of output.split("\n")) {
674
- if (!line.trim()) continue;
675
- const match = line.match(/^([\-+U\s])([0-9a-f]{40})\s+(\S+)(?:\s+\(([^)]+)\))?/);
676
- if (!match) continue;
677
- const prefix = match[1];
678
- const commit = match[2];
679
- const path41 = match[3];
680
- if (ignoreSet.has(path41)) continue;
681
- submodules.push({
682
- path: path41,
683
- commit,
684
- repoPath: repoRoot + "/" + path41,
685
- dirty: prefix === "U",
686
- outOfSync: prefix === "-" || prefix === "+",
687
- lastCheckedAt: Date.now()
688
- });
689
- }
690
- return submodules;
691
- }
692
772
  var lastKnownGoodStatus, DAEMON_RUNTIME_PACKAGES, WEB_ONLY_PACKAGES;
693
773
  var init_git_status = __esm({
694
774
  "src/git/git-status.ts"() {
@@ -1577,8 +1657,61 @@ function mergeMeshPolicy(base, patch) {
1577
1657
  } else {
1578
1658
  delete policy.autoConvergeCodeChange;
1579
1659
  }
1660
+ const normalizedAffinity = normalizeTaskAffinityPolicy(policy.taskAffinity);
1661
+ if (normalizedAffinity) {
1662
+ policy.taskAffinity = normalizedAffinity;
1663
+ } else {
1664
+ delete policy.taskAffinity;
1665
+ }
1580
1666
  return policy;
1581
1667
  }
1668
+ function normalizeTaskAffinityPolicy(value) {
1669
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1670
+ const record = value;
1671
+ const out = {};
1672
+ let hasContent = false;
1673
+ if (typeof record.enabled === "boolean") {
1674
+ out.enabled = record.enabled;
1675
+ if (record.enabled === false) hasContent = true;
1676
+ else delete out.enabled;
1677
+ }
1678
+ if (record.byTaskMode && typeof record.byTaskMode === "object" && !Array.isArray(record.byTaskMode)) {
1679
+ const byTaskMode = {};
1680
+ for (const [mode, raw] of Object.entries(record.byTaskMode)) {
1681
+ const modeKey = mode.trim();
1682
+ if (!modeKey) continue;
1683
+ if (typeof raw !== "string") continue;
1684
+ const role = raw.trim().toLowerCase();
1685
+ byTaskMode[modeKey] = role;
1686
+ if (role && !STANDARD_MESH_ROLES.includes(role)) {
1687
+ try {
1688
+ console.warn(`[mesh] task_affinity.byTaskMode.${modeKey}: custom role "${role}" is not a standard mesh role (${STANDARD_MESH_ROLES.join("/")}); routing still applies`);
1689
+ } catch {
1690
+ }
1691
+ }
1692
+ }
1693
+ if (Object.keys(byTaskMode).length > 0) {
1694
+ out.byTaskMode = byTaskMode;
1695
+ hasContent = true;
1696
+ }
1697
+ }
1698
+ if (Array.isArray(record.customRoles)) {
1699
+ const seen = /* @__PURE__ */ new Set();
1700
+ const customRoles = [];
1701
+ for (const raw of record.customRoles) {
1702
+ if (typeof raw !== "string") continue;
1703
+ const role = raw.trim().toLowerCase();
1704
+ if (!role || seen.has(role)) continue;
1705
+ seen.add(role);
1706
+ customRoles.push(role);
1707
+ }
1708
+ if (customRoles.length > 0) {
1709
+ out.customRoles = customRoles;
1710
+ hasContent = true;
1711
+ }
1712
+ }
1713
+ return hasContent ? out : void 0;
1714
+ }
1582
1715
  function normalizeAutoFastForwardPolicy(value) {
1583
1716
  const record = value && typeof value === "object" && !Array.isArray(value) ? value : {};
1584
1717
  const maxBehind = Number(record.maxBehind);
@@ -2872,6 +3005,7 @@ __export(mesh_work_queue_exports, {
2872
3005
  recordTaskAutoLaunch: () => recordTaskAutoLaunch,
2873
3006
  requeueTask: () => requeueTask,
2874
3007
  resolveConvergeRequiredTags: () => resolveConvergeRequiredTags,
3008
+ resolveTaskAffinityRequiredTags: () => resolveTaskAffinityRequiredTags,
2875
3009
  updateDirectDispatchStatus: () => updateDirectDispatchStatus,
2876
3010
  updateSessionTaskStatus: () => updateSessionTaskStatus,
2877
3011
  updateTaskStatus: () => updateTaskStatus,
@@ -3010,6 +3144,31 @@ function resolveConvergeRequiredTags(meshId, taskMode, explicitRequiredTags, opt
3010
3144
  if (!optedIn) return explicitRequiredTags;
3011
3145
  return normalizeMeshCapabilityTags([...explicitRequiredTags, MESH_CONVERGE_REFINE_TAG]);
3012
3146
  }
3147
+ function meshHasNodeAdvertisingRole(nodes, role) {
3148
+ if (!Array.isArray(nodes) || nodes.length === 0) return false;
3149
+ const wanted = `role=${role}`;
3150
+ return nodes.some((node) => buildMeshNodeCapabilityTags(node).includes(wanted));
3151
+ }
3152
+ function resolveTaskAffinityRequiredTags(meshId, taskMode, explicitRequiredTags, opts) {
3153
+ if (typeof opts?.targetNodeId === "string" && opts.targetNodeId.trim()) return explicitRequiredTags;
3154
+ if (opts?.callerSpecifiedRequiredTags === true) return explicitRequiredTags;
3155
+ let mesh;
3156
+ try {
3157
+ mesh = getMesh(meshId);
3158
+ } catch {
3159
+ return explicitRequiredTags;
3160
+ }
3161
+ const role = resolveTaskAffinityRole(taskMode, mesh?.policy?.taskAffinity);
3162
+ if (!role) return explicitRequiredTags;
3163
+ if (!meshHasNodeAdvertisingRole(mesh?.nodes, role)) {
3164
+ try {
3165
+ console.warn(`[mesh] task_affinity: no node advertises role=${role} for taskMode=${taskMode} in mesh ${meshId}; skipping injection (least_loaded fallback)`);
3166
+ } catch {
3167
+ }
3168
+ return explicitRequiredTags;
3169
+ }
3170
+ return normalizeMeshCapabilityTags([...explicitRequiredTags, `role=${role}`]);
3171
+ }
3013
3172
  function withQueueLock(_meshId, fn) {
3014
3173
  return MeshRuntimeStore.getInstance().transaction(fn);
3015
3174
  }
@@ -3060,6 +3219,20 @@ function enqueueTask(meshId, message, opts) {
3060
3219
  throw new Error(`duplicate_task_id: task '${id}' already exists in mesh '${meshId}'`);
3061
3220
  }
3062
3221
  assertNoDependencyCycle(meshId, id, dependsOn);
3222
+ const callerTags = normalizeMeshCapabilityTags(opts?.requiredTags);
3223
+ const callerSpecifiedRequiredTags = callerTags.length > 0;
3224
+ let resolvedRequiredTags = resolveConvergeRequiredTags(
3225
+ meshId,
3226
+ modeValidation.taskMode,
3227
+ callerTags,
3228
+ { targetNodeId: opts?.targetNodeId }
3229
+ );
3230
+ resolvedRequiredTags = resolveTaskAffinityRequiredTags(
3231
+ meshId,
3232
+ modeValidation.taskMode,
3233
+ resolvedRequiredTags,
3234
+ { targetNodeId: opts?.targetNodeId, callerSpecifiedRequiredTags }
3235
+ );
3063
3236
  const entry = {
3064
3237
  id,
3065
3238
  meshId,
@@ -3068,15 +3241,7 @@ function enqueueTask(meshId, message, opts) {
3068
3241
  taskMode: modeValidation.taskMode,
3069
3242
  targetNodeId: opts?.targetNodeId,
3070
3243
  targetSessionId: opts?.targetSessionId,
3071
- // Convergence routing (opt-in): auto-inject converge=refine for code_change
3072
- // tasks so they hard-filter onto refine-capable worktree nodes. No-op unless
3073
- // the mesh opts in; explicit target_node_id / required_tags are preserved.
3074
- requiredTags: resolveConvergeRequiredTags(
3075
- meshId,
3076
- modeValidation.taskMode,
3077
- normalizeMeshCapabilityTags(opts?.requiredTags),
3078
- { targetNodeId: opts?.targetNodeId }
3079
- ),
3244
+ requiredTags: resolvedRequiredTags,
3080
3245
  ...dependsOn.length > 0 ? { dependsOn } : {},
3081
3246
  ...typeof opts?.missionId === "string" && opts.missionId.trim() ? { missionId: opts.missionId.trim() } : {},
3082
3247
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3833,6 +3998,18 @@ var init_mesh_runtime_store = __esm({
3833
3998
  `).get(meshId, nodeId);
3834
3999
  return row?.count ?? 0;
3835
4000
  }
4001
+ /**
4002
+ * O(1) count of queue tasks in 'pending' status for a mesh. A COUNT(*) over the
4003
+ * indexed status column, so it avoids JSON.parse-ing every queue row — used as a
4004
+ * cheap guard before the reconcile loop runs a full triggerMeshQueue scan.
4005
+ */
4006
+ pendingQueueTaskCount(meshId) {
4007
+ const row = this.db.prepare(`
4008
+ SELECT COUNT(*) as count FROM mesh_queue
4009
+ WHERE mesh_id = ? AND status = 'pending'
4010
+ `).get(meshId);
4011
+ return row?.count ?? 0;
4012
+ }
3836
4013
  /**
3837
4014
  * Read the current per-mesh round-robin cursor (0 when unset). Used to rotate
3838
4015
  * the tie-break winner among nodes tied at the least load.
@@ -8766,7 +8943,7 @@ async function triggerMeshQueue(components, meshId) {
8766
8943
  }
8767
8944
  const remoteCandidates = [];
8768
8945
  for (const idle of remoteSessions) {
8769
- const node = mesh.nodes.find((n) => n.id === idle.nodeId);
8946
+ const node = mesh.nodes.find((n) => meshNodeIdMatches(n, idle.nodeId));
8770
8947
  if (node) {
8771
8948
  remoteIdleSessionsChecked += 1;
8772
8949
  remoteCandidates.push({ nodeId: idle.nodeId, sessionId: idle.sessionId, providerType: idle.providerType, origin: "remote", node });
@@ -9570,6 +9747,21 @@ async function runMeshReconcileTick(components) {
9570
9747
  }
9571
9748
  }
9572
9749
  }
9750
+ for (const mesh of listMeshes()) {
9751
+ const selfIds = resolveCoordinatorSelfIds(mesh, drainDaemonIds);
9752
+ if (!daemonHostsMesh(mesh, selfIds)) continue;
9753
+ if (store) {
9754
+ try {
9755
+ if (store.pendingQueueTaskCount(mesh.id) === 0) continue;
9756
+ } catch {
9757
+ }
9758
+ }
9759
+ try {
9760
+ await triggerMeshQueue(components, mesh.id);
9761
+ } catch (e) {
9762
+ LOG.warn("MeshReconcile", `Pending-claim recovery trigger failed for mesh ${mesh.id}: ${e?.message || e}`);
9763
+ }
9764
+ }
9573
9765
  const coordinators = findLiveCoordinators(components);
9574
9766
  if (coordinators.length === 0) {
9575
9767
  return;
@@ -10961,8 +11153,24 @@ function findBinary(name) {
10961
11153
  }
10962
11154
  const isWin = os12.platform() === "win32";
10963
11155
  const paths = (process.env.PATH || "").split(path17.delimiter);
11156
+ const extraDirs = [];
11157
+ if (isWin) {
11158
+ if (process.env.APPDATA) extraDirs.push(path17.join(process.env.APPDATA, "npm"));
11159
+ try {
11160
+ extraDirs.push(path17.dirname(process.execPath));
11161
+ } catch {
11162
+ }
11163
+ } else {
11164
+ extraDirs.push(path17.join(os12.homedir(), ".npm-global", "bin"));
11165
+ extraDirs.push("/usr/local/bin", "/opt/homebrew/bin");
11166
+ try {
11167
+ extraDirs.push(path17.dirname(process.execPath));
11168
+ } catch {
11169
+ }
11170
+ }
11171
+ const searchDirs = [...paths, ...extraDirs];
10964
11172
  const exes = isWin ? [".exe", ".cmd", ".bat", ""] : [""];
10965
- for (const p of paths) {
11173
+ for (const p of searchDirs) {
10966
11174
  if (!p) continue;
10967
11175
  for (const ext of exes) {
10968
11176
  const fullPath = path17.join(p, trimmed + ext);
@@ -14362,6 +14570,37 @@ ${lastSnapshot}`;
14362
14570
  if (!this.ptyProcess || typeof this.ptyProcess.getMetadata !== "function") return null;
14363
14571
  return this.ptyProcess.getMetadata();
14364
14572
  }
14573
+ /**
14574
+ * Launch metadata for the dashboard Session info panel. Re-derives the spawn plan
14575
+ * (pure — same inputs the live PTY was spawned with) so the dashboard sees the
14576
+ * resolved binary, full arg vector, and cwd without us having to persist it.
14577
+ * extraEnv values are intentionally dropped (keys only) so secrets passed at
14578
+ * launch time never reach the dashboard.
14579
+ */
14580
+ getLaunchInfo() {
14581
+ let command;
14582
+ let args = [...this.extraArgs];
14583
+ try {
14584
+ const plan = resolveCliSpawnPlan({
14585
+ provider: this.provider,
14586
+ runtimeSettings: this.runtimeSettings,
14587
+ workingDir: this.workingDir,
14588
+ extraArgs: this.extraArgs,
14589
+ extraEnv: this.extraEnv
14590
+ });
14591
+ command = plan.binaryPath;
14592
+ args = plan.allArgs;
14593
+ } catch {
14594
+ }
14595
+ return {
14596
+ command,
14597
+ args,
14598
+ extraArgs: [...this.extraArgs],
14599
+ cwd: this.workingDir,
14600
+ extraEnvKeys: Object.keys(this.extraEnv || {}),
14601
+ providerSessionId: this.providerSessionId || void 0
14602
+ };
14603
+ }
14365
14604
  updateRuntimeMeta(meta, replace = false) {
14366
14605
  const nextProviderSessionId = typeof meta?.providerSessionId === "string" ? meta.providerSessionId.trim() : "";
14367
14606
  if (nextProviderSessionId) {
@@ -15704,6 +15943,7 @@ __export(index_exports, {
15704
15943
  DEFAULT_STATUS_INITIAL_REPORT_DELAY_MS: () => DEFAULT_STATUS_INITIAL_REPORT_DELAY_MS,
15705
15944
  DEFAULT_STATUS_P2P_REPORT_INTERVAL_MS: () => DEFAULT_STATUS_P2P_REPORT_INTERVAL_MS,
15706
15945
  DEFAULT_STATUS_SERVER_REPORT_INTERVAL_MS: () => DEFAULT_STATUS_SERVER_REPORT_INTERVAL_MS,
15946
+ DEFAULT_TASKMODE_ROLE_MAP: () => DEFAULT_TASKMODE_ROLE_MAP,
15707
15947
  DEV_SERVER_PORT: () => DEV_SERVER_PORT,
15708
15948
  DaemonAgentStreamManager: () => DaemonAgentStreamManager,
15709
15949
  DaemonCdpInitializer: () => DaemonCdpInitializer,
@@ -15743,6 +15983,7 @@ __export(index_exports, {
15743
15983
  RawTerminalAttachment: () => RawTerminalAttachment,
15744
15984
  STALE_TERMINAL_REFINE_WINDOW_MS: () => STALE_TERMINAL_REFINE_WINDOW_MS,
15745
15985
  STANDALONE_CDP_SCAN_INTERVAL_MS: () => STANDALONE_CDP_SCAN_INTERVAL_MS,
15986
+ STANDARD_MESH_ROLES: () => STANDARD_MESH_ROLES,
15746
15987
  SessionHostPtyTransportFactory: () => SessionHostPtyTransportFactory,
15747
15988
  TerminalAdapter: () => TerminalAdapter,
15748
15989
  TurnSnapshotTracker: () => TurnSnapshotTracker,
@@ -15976,9 +16217,12 @@ __export(index_exports, {
15976
16217
  resolveGitRepository: () => resolveGitRepository,
15977
16218
  resolveMeshHostStatus: () => resolveMeshHostStatus,
15978
16219
  resolveMeshRefineValidationPlan: () => resolveMeshRefineValidationPlan,
16220
+ resolveMeshRoleOptions: () => resolveMeshRoleOptions,
15979
16221
  resolveNodeSchedulingPriority: () => resolveNodeSchedulingPriority,
15980
16222
  resolveSessionHostAppName: () => resolveSessionHostAppName,
15981
16223
  resolveSessionHostAppNameResolution: () => resolveSessionHostAppNameResolution,
16224
+ resolveTaskAffinityRequiredTags: () => resolveTaskAffinityRequiredTags,
16225
+ resolveTaskAffinityRole: () => resolveTaskAffinityRole,
15982
16226
  resolveWorktreePath: () => resolveWorktreePath,
15983
16227
  runAsyncBatch: () => runAsyncBatch,
15984
16228
  runGit: () => runGit,
@@ -18940,8 +19184,8 @@ async function detectIDEs(providerLoader) {
18940
19184
  if ((0, import_fs12.existsSync)(bundledCli)) resolvedCli = bundledCli;
18941
19185
  }
18942
19186
  if (!resolvedCli && appPath && os30 === "win32") {
18943
- const { dirname: dirname14 } = await import("path");
18944
- const appDir = dirname14(appPath);
19187
+ const { dirname: dirname15 } = await import("path");
19188
+ const appDir = dirname15(appPath);
18945
19189
  const candidates = [
18946
19190
  `${appDir}\\\\bin\\\\${def.cli}.cmd`,
18947
19191
  `${appDir}\\\\bin\\\\${def.cli}`,
@@ -43123,7 +43367,15 @@ var MESH_DIRECT_PROBE_MAX_RETRIES = 2;
43123
43367
  function readMeshConnectionState(connection) {
43124
43368
  return readStringValue(connection?.state);
43125
43369
  }
43370
+ function isMeshConnectionDefinitivelyDown(connection) {
43371
+ if (!connection) return true;
43372
+ const state = readMeshConnectionState(connection);
43373
+ return state === "failed" || state === "closed" || state === "disconnected";
43374
+ }
43126
43375
  async function probeRemoteMeshGitStatusWithRetry(args) {
43376
+ if (args.getConnection && isMeshConnectionDefinitivelyDown(args.getConnection(args.daemonId))) {
43377
+ return null;
43378
+ }
43127
43379
  for (let attempt = 0; attempt <= MESH_DIRECT_PROBE_MAX_RETRIES; attempt += 1) {
43128
43380
  if (attempt > 0) {
43129
43381
  const connection = args.getConnection?.(args.daemonId);
@@ -47104,6 +47356,7 @@ ${hintLines.join("\n")}` : "",
47104
47356
  if (!target && !coord) return { success: false, error: "Session not found", sessionId };
47105
47357
  const adapter = target ? this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter : void 0;
47106
47358
  const runtimeMeta = adapter && typeof adapter.getRuntimeMetadata === "function" ? adapter.getRuntimeMetadata() : void 0;
47359
+ const launchInfo = adapter && typeof adapter.getLaunchInfo === "function" ? adapter.getLaunchInfo() : void 0;
47107
47360
  const providerType = target?.providerType || coord?.cliType || "";
47108
47361
  const providerMetaForSession = providerType ? this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType) : void 0;
47109
47362
  return {
@@ -47115,8 +47368,11 @@ ${hintLines.join("\n")}` : "",
47115
47368
  transport: target?.transport,
47116
47369
  workspace: target?.workspace || coord?.workspace,
47117
47370
  spawnedAtMs: target?.spawnedAtMs || coord?.startedAt,
47118
- providerSessionId: target?.providerSessionId,
47119
- runtimeMetadata: runtimeMeta
47371
+ // providerSessionId now comes from the live adapter's launch info
47372
+ // (the registry target never carried it — it was always undefined).
47373
+ providerSessionId: launchInfo?.providerSessionId || target?.providerSessionId,
47374
+ runtimeMetadata: runtimeMeta,
47375
+ launch: launchInfo
47120
47376
  },
47121
47377
  coordinator: coord ? {
47122
47378
  meshId: coord.meshId,
@@ -48957,7 +49213,7 @@ ${ptyResult.output.slice(-2e3)}`);
48957
49213
  };
48958
49214
  }
48959
49215
  const { existsSync: existsSync44, readFileSync: readFileSync35, writeFileSync: writeFileSync23, copyFileSync: copyFileSync4, mkdirSync: mkdirSync21 } = await import("fs");
48960
- const { dirname: dirname14 } = await import("path");
49216
+ const { dirname: dirname15 } = await import("path");
48961
49217
  const mcpConfigPath = coordinatorSetup.configPath;
48962
49218
  const hermesManualFallback = cliType === "hermes-cli" && configFormat === "hermes_config_yaml" ? createHermesManualMeshCoordinatorSetup(meshId, workspace) : null;
48963
49219
  let hermesBaseConfig = null;
@@ -48992,7 +49248,7 @@ ${ptyResult.output.slice(-2e3)}`);
48992
49248
  };
48993
49249
  }
48994
49250
  try {
48995
- mkdirSync21(dirname14(mcpConfigPath), { recursive: true });
49251
+ mkdirSync21(dirname15(mcpConfigPath), { recursive: true });
48996
49252
  } catch (error) {
48997
49253
  const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
48998
49254
  LOG.error("MeshCoordinator", message);
@@ -49002,7 +49258,7 @@ ${ptyResult.output.slice(-2e3)}`);
49002
49258
  const hadExistingMcpConfig = existsSync44(mcpConfigPath);
49003
49259
  let existingMcpConfig = hermesBaseConfig?.config || {};
49004
49260
  if (hermesBaseConfig) {
49005
- copyHermesCoordinatorCredentialFiles(hermesBaseConfig.sourceHome, dirname14(mcpConfigPath));
49261
+ copyHermesCoordinatorCredentialFiles(hermesBaseConfig.sourceHome, dirname15(mcpConfigPath));
49006
49262
  }
49007
49263
  if (hadExistingMcpConfig) {
49008
49264
  try {
@@ -49040,7 +49296,7 @@ ${ptyResult.output.slice(-2e3)}`);
49040
49296
  const cliArgs = [];
49041
49297
  const launchEnv = {};
49042
49298
  if (configFormat === "hermes_config_yaml") {
49043
- launchEnv.HERMES_HOME = dirname14(mcpConfigPath);
49299
+ launchEnv.HERMES_HOME = dirname15(mcpConfigPath);
49044
49300
  launchEnv.HERMES_IGNORE_USER_CONFIG = "";
49045
49301
  }
49046
49302
  let autoImportContextFilePath;
@@ -58168,6 +58424,7 @@ var V1_CONTRACT_VERSION = "1.0.0";
58168
58424
  DEFAULT_STATUS_INITIAL_REPORT_DELAY_MS,
58169
58425
  DEFAULT_STATUS_P2P_REPORT_INTERVAL_MS,
58170
58426
  DEFAULT_STATUS_SERVER_REPORT_INTERVAL_MS,
58427
+ DEFAULT_TASKMODE_ROLE_MAP,
58171
58428
  DEV_SERVER_PORT,
58172
58429
  DaemonAgentStreamManager,
58173
58430
  DaemonCdpInitializer,
@@ -58207,6 +58464,7 @@ var V1_CONTRACT_VERSION = "1.0.0";
58207
58464
  RawTerminalAttachment,
58208
58465
  STALE_TERMINAL_REFINE_WINDOW_MS,
58209
58466
  STANDALONE_CDP_SCAN_INTERVAL_MS,
58467
+ STANDARD_MESH_ROLES,
58210
58468
  SessionHostPtyTransportFactory,
58211
58469
  TerminalAdapter,
58212
58470
  TurnSnapshotTracker,
@@ -58440,9 +58698,12 @@ var V1_CONTRACT_VERSION = "1.0.0";
58440
58698
  resolveGitRepository,
58441
58699
  resolveMeshHostStatus,
58442
58700
  resolveMeshRefineValidationPlan,
58701
+ resolveMeshRoleOptions,
58443
58702
  resolveNodeSchedulingPriority,
58444
58703
  resolveSessionHostAppName,
58445
58704
  resolveSessionHostAppNameResolution,
58705
+ resolveTaskAffinityRequiredTags,
58706
+ resolveTaskAffinityRole,
58446
58707
  resolveWorktreePath,
58447
58708
  runAsyncBatch,
58448
58709
  runGit,