@adhdev/daemon-standalone 1.0.28-rc.9 → 1.0.29-rc.1

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.
@@ -51,6 +51,9 @@ var IPC_COMMAND_TIMEOUTS_MS = {
51
51
  // 120s leaves headroom and matches the relay-wrapped remote path.
52
52
  clone_mesh_node: 12e4,
53
53
  remove_mesh_node: 6e4,
54
+ // Retention plan/execute runs the same git probes as a remove dry-run plus
55
+ // queue/session/ledger reads; 60s matches the remove budget.
56
+ cleanup_worktree_nodes: 6e4,
54
57
  // A5: plan_mesh_refine_node is the SYNCHRONOUS refine dry-run — it runs several git
55
58
  // probes (status/merge-tree/submodule) inline before replying, which can approach the
56
59
  // 15s default on a slow (Windows) host. 45s defensively, matching git_status/diff.
@@ -304,6 +307,33 @@ function dedupeSummaryFromTail(messages, summary) {
304
307
  return { ...rest, content: "", _sameAsSummary: true };
305
308
  });
306
309
  }
310
+ var TURN_IDENTITY_FIELDS = [
311
+ "authority",
312
+ "status",
313
+ "stage",
314
+ "terminalOutcome",
315
+ "terminalReason",
316
+ "meshId",
317
+ "taskId",
318
+ "attemptId",
319
+ "attemptSeq",
320
+ "sessionId",
321
+ "nodeId",
322
+ "providerType",
323
+ "acceptedAt",
324
+ "deliveredAt",
325
+ "consumedAt",
326
+ "terminalAt",
327
+ "updatedAt"
328
+ ];
329
+ function slimTurnPresentation(turn) {
330
+ if (!turn || typeof turn !== "object" || Array.isArray(turn)) return null;
331
+ const slim = {};
332
+ for (const key of TURN_IDENTITY_FIELDS) {
333
+ if (turn[key] !== void 0) slim[key] = turn[key];
334
+ }
335
+ return Object.keys(slim).length > 0 ? slim : null;
336
+ }
307
337
  function compactChatPayload(payload, opts = {}) {
308
338
  const rawMessages = Array.isArray(payload?.messages) ? payload.messages : [];
309
339
  const visible = rawMessages.filter(isCoordinatorVisibleMessage);
@@ -320,6 +350,7 @@ function compactChatPayload(payload, opts = {}) {
320
350
  const toolSummaries = rawMessages.filter((m) => !isCoordinatorVisibleMessage(m)).map(summarizeToolMessage).filter((s) => s !== null);
321
351
  const omittedMessages = Math.max(0, rawMessages.length - messages.length);
322
352
  const filteredMessages = Math.max(0, rawMessages.length - visible.length);
353
+ const slimTurn = opts.preserveTurn ? slimTurnPresentation(payload?.turn) : null;
323
354
  return {
324
355
  success: payload?.success !== false,
325
356
  compact: true,
@@ -327,6 +358,9 @@ function compactChatPayload(payload, opts = {}) {
327
358
  ...opts.sessionId !== void 0 ? { sessionId: opts.sessionId } : {},
328
359
  status: payload?.status ?? null,
329
360
  providerSessionId: payload?.providerSessionId ?? null,
361
+ ...slimTurn ? { turn: slimTurn } : {},
362
+ ...slimTurn?.attemptId !== void 0 ? { attemptId: slimTurn.attemptId } : {},
363
+ ...slimTurn?.stage !== void 0 ? { turnStage: slimTurn.stage } : {},
330
364
  totalMessages: rawMessages.length,
331
365
  visibleMessages: visible.length,
332
366
  filteredMessages,
@@ -393,10 +427,12 @@ var CANONICAL_MESH_TOOL_NAMES = [
393
427
  "mesh_approve",
394
428
  "mesh_answer_question",
395
429
  "mesh_list_pending_approvals",
430
+ "mesh_plan_onboarding",
396
431
  "mesh_create",
397
432
  "mesh_add_node",
398
433
  "mesh_clone_node",
399
434
  "mesh_remove_node",
435
+ "mesh_cleanup_worktree_nodes",
400
436
  "mesh_refine_node",
401
437
  "mesh_refine_batch",
402
438
  "mesh_refine_config",
@@ -1000,12 +1036,19 @@ var MESH_FAST_FORWARD_NODE_TOOL = {
1000
1036
  };
1001
1037
  var MESH_RESTART_DAEMON_TOOL = {
1002
1038
  name: "mesh_restart_daemon",
1003
- description: `Update a mesh node's daemon to the latest published version on its release channel and restart it \u2014 the same path as the dashboard "preview update" button, exposed as a mesh command so a coordinator can roll a worker daemon onto a freshly deployed version without a manual restart round-trip. No agent session is launched. Idle-gated: a node whose daemon has an active session (generating / waiting_approval / starting) is refused with code "blocking_sessions" so an in-flight turn is never interrupted. If the node is already on the latest version it is a no-op (no restart), matching the dashboard button (returns alreadyLatest:true). Targets a single node \u2014 call other (idle) nodes first; restarting the coordinator's OWN daemon is naturally refused while its calling turn is active. Passing channel switches the daemon's release channel (and server URL) before restarting; omit it to keep the daemon on its configured channel.`,
1039
+ description: `Restart a mesh node's daemon, optionally updating it first \u2014 the same path as the dashboard "preview update" button, exposed as a mesh command so a coordinator can roll a worker daemon without a manual restart round-trip. No agent session is launched. mode="upgrade" (default): update to the latest published version on the release channel, then restart; already-latest is a no-op (no restart, returns alreadyLatest:true). mode="restart": pure re-spawn with no reinstall \u2014 restarts even when already latest, with much shorter downtime; use it to reset wedged daemon state (memory leaks, zombie sessions). Idle-gated: a node whose daemon has an active session (generating / waiting_approval / starting) is refused with code "blocking_sessions" so an in-flight turn is never interrupted. self_only=true waives ONLY this mesh's own coordinator session (the structural self-deadlock case \u2014 the coordinator is always generating while it calls). Other sessions still refuse. force=true bypasses the gate entirely: in-flight turns die and the unpersisted pendingOutboundQueue is lost. when_idle=true schedules the restart to run automatically once the daemon goes idle (the safest path \u2014 no queue loss); cancel_when_idle=true cancels it and every response reports the schedule under deferredRestart. kill_session_host=true additionally stops the session-host process, destroying ALL hosted CLI sessions (hard refresh; this is what Windows already does on every upgrade). Default off. Note: on Windows any daemon restart/upgrade terminates all hosted sessions regardless of options; on POSIX hosted sessions survive a plain restart and rebind on next boot. Passing channel switches the daemon's release channel (and server URL) before restarting; omit it to keep the daemon on its configured channel.`,
1004
1040
  inputSchema: {
1005
1041
  type: "object",
1006
1042
  properties: {
1007
- node_id: { type: "string", description: "Target node ID \u2014 the daemon that owns this node is updated and restarted." },
1008
- channel: { type: "string", enum: ["stable", "preview"], description: "Optional release channel to update from. Defaults to the daemon's configured updateChannel. Setting it also repoints the daemon's server URL to that channel." }
1043
+ node_id: { type: "string", description: "Target node ID \u2014 the daemon that owns this node is restarted (and updated, in upgrade mode)." },
1044
+ channel: { type: "string", enum: ["stable", "preview"], description: "Optional release channel to update from (upgrade mode only). Defaults to the daemon's configured updateChannel. Setting it also repoints the daemon's server URL to that channel." },
1045
+ mode: { type: "string", enum: ["upgrade", "restart"], description: "upgrade (default): update to latest on channel, then restart (already-latest is a no-op). restart: pure re-spawn, no reinstall \u2014 restarts even when already latest." },
1046
+ force: { type: "boolean", description: "Bypass the idle-gate entirely. Destructive: in-flight turns are killed and the in-memory pendingOutboundQueue is permanently lost. Default false." },
1047
+ self_only: { type: "boolean", description: "Waive only this mesh's own coordinator session when it blocks the restart (the coordinator self-deadlock). Other nodes' active sessions still refuse. Default false." },
1048
+ when_idle: { type: "boolean", description: "If blocked, schedule the restart to execute automatically once the daemon goes idle (safest \u2014 no pendingOutboundQueue loss). The schedule expires after timeout_ms (default 30 min). Default false." },
1049
+ cancel_when_idle: { type: "boolean", description: "Cancel a previously scheduled when_idle restart on the owning daemon." },
1050
+ timeout_ms: { type: "number", description: "Expiry for a when_idle schedule in milliseconds (default 1800000 = 30 min, max 6 h)." },
1051
+ kill_session_host: { type: "boolean", description: "Hard refresh: also stop the session-host process, destroying ALL hosted CLI sessions on the machine. Default false." }
1009
1052
  },
1010
1053
  required: ["node_id"]
1011
1054
  }
@@ -1120,23 +1163,41 @@ var MESH_LIST_PENDING_APPROVALS_TOOL = {
1120
1163
  };
1121
1164
  var MESH_CREATE_TOOL = {
1122
1165
  name: "mesh_create",
1123
- description: "Bootstrap a brand-new mesh for a Git repository \u2014 the first step for an MCP-only agent that has no mesh yet. Mirrors `adhdev mesh create <name>`. A mesh groups one repo's workspaces/nodes so the coordinator can delegate work across them. You MUST supply the repo identity: pass repo_remote_url (the git remote URL, e.g. git@github.com:user/repo.git or https://github.com/user/repo.git \u2014 the identity is derived from it) OR repo_identity (an explicit normalized identity like github.com/user/repo). At least one is required; there is no auto-detection here (the CLI auto-detects from the current dir, but the MCP server may run elsewhere). Set add_current:true to also register a node in the same call (uses workspace if given, else the daemon's current working directory) \u2014 handy when the daemon runs in the repo you want as the base node. BOOT-GATE / WHEN CALLABLE: this tool is reachable in STANDARD mode (adhdev mcp, no --repo-mesh) \u2014 the bootstrap context where no mesh exists \u2014 and also in mesh mode (where it creates a SEPARATE additional mesh). It is NOT reachable before any mesh exists via mesh mode, because `adhdev mcp --repo-mesh <id>` refuses to start without an existing meshId. So the intended flow is: run standard-mode MCP \u2192 mesh_create \u2192 mesh_add_node \u2192 then relaunch as `adhdev mcp --repo-mesh <returned mesh_id>`. Returns mesh_id (and node_id when add_current is used); use mesh_id for the follow-up mesh_add_node call and to launch mesh mode.",
1166
+ description: "Bootstrap a brand-new mesh for a Git repository \u2014 the first step for an MCP-only agent that has no mesh yet. Mirrors `adhdev mesh create <name>`. A mesh groups one repo's workspaces/nodes so the coordinator can delegate work across them. Pass workspace to auto-detect Git identity/branch/worktree metadata through the read-only planner, or explicitly pass repo_remote_url / repo_identity for backward compatibility. This is a persistent write: call mesh_plan_onboarding first and obtain explicit user approval before invoking it. Set add_current:true to also register a node in the same call (uses workspace if given, else the daemon's current working directory). BOOT-GATE / WHEN CALLABLE: this tool is reachable in STANDARD mode (adhdev mcp, no --repo-mesh) \u2014 the bootstrap context where no mesh exists \u2014 and also in mesh mode (where it creates a SEPARATE additional mesh). It is NOT reachable before any mesh exists via mesh mode, because `adhdev mcp --repo-mesh <id>` refuses to start without an existing meshId. So the intended flow is: run standard-mode MCP \u2192 mesh_create \u2192 mesh_add_node \u2192 then relaunch as `adhdev mcp --repo-mesh <returned mesh_id>`. Returns mesh_id (and node_id when add_current is used); use mesh_id for the follow-up mesh_add_node call and to launch mesh mode.",
1124
1167
  inputSchema: {
1125
1168
  type: "object",
1126
1169
  properties: {
1127
1170
  name: { type: "string", description: 'Human-readable mesh name (e.g. "adhdev-main"). Trimmed, max 100 chars.' },
1128
- repo_remote_url: { type: "string", description: "Git remote URL of the repo (e.g. git@github.com:user/repo.git). The repo identity is normalized from this. Provide this OR repo_identity." },
1129
- repo_identity: { type: "string", description: "Explicit normalized repo identity (e.g. github.com/user/repo). Provide this OR repo_remote_url. Wins over repo_remote_url when both are given." },
1171
+ repo_remote_url: { type: "string", description: "Optional explicit Git remote URL. When omitted with repo_identity, identity is read-only auto-detected from workspace." },
1172
+ repo_identity: { type: "string", description: "Optional explicit normalized repo identity. Wins over repo_remote_url; when both are omitted, workspace is auto-detected." },
1130
1173
  default_branch: { type: "string", description: 'Default branch for the repo (e.g. "main"). Optional; used as the merge/convergence target.' },
1131
1174
  add_current: { type: "boolean", description: "Also register a node in this same call (parity with CLI --add-current). Uses `workspace` if provided, otherwise the daemon's current working directory." },
1132
- workspace: { type: "string", description: "Absolute workspace path to register when add_current:true. Ignored unless add_current is true. Defaults to the daemon's cwd." }
1175
+ workspace: { type: "string", description: "Absolute workspace path used for Git auto-detection and, when add_current:true, node registration. Defaults to the daemon cwd." }
1133
1176
  },
1134
1177
  required: ["name"]
1135
1178
  }
1136
1179
  };
1180
+ var MESH_PLAN_ONBOARDING_TOOL = {
1181
+ name: "mesh_plan_onboarding",
1182
+ description: "Read-only Git-aware Repo Mesh discovery and dry-run planning for a workspace path. Detects the Git root, normalized remotes/repo identity, current/default branch, main checkout vs linked worktree/common-dir metadata, dirty/conflict state, and existing mesh/node membership. Returns a typed create+onboarding, add-existing-workspace, or clone-new-worktree plan with suggested .adhdev configs. It never fetches, writes config, creates a mesh/node/branch/worktree, or otherwise mutates state. Use this before mesh_create, mesh_add_node, or mesh_clone_node; execute write steps only after explicit user approval.",
1183
+ inputSchema: {
1184
+ type: "object",
1185
+ properties: {
1186
+ workspace: { type: "string", description: "Absolute path on the daemon that owns the Git checkout." },
1187
+ mesh_id: { type: "string", description: "Optional existing mesh to validate against. In mesh mode defaults to the active mesh." },
1188
+ operation: {
1189
+ type: "string",
1190
+ enum: ["auto", "add_existing", "clone_worktree", "create_mesh"],
1191
+ description: "Planning intent. auto chooses create+onboard when no compatible mesh exists, otherwise add existing. clone_worktree requires branch and a clean source."
1192
+ },
1193
+ branch: { type: "string", description: "New branch name when operation=clone_worktree." }
1194
+ },
1195
+ required: ["workspace"]
1196
+ }
1197
+ };
1137
1198
  var MESH_ADD_NODE_TOOL = {
1138
1199
  name: "mesh_add_node",
1139
- description: "Register a workspace as a node in an EXISTING mesh \u2014 the second bootstrap step after mesh_create (or to add more nodes later). Mirrors `adhdev mesh add-node <mesh_id>` with --workspace / --read-only / --provider-priority. A node is a repo checkout on a daemon that the coordinator can launch agents on and delegate tasks to. mesh_id is REQUIRED in standard mode (pass the id returned by mesh_create); in mesh mode it defaults to the active mesh. workspace is the absolute path to the repo checkout ON THE DAEMON that owns it \u2014 the local base node is added by the daemon that created the mesh. NOTE: this registers an EXISTING directory as a node (including marking one as a worktree via is_worktree). To CREATE a fresh git worktree + branch for isolated parallel work, use mesh_clone_node instead \u2014 that runs the actual `git worktree add`. Returns node_id + workspace so you can immediately target the node with mesh_launch_session / mesh_send_task / mesh_enqueue_task.",
1200
+ description: "Register a workspace as a node in an EXISTING mesh \u2014 the second bootstrap step after mesh_create (or to add more nodes later). Mirrors `adhdev mesh add-node <mesh_id>` with --workspace / --read-only / --provider-priority. A node is a repo checkout on a daemon that the coordinator can launch agents on and delegate tasks to. mesh_id is REQUIRED in standard mode (pass the id returned by mesh_create); in mesh mode it defaults to the active mesh. workspace is the absolute path to the repo checkout ON THE DAEMON that owns it \u2014 the local base node is added by the daemon that created the mesh. This is a persistent mesh write: call mesh_plan_onboarding first and obtain explicit user approval. The implementation re-runs that preflight before writing. NOTE: this registers an EXISTING directory as a node (including auto-detected linked worktrees). To CREATE a fresh git worktree + branch for isolated parallel work, use mesh_clone_node instead \u2014 that runs the actual `git worktree add`. Returns node_id + workspace so you can immediately target the node with mesh_launch_session / mesh_send_task / mesh_enqueue_task.",
1140
1201
  inputSchema: {
1141
1202
  type: "object",
1142
1203
  properties: {
@@ -1155,7 +1216,7 @@ var MESH_ADD_NODE_TOOL = {
1155
1216
  };
1156
1217
  var MESH_CLONE_NODE_TOOL = {
1157
1218
  name: "mesh_clone_node",
1158
- description: "Create a new worktree-based node from an existing node for isolated parallel work. Creates a git worktree on a new branch so multiple tasks can run on separate branches simultaneously.",
1219
+ description: "Create a new worktree-based node from an existing node for isolated parallel work. Creates a git worktree on a new branch so multiple tasks can run on separate branches simultaneously. This writes a branch, worktree and mesh node: call mesh_plan_onboarding with operation=clone_worktree and obtain explicit user approval first; the implementation re-runs the clean/source preflight.",
1159
1220
  inputSchema: {
1160
1221
  type: "object",
1161
1222
  properties: {
@@ -1183,6 +1244,18 @@ var MESH_REMOVE_NODE_TOOL = {
1183
1244
  required: ["node_id"]
1184
1245
  }
1185
1246
  };
1247
+ var MESH_CLEANUP_WORKTREE_NODES_TOOL = {
1248
+ name: "mesh_cleanup_worktree_nodes",
1249
+ description: "Plan (dry-run, default) or execute safe removal of CONVERGED local worktree nodes (lifecycle retention). A node is eligible only when its feature branch is proven merged/pushed/converged AND every safety exclusion passes: no dirty/conflicted/stashed/submodule-drift state, no live session, no queue/direct-dispatch reference, no in-flight or blocked_review Refinery job, not the coordinator/base/cwd/evidence node. The automatic reconcile pass additionally requires two consecutive eligible ticks spanning a grace window (default 48h). Per-node reason codes are always returned; removal never uses force and branch refs are deleted only when proven fully merged.",
1250
+ inputSchema: {
1251
+ type: "object",
1252
+ properties: {
1253
+ node_id: { type: "string", description: "Optional: restrict the plan/execute to a single node. When omitted, every node in the mesh is evaluated." },
1254
+ dry_run: { type: "boolean", description: "Default true. true = read-only reason-coded plan (identical shape to the automatic pass); false = execute removal for every currently-eligible node (never forces; the non-destructive precheck re-runs immediately before each removal)." }
1255
+ },
1256
+ required: []
1257
+ }
1258
+ };
1186
1259
  var MESH_CLEANUP_SESSIONS_TOOL = {
1187
1260
  name: "mesh_cleanup_sessions",
1188
1261
  description: "Manually clean up delegated session records for a mesh node without removing the node. Defaults should preserve reviewable history unless the caller chooses a mode explicitly.",
@@ -1579,10 +1652,12 @@ var ALL_MESH_TOOLS = [
1579
1652
  MESH_APPROVE_TOOL,
1580
1653
  MESH_ANSWER_QUESTION_TOOL,
1581
1654
  MESH_LIST_PENDING_APPROVALS_TOOL,
1655
+ MESH_PLAN_ONBOARDING_TOOL,
1582
1656
  MESH_CREATE_TOOL,
1583
1657
  MESH_ADD_NODE_TOOL,
1584
1658
  MESH_CLONE_NODE_TOOL,
1585
1659
  MESH_REMOVE_NODE_TOOL,
1660
+ MESH_CLEANUP_WORKTREE_NODES_TOOL,
1586
1661
  MESH_REFINE_NODE_TOOL,
1587
1662
  MESH_REFINE_BATCH_TOOL,
1588
1663
  MESH_REFINE_CONFIG_TOOL,
@@ -2359,6 +2434,7 @@ async function reconcileDirectDispatchesFromTranscriptEvidence(ctx, liveNodes, d
2359
2434
  });
2360
2435
  const payload = unwrapCommandPayload(readResult);
2361
2436
  if (payload?.success === false) continue;
2437
+ if ((0, import_daemon_core3.hasTrailingToolActivityAfterFinalAssistant)(Array.isArray(payload?.messages) ? payload.messages : [])) continue;
2362
2438
  const evidence = readFinalAssistantTranscriptEvidence(payload);
2363
2439
  if (!evidence.finalSummary) continue;
2364
2440
  const result = (0, import_daemon_core3.reconcileDirectDispatchCompletionFromTranscript)({
@@ -3231,6 +3307,12 @@ function buildMeshForwardPayloadFromPendingEvent(event) {
3231
3307
  interactionId: readString(metadataEvent.interactionId),
3232
3308
  status: readString(metadataEvent.status),
3233
3309
  targetDaemonId: readString(metadataEvent.targetDaemonId),
3310
+ // RC32: carry the coordinator DAEMON anchor across the remote-pull relay (the
3311
+ // pending event stores it top-level, not inside metadataEvent). The receive-side
3312
+ // whitelist (daemon-core buildRelayMetadataEvent) reads it back so a sessionless
3313
+ // refine terminal event re-queues targeted at THIS coordinator instead of
3314
+ // self-fallback-stamping the relaying worker daemon.
3315
+ targetCoordinatorDaemonId: readString(event?.targetCoordinatorDaemonId),
3234
3316
  startedAt: readString(metadataEvent.startedAt),
3235
3317
  completedAt: readString(metadataEvent.completedAt),
3236
3318
  retryOfJobId: readString(metadataEvent.retryOfJobId),
@@ -3577,6 +3659,11 @@ async function meshStatus(ctx, args = {}) {
3577
3659
  status: s.status ?? s.lifecycle ?? s.state,
3578
3660
  providerType: s.providerType ?? s.cliType ?? s.type,
3579
3661
  ...s.activeChat?.status ? { chatStatus: s.activeChat.status } : {},
3662
+ // Stage 6: attempt identity + causal stage from the unified turn
3663
+ // projection (present on mesh-owned sessions only), so mesh_status
3664
+ // reports the SAME attemptId/stage as read_chat and the dashboard.
3665
+ ...s.turn?.attemptId ? { attemptId: s.turn.attemptId } : {},
3666
+ ...s.turn?.stage ? { turnStage: s.turn.stage } : {},
3580
3667
  ...isSelfCoordinator ? { isSelfCoordinator: true, role: "coordinator" } : {},
3581
3668
  // [T2] Carry the worker-computed last-message preview through the slim so
3582
3669
  // the coordinator's inbox can show the worker's latest ASSISTANT reply
@@ -6671,30 +6758,29 @@ async function meshSendTask(ctx, args) {
6671
6758
  }
6672
6759
  if (explicitTargetSession && !isIdleSessionRecord(explicitTargetSession) && !isTerminalSessionRecord(explicitTargetSession)) {
6673
6760
  const sessionStatus = typeof explicitTargetSession?.status === "string" ? explicitTargetSession.status : "unknown";
6674
- const { createSessionDelivery: createDelivery, resolveDeliveryDecision } = await import("@adhdev/daemon-core");
6761
+ const { resolveDeliveryDecision } = await import("@adhdev/daemon-core");
6675
6762
  const policyResult = resolveDeliveryDecision(sessionStatus, { kind: "task" });
6676
6763
  if (policyResult.decision === "queued") {
6677
- const delivery = createDelivery({
6678
- meshId: ctx.mesh.id,
6679
- nodeId: args.node_id,
6680
- sessionId: args.session_id,
6681
- providerType: resolvedProviderType,
6682
- kind: "task",
6683
- message,
6684
- status: "queued"
6764
+ const queuedTask = (0, import_daemon_core4.enqueueTask)(ctx.mesh.id, message, {
6765
+ targetNodeId: args.node_id,
6766
+ targetSessionId: args.session_id,
6767
+ taskMode,
6768
+ ...readonly ? { readonly: true } : {},
6769
+ ...missionId ? { missionId } : {},
6770
+ ...ctx.coordinatorSessionId ? { sourceCoordinatorSessionId: ctx.coordinatorSessionId } : {}
6685
6771
  });
6686
6772
  return JSON.stringify({
6687
6773
  success: true,
6688
6774
  dispatched: false,
6689
6775
  decision: "queued_delivery",
6690
- deliveryId: delivery.id,
6776
+ taskId: queuedTask.id,
6691
6777
  reason: policyResult.reason,
6692
6778
  nodeId: args.node_id,
6693
6779
  sessionId: args.session_id,
6694
6780
  sessionStatus,
6695
6781
  taskMode: taskMode || void 0,
6696
6782
  message: policyResult.message,
6697
- nextAction: `Use mesh_status to watch for session idle transition, or use mesh_enqueue_task for queue-based assignment. Check deliveryId '${delivery.id}' to track queued delivery.`
6783
+ nextAction: `Task '${queuedTask.id}' is queued and pinned to session '${args.session_id}' \u2014 it auto-delivers the moment the session goes idle. Use mesh_status or mesh_task_history to track it; no manual resend needed.`
6698
6784
  });
6699
6785
  }
6700
6786
  }
@@ -6886,7 +6972,11 @@ async function meshReadChat(ctx, args) {
6886
6972
  const compactPayload = compactChatPayload(payload, {
6887
6973
  nodeId: args.node_id,
6888
6974
  sessionId: args.session_id,
6889
- limit: args.tail ?? 10
6975
+ limit: args.tail ?? 10,
6976
+ // Carry the daemon's Stage 6 turn projection (attemptId/turnStage/
6977
+ // authority/terminal outcome) so the slim response stays at parity
6978
+ // with daemon read_chat and mesh_status. Non-content scalars only.
6979
+ preserveTurn: true
6890
6980
  });
6891
6981
  return JSON.stringify(
6892
6982
  payload.pollingAdvisory ? { ...compactPayload, pollingAdvisory: payload.pollingAdvisory } : compactPayload,
@@ -7365,7 +7455,14 @@ async function meshRestartDaemon(ctx, args) {
7365
7455
  meshId: ctx.mesh.id,
7366
7456
  nodeId: node.id,
7367
7457
  inlineMesh: ctx.mesh,
7368
- ...args.channel ? { channel: args.channel } : {}
7458
+ ...args.channel ? { channel: args.channel } : {},
7459
+ ...args.mode ? { mode: args.mode } : {},
7460
+ ...args.force === true ? { force: true } : {},
7461
+ ...args.self_only === true ? { selfOnly: true } : {},
7462
+ ...args.when_idle === true ? { whenIdle: true } : {},
7463
+ ...args.cancel_when_idle === true ? { cancelWhenIdle: true } : {},
7464
+ ...typeof args.timeout_ms === "number" ? { timeoutMs: args.timeout_ms } : {},
7465
+ ...args.kill_session_host === true ? { killSessionHost: true } : {}
7369
7466
  });
7370
7467
  return JSON.stringify(unwrapCommandPayload(result), null, 2);
7371
7468
  } catch (e) {
@@ -7405,6 +7502,23 @@ async function meshCheckpoint(ctx, args) {
7405
7502
  }
7406
7503
  async function meshCloneNode(ctx, args) {
7407
7504
  const sourceNode = await findNodeWithRefresh(ctx, args.source_node_id);
7505
+ const planned = unwrapCommandPayload(await commandForNode(ctx, sourceNode, "plan_mesh_onboarding", {
7506
+ workspace: sourceNode.workspace,
7507
+ meshId: ctx.mesh.id,
7508
+ inlineMesh: ctx.mesh,
7509
+ operation: "clone_worktree",
7510
+ branch: args.branch
7511
+ }));
7512
+ if (!planned?.success) {
7513
+ return JSON.stringify({
7514
+ success: false,
7515
+ dry_run: true,
7516
+ code: planned?.code || "onboarding_blocked",
7517
+ error: planned?.error || "Worktree clone preflight failed",
7518
+ action: planned?.action,
7519
+ raw: planned
7520
+ }, null, 2);
7521
+ }
7408
7522
  const result = await commandForNode(ctx, sourceNode, "clone_mesh_node", {
7409
7523
  meshId: ctx.mesh.id,
7410
7524
  sourceNodeId: args.source_node_id,
@@ -7455,21 +7569,105 @@ async function meshRemoveNode(ctx, args) {
7455
7569
  }
7456
7570
  return JSON.stringify({ ...result || {}, ...transportFallback ? { transportFallback } : {} }, null, 2);
7457
7571
  }
7572
+ async function meshCleanupWorktreeNodes(ctx, args) {
7573
+ const dryRun = args?.dry_run !== false;
7574
+ const wireArgs = {
7575
+ meshId: ctx.mesh.id,
7576
+ dryRun,
7577
+ ...args?.node_id ? { nodeId: args.node_id } : {}
7578
+ };
7579
+ let result;
7580
+ try {
7581
+ if (args?.node_id) {
7582
+ const node = await findNodeWithRefresh(ctx, args.node_id);
7583
+ result = await commandForNode(ctx, node, "cleanup_worktree_nodes", wireArgs);
7584
+ } else {
7585
+ result = await ctx.transport.command("cleanup_worktree_nodes", wireArgs);
7586
+ }
7587
+ } catch (e) {
7588
+ return JSON.stringify({
7589
+ success: false,
7590
+ code: isP2pTransportUnavailableError(e) ? "p2p_unavailable" : "mesh_cleanup_worktree_nodes_failed",
7591
+ error: e?.message || String(e),
7592
+ recoveryHint: "Inspect mesh_status and retry after resolving the reported failure; per-node reasons are never hidden in a successful plan."
7593
+ }, null, 2);
7594
+ }
7595
+ if (!dryRun && result?.success && Array.isArray(result.entries)) {
7596
+ let changed = false;
7597
+ for (const entry of result.entries) {
7598
+ if (entry?.execution?.success && entry.execution.removed) {
7599
+ const idx = ctx.mesh.nodes.findIndex((n) => n.id === entry.nodeId);
7600
+ if (idx >= 0) {
7601
+ ctx.mesh.nodes.splice(idx, 1);
7602
+ changed = true;
7603
+ }
7604
+ }
7605
+ }
7606
+ if (changed) ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7607
+ }
7608
+ return JSON.stringify(result ?? { success: false, error: "no response from daemon" }, null, 2);
7609
+ }
7458
7610
 
7459
7611
  // src/tools/mesh-tools-crud.ts
7612
+ async function meshPlanOnboarding(transport, args, defaultMeshId) {
7613
+ const workspace = typeof args?.workspace === "string" ? args.workspace.trim() : "";
7614
+ if (!workspace) {
7615
+ return JSON.stringify({
7616
+ success: false,
7617
+ dryRun: true,
7618
+ code: "workspace_required",
7619
+ error: "workspace required",
7620
+ action: "Pass an absolute path on the daemon that owns the checkout."
7621
+ }, null, 2);
7622
+ }
7623
+ const meshId = typeof args?.mesh_id === "string" && args.mesh_id.trim() ? args.mesh_id.trim() : typeof defaultMeshId === "string" ? defaultMeshId.trim() : "";
7624
+ const result = await transport.command("plan_mesh_onboarding", {
7625
+ workspace,
7626
+ ...meshId ? { meshId } : {},
7627
+ ...args?.operation ? { operation: args.operation } : {},
7628
+ ...typeof args?.branch === "string" && args.branch.trim() ? { branch: args.branch.trim() } : {}
7629
+ });
7630
+ return JSON.stringify(unwrapCommandPayload(result), null, 2);
7631
+ }
7460
7632
  async function meshCreate(transport, args) {
7461
7633
  const name = typeof args?.name === "string" ? args.name.trim() : "";
7462
7634
  if (!name) {
7463
7635
  return JSON.stringify({ success: false, error: "name required" }, null, 2);
7464
7636
  }
7465
- const repoRemoteUrl = typeof args?.repo_remote_url === "string" ? args.repo_remote_url.trim() : "";
7466
- const repoIdentity = typeof args?.repo_identity === "string" ? args.repo_identity.trim() : "";
7467
- const defaultBranch = typeof args?.default_branch === "string" ? args.default_branch.trim() : "";
7637
+ let repoRemoteUrl = typeof args?.repo_remote_url === "string" ? args.repo_remote_url.trim() : "";
7638
+ let repoIdentity = typeof args?.repo_identity === "string" ? args.repo_identity.trim() : "";
7639
+ let defaultBranch = typeof args?.default_branch === "string" ? args.default_branch.trim() : "";
7640
+ let discovery;
7468
7641
  if (!repoRemoteUrl && !repoIdentity) {
7469
- return JSON.stringify({
7470
- success: false,
7471
- error: "Either repo_remote_url or repo_identity is required. Pass the repo's git remote URL (e.g. git@github.com:user/repo.git) or an explicit identity (e.g. github.com/user/repo)."
7472
- }, null, 2);
7642
+ const workspace = typeof args?.workspace === "string" && args.workspace.trim() ? args.workspace.trim() : void 0;
7643
+ const planned = unwrapCommandPayload(await transport.command("plan_mesh_onboarding", {
7644
+ ...workspace ? { workspace } : {},
7645
+ operation: "auto"
7646
+ }));
7647
+ if (!planned?.success) {
7648
+ return JSON.stringify({
7649
+ success: false,
7650
+ dry_run: true,
7651
+ code: planned?.code || "onboarding_blocked",
7652
+ error: planned?.error || "Could not infer repository identity from the workspace.",
7653
+ action: planned?.action,
7654
+ raw: planned
7655
+ }, null, 2);
7656
+ }
7657
+ if (planned?.plan?.kind !== "create_mesh_and_onboard") {
7658
+ return JSON.stringify({
7659
+ success: false,
7660
+ dry_run: true,
7661
+ code: "compatible_mesh_exists",
7662
+ error: planned?.plan?.summary || "A compatible mesh already exists.",
7663
+ action: "Use mesh_add_node with the compatible mesh instead of creating a duplicate mesh.",
7664
+ raw: planned
7665
+ }, null, 2);
7666
+ }
7667
+ discovery = planned.discovery;
7668
+ repoRemoteUrl = discovery?.origin?.urls?.[0] || discovery?.upstream?.urls?.[0] || "";
7669
+ repoIdentity = discovery?.repoIdentity || "";
7670
+ defaultBranch ||= discovery?.defaultBranch || "";
7473
7671
  }
7474
7672
  const createResult = await transport.command("create_mesh", {
7475
7673
  name,
@@ -7499,7 +7697,9 @@ async function meshCreate(transport, args) {
7499
7697
  const workspace = typeof args?.workspace === "string" && args.workspace.trim() ? args.workspace.trim() : "";
7500
7698
  const addResult = await transport.command("add_mesh_node", {
7501
7699
  meshId: mesh.id,
7502
- ...workspace ? { workspace } : {},
7700
+ ...workspace || discovery?.repoRoot ? { workspace: discovery?.repoRoot || workspace } : {},
7701
+ ...discovery?.repoRoot ? { repoRoot: discovery.repoRoot } : {},
7702
+ ...discovery?.isLinkedWorktree === true ? { isLocalWorktree: true } : {},
7503
7703
  inlineMesh: mesh
7504
7704
  });
7505
7705
  const addPayload = unwrapCommandPayload(addResult);
@@ -7522,12 +7722,31 @@ async function meshAddNode(transport, args, defaultMeshId) {
7522
7722
  return JSON.stringify({ success: false, error: "workspace required (absolute path to the repo checkout on the target daemon)." }, null, 2);
7523
7723
  }
7524
7724
  const providerPriority = Array.isArray(args?.provider_priority) ? args.provider_priority.map((v) => typeof v === "string" ? v.trim() : "").filter(Boolean) : typeof args?.provider_priority === "string" ? args.provider_priority.split(",").map((v) => v.trim()).filter(Boolean) : [];
7525
- const addResult = await transport.command("add_mesh_node", {
7725
+ const planResult = await transport.command("plan_mesh_onboarding", {
7526
7726
  meshId,
7527
7727
  workspace,
7728
+ operation: "add_existing",
7729
+ ...args?.inline_mesh ? { inlineMesh: args.inline_mesh } : {}
7730
+ });
7731
+ const planPayload = unwrapCommandPayload(planResult);
7732
+ if (!planPayload?.success) {
7733
+ return JSON.stringify({
7734
+ success: false,
7735
+ dry_run: true,
7736
+ code: planPayload?.code || "onboarding_blocked",
7737
+ error: planPayload?.error || "Repo Mesh onboarding preflight failed",
7738
+ action: planPayload?.action,
7739
+ raw: planPayload ?? planResult
7740
+ }, null, 2);
7741
+ }
7742
+ const discoveredWorkspace = typeof planPayload?.discovery?.repoRoot === "string" ? planPayload.discovery.repoRoot : workspace;
7743
+ const addResult = await transport.command("add_mesh_node", {
7744
+ meshId,
7745
+ workspace: discoveredWorkspace,
7746
+ repoRoot: discoveredWorkspace,
7528
7747
  ...args?.read_only === true ? { readOnly: true } : {},
7529
7748
  ...providerPriority.length ? { providerPriority } : {},
7530
- ...args?.is_worktree === true ? { isLocalWorktree: true } : {},
7749
+ ...args?.is_worktree === true || planPayload?.discovery?.isLinkedWorktree === true ? { isLocalWorktree: true } : {},
7531
7750
  ...args?.inline_mesh ? { inlineMesh: args.inline_mesh } : {}
7532
7751
  });
7533
7752
  const addPayload = unwrapCommandPayload(addResult);
@@ -7668,11 +7887,13 @@ async function meshRefinePlan(ctx, args) {
7668
7887
  }
7669
7888
  async function meshRefineNode(ctx, args) {
7670
7889
  const node = await findNodeWithRefresh(ctx, args.node_id);
7890
+ const coordinatorDaemonId = resolveCoordinatorDaemonId(ctx);
7671
7891
  const result = await commandForNode(ctx, node, "refine_mesh_node", {
7672
7892
  meshId: ctx.mesh.id,
7673
7893
  nodeId: args.node_id,
7674
7894
  ...args.execute !== void 0 ? { execute: args.execute } : {},
7675
7895
  ...args.dry_run !== void 0 ? { dryRun: args.dry_run } : {},
7896
+ ...coordinatorDaemonId ? { coordinatorDaemonId } : {},
7676
7897
  inlineMesh: ctx.mesh
7677
7898
  });
7678
7899
  if (result?.success && result.async !== true && result.removeResult?.removed !== false) {
@@ -7687,11 +7908,13 @@ async function meshRefineNode(ctx, args) {
7687
7908
  async function meshRefineBatch(ctx, args = {}) {
7688
7909
  await refreshMeshFromDaemon(ctx);
7689
7910
  const nodeIds = Array.isArray(args.node_ids) ? args.node_ids.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim()) : void 0;
7911
+ const coordinatorDaemonId = resolveCoordinatorDaemonId(ctx);
7690
7912
  const result = await ctx.transport.command("batch_refine_mesh_nodes", {
7691
7913
  meshId: ctx.mesh.id,
7692
7914
  ...nodeIds ? { nodeIds } : {},
7693
7915
  ...args.execute !== void 0 ? { execute: args.execute } : {},
7694
7916
  ...args.dry_run !== void 0 ? { dryRun: args.dry_run } : {},
7917
+ ...coordinatorDaemonId ? { coordinatorDaemonId } : {},
7695
7918
  inlineMesh: ctx.mesh
7696
7919
  });
7697
7920
  const payload = unwrapCommandPayload(result) ?? result;
@@ -7916,7 +8139,7 @@ function formatChatResult(result, sessionId, format, limit = 50, compact = false
7916
8139
  }
7917
8140
  const messages = result?.messages ?? result?.data?.messages ?? [];
7918
8141
  const source = { ...result, messages };
7919
- const compactPayload = compact ? compactChatPayload(source, { sessionId: sessionId ?? null, limit }) : null;
8142
+ const compactPayload = compact ? compactChatPayload(source, { sessionId: sessionId ?? null, limit, preserveTurn: true }) : null;
7920
8143
  const outputMessages = compact ? compactPayload.messages : messages;
7921
8144
  if (format === "json") {
7922
8145
  if (compact && compactPayload) {
@@ -8649,13 +8872,13 @@ ${raw.output}` : "";
8649
8872
  // src/tools/launch-session.ts
8650
8873
  var LAUNCH_SESSION_TOOL = {
8651
8874
  name: "launch_session",
8652
- description: "Launch a new agent session on the daemon. Supports CLI agents (e.g. hermes-cli, claude-cli, gemini-cli), ACP agents (e.g. claude-acp), and IDEs (e.g. cursor, vscode).",
8875
+ description: "Launch a new agent session on the daemon. Supports CLI agents (e.g. kimi, hermes-cli, claude-cli, gemini-cli), ACP agents (e.g. claude-acp), and IDEs (e.g. cursor, vscode).",
8653
8876
  inputSchema: {
8654
8877
  type: "object",
8655
8878
  properties: {
8656
8879
  type: {
8657
8880
  type: "string",
8658
- description: "Provider type to launch. CLI examples: hermes-cli, claude-cli, gemini-cli. ACP examples: claude-acp. IDE examples: cursor, vscode."
8881
+ description: "Provider type to launch. CLI examples: kimi, hermes-cli, claude-cli, gemini-cli. ACP examples: claude-acp. IDE examples: cursor, vscode. Manifest aliases (e.g. codex \u2192 codex-cli) resolve to the canonical provider type."
8659
8882
  },
8660
8883
  workspace: {
8661
8884
  type: "string",
@@ -8669,14 +8892,48 @@ var LAUNCH_SESSION_TOOL = {
8669
8892
  required: ["type"]
8670
8893
  }
8671
8894
  };
8895
+ function legacyHeuristicRoute(type) {
8896
+ const isCliOrAcp = type.includes("-cli") || type.includes("-acp") || type === "codex";
8897
+ return { route: isCliOrAcp ? "cli" : "ide", canonicalType: type };
8898
+ }
8899
+ async function resolveProviderRoute(transport, requestedType) {
8900
+ const type = requestedType.trim();
8901
+ if (!type) return { error: "type is required" };
8902
+ let catalog;
8903
+ try {
8904
+ catalog = await transport.command("list_provider_availability", {});
8905
+ } catch {
8906
+ catalog = null;
8907
+ }
8908
+ const providers = Array.isArray(catalog?.providers) ? catalog.providers : null;
8909
+ if (!catalog || catalog.success === false || !providers) {
8910
+ return legacyHeuristicRoute(type);
8911
+ }
8912
+ const query = type.toLowerCase();
8913
+ const hit = providers.find((p) => {
8914
+ if (!p || typeof p.type !== "string") return false;
8915
+ if (p.type.toLowerCase() === query) return true;
8916
+ const aliases = Array.isArray(p.aliases) ? p.aliases : [];
8917
+ return aliases.some((alias) => typeof alias === "string" && alias.toLowerCase() === query);
8918
+ });
8919
+ if (!hit) {
8920
+ const known = providers.map((p) => p && typeof p.type === "string" ? p.type : null).filter(Boolean).sort();
8921
+ return {
8922
+ error: `Unknown provider type '${type}'. Known provider types: ${known.join(", ")}`
8923
+ };
8924
+ }
8925
+ const route = hit.category === "cli" || hit.category === "acp" ? "cli" : "ide";
8926
+ return { route, canonicalType: hit.type };
8927
+ }
8672
8928
  async function launchSession(transport, args) {
8673
- const isCliOrAcp = args.type.includes("-cli") || args.type.includes("-acp") || args.type === "codex";
8674
- const commandType = isCliOrAcp ? "launch_cli" : "launch_ide";
8675
- const payload = isCliOrAcp ? { cliType: args.type, dir: args.workspace ?? "~", ...args.model ? { model: args.model } : {} } : { ideType: args.type, enableCdp: true };
8929
+ const resolved = await resolveProviderRoute(transport, args.type);
8930
+ if ("error" in resolved) return `Error: ${resolved.error}`;
8931
+ const commandType = resolved.route === "cli" ? "launch_cli" : "launch_ide";
8932
+ const payload = resolved.route === "cli" ? { cliType: resolved.canonicalType, dir: args.workspace ?? "~", ...args.model ? { model: args.model } : {} } : { ideType: resolved.canonicalType, enableCdp: true };
8676
8933
  const result = await transport.command(commandType, payload);
8677
8934
  if (result?.success === false) return `Error: ${result.error ?? "launch failed"}`;
8678
8935
  const id = result?.id ?? result?.sessionId;
8679
- return id ? `Session launched. id: ${id}, type: ${args.type}` : `Launched: ${JSON.stringify(result)}`;
8936
+ return id ? `Session launched. id: ${id}, type: ${resolved.canonicalType}` : `Launched: ${JSON.stringify(result)}`;
8680
8937
  }
8681
8938
 
8682
8939
  // src/tools/stop-session.ts
@@ -8844,7 +9101,7 @@ async function startMcpServer(opts) {
8844
9101
  const meshCtx = { mesh, transport, ...localDaemonId ? { localDaemonId } : {}, ...localMachineId ? { localMachineId } : {}, ...coordinatorHostname ? { coordinatorHostname } : {}, ...coordinatorSessionId ? { coordinatorSessionId } : {} };
8845
9102
  const coordinatorPrompt = await buildMeshModeCoordinatorPrompt(mesh);
8846
9103
  const server2 = new import_server.Server(
8847
- { name: "adhdev-mcp-server", version: "1.0.27" },
9104
+ { name: "adhdev-mcp-server", version: "1.0.28" },
8848
9105
  { capabilities: { tools: {}, resources: {} } }
8849
9106
  );
8850
9107
  const { ListResourcesRequestSchema, ReadResourceRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
@@ -8929,11 +9186,14 @@ async function startMcpServer(opts) {
8929
9186
  case "mesh_list_pending_approvals":
8930
9187
  text = await meshListPendingApprovals(meshCtx, a);
8931
9188
  break;
9189
+ case "mesh_plan_onboarding":
9190
+ text = await meshPlanOnboarding(meshCtx.transport, a, meshCtx.mesh.id);
9191
+ break;
8932
9192
  case "mesh_create":
8933
9193
  text = await meshCreate(meshCtx.transport, a);
8934
9194
  break;
8935
9195
  case "mesh_add_node":
8936
- text = await meshAddNode(meshCtx.transport, a, meshCtx.mesh.id);
9196
+ text = await meshAddNode(meshCtx.transport, { ...a, inline_mesh: meshCtx.mesh }, meshCtx.mesh.id);
8937
9197
  break;
8938
9198
  case "mesh_clone_node":
8939
9199
  text = await meshCloneNode(meshCtx, a);
@@ -8941,6 +9201,9 @@ async function startMcpServer(opts) {
8941
9201
  case "mesh_remove_node":
8942
9202
  text = await meshRemoveNode(meshCtx, a);
8943
9203
  break;
9204
+ case "mesh_cleanup_worktree_nodes":
9205
+ text = await meshCleanupWorktreeNodes(meshCtx, a);
9206
+ break;
8944
9207
  case "mesh_refine_node":
8945
9208
  text = await meshRefineNode(meshCtx, a);
8946
9209
  break;
@@ -9075,6 +9338,7 @@ async function startMcpServer(opts) {
9075
9338
  // Mesh bootstrap: create a mesh + register its first node from an MCP-only agent.
9076
9339
  // Exposed in standard mode precisely because this is the no-mesh-yet context —
9077
9340
  // mesh mode refuses to boot without an existing meshId (see the mesh-mode block above).
9341
+ MESH_PLAN_ONBOARDING_TOOL,
9078
9342
  MESH_CREATE_TOOL,
9079
9343
  MESH_ADD_NODE_TOOL,
9080
9344
  ...isLocal ? [SCREENSHOT_TOOL] : []
@@ -9170,6 +9434,10 @@ async function startMcpServer(opts) {
9170
9434
  const text = await meshCreate(transport, a);
9171
9435
  return { content: [{ type: "text", text }] };
9172
9436
  }
9437
+ case "mesh_plan_onboarding": {
9438
+ const text = await meshPlanOnboarding(transport, a);
9439
+ return { content: [{ type: "text", text }] };
9440
+ }
9173
9441
  case "mesh_add_node": {
9174
9442
  const text = await meshAddNode(transport, a);
9175
9443
  return { content: [{ type: "text", text }] };