@adhdev/daemon-core 0.9.77-rc.41 → 0.9.77-rc.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -96,13 +96,25 @@ async function createWorktree(opts) {
96
96
  branch
97
97
  };
98
98
  }
99
- async function removeWorktree(repoRoot, worktreePath) {
99
+ async function removeWorktree(repoRoot, worktreePath, opts = {}) {
100
100
  if (!existsSync(worktreePath)) {
101
101
  await pruneWorktrees(repoRoot);
102
102
  return { success: true, removedPath: worktreePath };
103
103
  }
104
+ if (opts.requireClean) {
105
+ const { stdout } = await execFileAsync2("git", ["status", "--porcelain"], {
106
+ cwd: worktreePath,
107
+ encoding: "utf8",
108
+ timeout: GIT_TIMEOUT_MS,
109
+ maxBuffer: GIT_MAX_BUFFER,
110
+ windowsHide: true
111
+ });
112
+ if (stdout.trim()) {
113
+ throw new Error(`Refusing to remove dirty worktree: ${worktreePath}`);
114
+ }
115
+ }
104
116
  try {
105
- await execFileAsync2("git", ["worktree", "remove", worktreePath, "--force"], {
117
+ await execFileAsync2("git", ["worktree", "remove", worktreePath], {
106
118
  cwd: repoRoot,
107
119
  encoding: "utf8",
108
120
  timeout: GIT_TIMEOUT_MS,
@@ -680,6 +692,7 @@ function buildRulesSection(coordinatorCliType) {
680
692
  - **Respect node capabilities.** Don't send build tasks to read-only nodes. Don't push from nodes that aren't allowed to.
681
693
  - **Never fabricate tool results.** Always call the actual tool; never pretend you did.
682
694
  - **Clean up worktree nodes.** After a worktree task completes and its changes are merged or checkpointed, call \`mesh_remove_node\` to free resources.
695
+ - **Do not strand completed branches.** A checkpointed or clean feature/worktree branch is not done by itself. Merge/refine it to the mesh default branch, or explicitly report one of \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\` with the next action.
683
696
  - **Name worktree branches meaningfully.** Use descriptive names like \`feat/auth-refactor\` or \`fix/build-123\`.${coordinatorNote}`;
684
697
  }
685
698
  var TOOLS_SECTION, TOOL_EXPOSURE_PREFLIGHT_SECTION, WORKFLOW_SECTION;
@@ -701,6 +714,7 @@ var init_coordinator_prompt = __esm({
701
714
  | \`mesh_checkpoint\` | Create a git checkpoint on a node |
702
715
  | \`mesh_approve\` | Approve/reject a pending agent action |
703
716
  | \`mesh_clone_node\` | Create a worktree node for isolated parallel branch work |
717
+ | \`mesh_refine_node\` | Validate and merge a completed worktree node back into its base branch |
704
718
  | \`mesh_remove_node\` | Remove a node (cleans up worktree if applicable) |`;
705
719
  TOOL_EXPOSURE_PREFLIGHT_SECTION = `## Tool Exposure Preflight
706
720
 
@@ -717,8 +731,9 @@ Before doing any coordinator work, confirm that the actual callable tool list in
717
731
  4. **Monitor** \u2014 Prefer event-driven completion/status notifications. Do **not** poll \`mesh_read_chat\` repeatedly. Use \`mesh_view_queue\` to see the status of all pending, assigned, completed, and failed tasks. Do not call \`mesh_read_chat\` again within a few seconds for the same generating session. Use at most one compact \`mesh_read_chat\` check after a completion/approval signal. Handle approvals via \`mesh_approve\`.
718
732
  5. **Verify** \u2014 When a task reports completion or git work is visible, call \`mesh_git_status\` to verify changes were made.
719
733
  6. **Checkpoint** \u2014 Call \`mesh_checkpoint\` to save the work.
720
- 7. **Clean up** \u2014 Remove worktree nodes via \`mesh_remove_node\` after their work is merged or no longer needed.
721
- 8. **Report** \u2014 Summarize what was done, what changed, and any issues.
734
+ 7. **Converge branches** \u2014 Before marking any task complete, classify every touched node/branch into exactly one final state: \`merged_to_main\`, \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\`. Use \`mesh_status\` branchConvergenceSummary and \`mesh_refine_node\` for clean worktree branches when safe. A task that remains on a non-main branch is not fully complete unless the final report names the follow-up state and next step.
735
+ 8. **Clean up** \u2014 Remove worktree nodes via \`mesh_remove_node\` after their work is merged or no longer needed.
736
+ 9. **Report** \u2014 Summarize what was done, what changed, any issues, and the branch convergence state.
722
737
 
723
738
  ## Failure Recovery
724
739
 
@@ -2766,6 +2781,7 @@ var init_provider_cli_adapter = __esm({
2766
2781
  this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
2767
2782
  this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
2768
2783
  this.cliScripts = provider.scripts || {};
2784
+ this.scriptState = typeof this.cliScripts.createState === "function" ? this.cliScripts.createState() ?? null : null;
2769
2785
  const scriptNames = listCliScriptNames(this.cliScripts);
2770
2786
  if (scriptNames.length > 0) {
2771
2787
  LOG.info("CLI", `[${this.cliType}] CLI scripts: [${scriptNames.join(", ")}]`);
@@ -3035,7 +3051,7 @@ ${lastSnapshot}`;
3035
3051
  this.cliScripts = scripts;
3036
3052
  this.parsedStatusCache = null;
3037
3053
  this.parseErrorMessage = null;
3038
- this.scriptState = typeof scripts.createState === "function" ? scripts.createState() : null;
3054
+ this.scriptState = typeof scripts.createState === "function" ? scripts.createState() ?? null : null;
3039
3055
  const scriptNames = listCliScriptNames(scripts);
3040
3056
  LOG.info("CLI", `[${this.cliType}] CLI scripts injected: [${scriptNames.join(", ")}]`);
3041
3057
  }
@@ -4078,7 +4094,7 @@ ${lastSnapshot}`;
4078
4094
  scope: this.currentTurnScope,
4079
4095
  runtimeSettings: this.runtimeSettings
4080
4096
  });
4081
- return await Promise.resolve(fn(this.scriptState, {
4097
+ return await Promise.resolve(this.invokeCliScript(fn, {
4082
4098
  ...input,
4083
4099
  args: args && typeof args === "object" ? { ...args } : {}
4084
4100
  }));
@@ -16149,6 +16165,8 @@ function normalizeProviderSessionId(provider, providerSessionId) {
16149
16165
  }
16150
16166
 
16151
16167
  // src/providers/cli-provider-instance.ts
16168
+ var COMPLETED_FINALIZATION_RETRY_MS = 1e3;
16169
+ var COMPLETED_FINALIZATION_MAX_WAIT_MS = 3e4;
16152
16170
  var IMAGE_MIME_EXTENSIONS = {
16153
16171
  "image/png": ".png",
16154
16172
  "image/jpeg": ".jpg",
@@ -16212,6 +16230,13 @@ function cleanupStaleMaterializedImages(dir) {
16212
16230
  } catch {
16213
16231
  }
16214
16232
  }
16233
+ function hasNonEmptyCliModalButtons(activeModal) {
16234
+ const buttons = activeModal?.buttons;
16235
+ return Array.isArray(buttons) && buttons.some((button) => String(button || "").trim().length > 0);
16236
+ }
16237
+ function isCliGeneratingLikeStatus(status) {
16238
+ return status === "generating" || status === "streaming" || status === "long_generating" || status === "starting";
16239
+ }
16215
16240
  function buildCliStructuredInputPrompt(input, options = {}) {
16216
16241
  const promptParts = [];
16217
16242
  const imageRefs = [];
@@ -16496,10 +16521,12 @@ var CliProviderInstance = class {
16496
16521
  const mergedMessages = this.mergeConversationMessages(parsedMessages);
16497
16522
  const canonicalBackedHistory = this.syncCanonicalSavedHistoryIfNeeded();
16498
16523
  const dirName = this.workingDir.split("/").filter(Boolean).pop() || "session";
16524
+ const parsedChatStatus = typeof parsedStatus?.status === "string" && parsedStatus.status.trim() ? parsedStatus.status.trim() : void 0;
16525
+ const suppressStaleParsedBusyStatus = this.shouldSuppressStaleParsedBusyStatus(parsedStatus, adapterStatus);
16499
16526
  if (parsedMessages.length > 0) {
16500
16527
  const shouldSkipReplayPersist = this.suppressIdleHistoryReplay && adapterStatus.status === "idle" && parsedStatus?.status === "idle";
16501
16528
  let messagesToSave = parsedMessages;
16502
- if (parsedStatus?.status === "generating" || parsedStatus?.status === "long_generating") {
16529
+ if (!suppressStaleParsedBusyStatus && (parsedChatStatus === "generating" || parsedChatStatus === "long_generating")) {
16503
16530
  const lastIdx = messagesToSave.length - 1;
16504
16531
  if (lastIdx >= 0 && messagesToSave[lastIdx]?.role === "assistant") {
16505
16532
  messagesToSave = messagesToSave.slice(0, lastIdx);
@@ -16533,6 +16560,7 @@ var CliProviderInstance = class {
16533
16560
  summaryMetadata: this.summaryMetadata,
16534
16561
  controlValues: this.controlValues
16535
16562
  });
16563
+ const activeChatStatus = parseErrorMessage ? "error" : autoApproveActive && parsedStatus?.status === "waiting_approval" ? "generating" : adapterStatus.status !== "idle" ? visibleStatus : suppressStaleParsedBusyStatus ? visibleStatus : parsedChatStatus || visibleStatus;
16536
16564
  return {
16537
16565
  type: this.type,
16538
16566
  name: this.provider.name,
@@ -16542,7 +16570,7 @@ var CliProviderInstance = class {
16542
16570
  activeChat: {
16543
16571
  id: `${this.type}_${this.workingDir}`,
16544
16572
  title: parsedStatus?.title || dirName,
16545
- status: parseErrorMessage ? "error" : autoApproveActive && parsedStatus?.status === "waiting_approval" ? "generating" : adapterStatus.status !== "idle" ? visibleStatus : parsedStatus?.status || visibleStatus,
16573
+ status: activeChatStatus,
16546
16574
  messages: mergedMessages,
16547
16575
  activeModal: autoApproveActive ? null : parsedStatus?.activeModal ?? adapterStatus.activeModal,
16548
16576
  inputContent: ""
@@ -16672,6 +16700,102 @@ var CliProviderInstance = class {
16672
16700
  }
16673
16701
  this.applyProviderResponse(parsed.payload, { phase: "immediate" });
16674
16702
  }
16703
+ completionHasFinalAssistantMessage(messages) {
16704
+ const visibleMessages = (Array.isArray(messages) ? messages : []).filter((message) => isUserFacingChatMessage(message));
16705
+ const lastVisible = visibleMessages[visibleMessages.length - 1];
16706
+ const role = typeof lastVisible?.role === "string" ? lastVisible.role.trim().toLowerCase() : "";
16707
+ const content = lastVisible ? flattenContent(lastVisible.content).trim() : "";
16708
+ return role === "assistant" && !!content;
16709
+ }
16710
+ hasAdapterPendingResponse() {
16711
+ const adapterAny = this.adapter;
16712
+ if (adapterAny?.isWaitingForResponse === true) return true;
16713
+ if (adapterAny?.currentTurnScope) return true;
16714
+ try {
16715
+ if (typeof this.adapter.isProcessing === "function" && this.adapter.isProcessing()) return true;
16716
+ } catch {
16717
+ }
16718
+ try {
16719
+ const partial = typeof this.adapter.getPartialResponse === "function" ? this.adapter.getPartialResponse() : "";
16720
+ if (typeof partial === "string" && partial.trim()) return true;
16721
+ } catch {
16722
+ }
16723
+ return false;
16724
+ }
16725
+ shouldSuppressStaleParsedBusyStatus(parsedStatus, adapterStatus) {
16726
+ const parsedRawStatus = typeof parsedStatus?.status === "string" ? parsedStatus.status.trim() : "";
16727
+ const adapterRawStatus = typeof adapterStatus?.status === "string" ? adapterStatus.status.trim() : "";
16728
+ if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
16729
+ if (adapterRawStatus !== "idle") return false;
16730
+ if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
16731
+ return !this.hasAdapterPendingResponse();
16732
+ }
16733
+ getCompletedFinalizationBlockReason(latestVisibleStatus) {
16734
+ if (latestVisibleStatus !== "idle") return `status:${latestVisibleStatus}`;
16735
+ const adapterAny = this.adapter;
16736
+ if (adapterAny?.isWaitingForResponse === true) return "adapter_waiting_for_response";
16737
+ if (adapterAny?.currentTurnScope) return "adapter_turn_scope_active";
16738
+ const partial = typeof this.adapter.getPartialResponse === "function" ? this.adapter.getPartialResponse() : "";
16739
+ if (typeof partial === "string" && partial.trim()) return "partial_response_pending";
16740
+ let parsed;
16741
+ try {
16742
+ parsed = this.adapter.getScriptParsedStatus();
16743
+ } catch (error) {
16744
+ return `parse_error:${error?.message || String(error)}`;
16745
+ }
16746
+ const parsedStatus = typeof parsed?.status === "string" ? parsed.status : "unknown";
16747
+ if (parsedStatus !== "idle") return `parsed_status:${parsedStatus}`;
16748
+ if (parsed?.activeModal || parsed?.modal) return "parsed_modal_active";
16749
+ if (!this.completionHasFinalAssistantMessage(parsed?.messages)) return "missing_final_assistant";
16750
+ return null;
16751
+ }
16752
+ scheduleCompletedDebounceFlush(delayMs) {
16753
+ if (this.completedDebounceTimer) clearTimeout(this.completedDebounceTimer);
16754
+ this.completedDebounceTimer = setTimeout(() => this.flushCompletedDebounceIfFinalized(), delayMs);
16755
+ }
16756
+ flushCompletedDebounceIfFinalized() {
16757
+ const pending = this.completedDebouncePending;
16758
+ if (!pending) {
16759
+ this.completedDebounceTimer = null;
16760
+ return;
16761
+ }
16762
+ const latestStatus = this.adapter.getStatus({ allowParse: false });
16763
+ const latestAutoApproveActive = latestStatus.status === "waiting_approval" && this.shouldAutoApprove();
16764
+ const latestVisibleStatus = latestAutoApproveActive ? "generating" : latestStatus.status;
16765
+ if (latestVisibleStatus !== "idle") {
16766
+ LOG.info("CLI", `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
16767
+ this.completedDebouncePending = null;
16768
+ this.completedDebounceTimer = null;
16769
+ return;
16770
+ }
16771
+ const blockReason = this.getCompletedFinalizationBlockReason(latestVisibleStatus);
16772
+ if (blockReason) {
16773
+ const waitedMs = Date.now() - pending.firstObservedAt;
16774
+ if (waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
16775
+ if (pending.loggedBlockReason !== blockReason) {
16776
+ LOG.info("CLI", `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
16777
+ pending.loggedBlockReason = blockReason;
16778
+ }
16779
+ this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
16780
+ return;
16781
+ }
16782
+ LOG.warn("CLI", `[${this.type}] suppressed completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
16783
+ this.completedDebouncePending = null;
16784
+ this.completedDebounceTimer = null;
16785
+ this.generatingStartedAt = 0;
16786
+ return;
16787
+ }
16788
+ LOG.info("CLI", `[${this.type}] completed in ${pending.duration}s`);
16789
+ this.pushEvent({
16790
+ event: "agent:generating_completed",
16791
+ chatTitle: pending.chatTitle,
16792
+ duration: pending.duration,
16793
+ timestamp: pending.timestamp
16794
+ });
16795
+ this.completedDebouncePending = null;
16796
+ this.completedDebounceTimer = null;
16797
+ this.generatingStartedAt = 0;
16798
+ }
16675
16799
  maybeAutoApproveStatus(adapterStatus, now = Date.now()) {
16676
16800
  const autoApproveActive = adapterStatus?.status === "waiting_approval" && this.shouldAutoApprove();
16677
16801
  if (autoApproveActive && !this.autoApproveBusy) {
@@ -16769,26 +16893,8 @@ var CliProviderInstance = class {
16769
16893
  this.generatingDebouncePending = null;
16770
16894
  this.generatingStartedAt = 0;
16771
16895
  } else {
16772
- if (this.completedDebounceTimer) clearTimeout(this.completedDebounceTimer);
16773
- this.completedDebouncePending = { chatTitle, duration, timestamp: now };
16774
- this.completedDebounceTimer = setTimeout(() => {
16775
- if (this.completedDebouncePending) {
16776
- const latestStatus = this.adapter.getStatus({ allowParse: false });
16777
- const latestAutoApproveActive = latestStatus.status === "waiting_approval" && this.shouldAutoApprove();
16778
- const latestVisibleStatus = latestAutoApproveActive ? "generating" : latestStatus.status;
16779
- if (latestVisibleStatus !== "idle") {
16780
- LOG.info("CLI", `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
16781
- this.completedDebouncePending = null;
16782
- this.completedDebounceTimer = null;
16783
- return;
16784
- }
16785
- LOG.info("CLI", `[${this.type}] completed in ${this.completedDebouncePending.duration}s`);
16786
- this.pushEvent({ event: "agent:generating_completed", ...this.completedDebouncePending });
16787
- this.completedDebouncePending = null;
16788
- this.generatingStartedAt = 0;
16789
- }
16790
- this.completedDebounceTimer = null;
16791
- }, 3e3);
16896
+ this.completedDebouncePending = { chatTitle, duration, timestamp: now, firstObservedAt: now };
16897
+ this.scheduleCompletedDebounceFlush(3e3);
16792
16898
  }
16793
16899
  } else if (newStatus === "idle" && this.lastStatus === "starting") {
16794
16900
  this.pushEvent({ event: "agent:ready", chatTitle, timestamp: now });
@@ -22976,6 +23082,89 @@ var DaemonCommandRouter = class {
22976
23082
  if (record?.meta?.meshNodeId === nodeId) return true;
22977
23083
  return false;
22978
23084
  }
23085
+ async cleanupLocalWorktreeNode(args) {
23086
+ const workspace = typeof args.node?.workspace === "string" ? args.node.workspace.trim() : "";
23087
+ if (!workspace) {
23088
+ return {
23089
+ success: false,
23090
+ code: "mesh_worktree_cleanup_missing_workspace",
23091
+ error: `Worktree node '${args.nodeId}' is missing workspace metadata`,
23092
+ recoveryHint: "Inspect the mesh node record before removing it, or remove stale metadata manually only after confirming no managed worktree remains."
23093
+ };
23094
+ }
23095
+ const worktreeExists = fs10.existsSync(workspace);
23096
+ const sourceNode = args.node?.clonedFromNodeId ? args.mesh?.nodes?.find((n) => n.id === args.node.clonedFromNodeId || n.nodeId === args.node.clonedFromNodeId) : args.mesh?.nodes?.find((n) => !n.isLocalWorktree);
23097
+ const repoRoot = typeof sourceNode?.repoRoot === "string" && sourceNode.repoRoot.trim() ? sourceNode.repoRoot.trim() : typeof sourceNode?.workspace === "string" && sourceNode.workspace.trim() ? sourceNode.workspace.trim() : "";
23098
+ if (!worktreeExists) {
23099
+ return { success: true, skipped: true, removedPath: workspace, repoRoot: repoRoot || void 0, reason: "worktree_path_missing" };
23100
+ }
23101
+ if (!repoRoot || !fs10.existsSync(repoRoot)) {
23102
+ return {
23103
+ success: false,
23104
+ code: "mesh_worktree_cleanup_missing_source_repo",
23105
+ error: `Refusing to remove worktree '${workspace}' because the source repo root is unavailable`,
23106
+ recoveryHint: "Run mesh_remove_node from the machine that owns the source repo, or verify the source node metadata before retrying."
23107
+ };
23108
+ }
23109
+ if (typeof args.node?.worktreeBranch !== "string" || !args.node.worktreeBranch.trim()) {
23110
+ return {
23111
+ success: false,
23112
+ code: "mesh_worktree_cleanup_missing_branch",
23113
+ error: `Refusing to remove worktree '${workspace}' because worktreeBranch metadata is missing`,
23114
+ recoveryHint: "Confirm this is an ADHDev-managed worktree before removing it manually; managed worktree nodes include worktreeBranch metadata."
23115
+ };
23116
+ }
23117
+ const { resolveWorktreePath: resolveWorktreePath2, listWorktrees: listWorktrees2, removeWorktree: removeWorktree2 } = await Promise.resolve().then(() => (init_git_worktree(), git_worktree_exports));
23118
+ const normalizePath = (value) => {
23119
+ const resolved = pathResolve(value);
23120
+ try {
23121
+ return fs10.realpathSync(resolved);
23122
+ } catch {
23123
+ return resolved;
23124
+ }
23125
+ };
23126
+ const expectedPath = normalizePath(resolveWorktreePath2(repoRoot, String(args.mesh?.name || args.mesh?.id || "mesh"), args.node.worktreeBranch));
23127
+ const actualPath = normalizePath(workspace);
23128
+ if (actualPath !== expectedPath) {
23129
+ return {
23130
+ success: false,
23131
+ code: "mesh_worktree_cleanup_unexpected_path",
23132
+ error: `Refusing to remove worktree '${workspace}' because it is not at the expected managed path '${expectedPath}'`,
23133
+ recoveryHint: "Use git worktree list/status to inspect the path. Retry only after confirming the mesh node metadata points to an ADHDev-managed worktree."
23134
+ };
23135
+ }
23136
+ const entries = await listWorktrees2(repoRoot);
23137
+ const managedEntry = entries.find((entry) => normalizePath(entry.path) === actualPath);
23138
+ if (!managedEntry) {
23139
+ return {
23140
+ success: false,
23141
+ code: "mesh_worktree_cleanup_not_registered",
23142
+ error: `Refusing to remove '${workspace}' because it is not registered in git worktree list for '${repoRoot}'`,
23143
+ recoveryHint: "Inspect git worktree list --porcelain from the source repo. If the path was already removed, prune git worktrees before retrying."
23144
+ };
23145
+ }
23146
+ if (managedEntry.branch && managedEntry.branch !== args.node.worktreeBranch) {
23147
+ return {
23148
+ success: false,
23149
+ code: "mesh_worktree_cleanup_branch_mismatch",
23150
+ error: `Refusing to remove '${workspace}' because git reports branch '${managedEntry.branch}', expected '${args.node.worktreeBranch}'`,
23151
+ recoveryHint: "Inspect the worktree branch and mesh metadata before retrying cleanup."
23152
+ };
23153
+ }
23154
+ try {
23155
+ const result = await removeWorktree2(repoRoot, workspace, { requireClean: true });
23156
+ return { success: true, removedPath: result.removedPath, repoRoot };
23157
+ } catch (e) {
23158
+ const message = String(e?.message || e || "worktree cleanup failed");
23159
+ const dirty = message.includes("dirty worktree") || message.includes("local changes");
23160
+ return {
23161
+ success: false,
23162
+ code: dirty ? "mesh_worktree_cleanup_dirty" : "mesh_worktree_cleanup_failed",
23163
+ error: message,
23164
+ recoveryHint: dirty ? "Commit, stash, or intentionally discard the worktree changes before retrying mesh_remove_node. The mesh registry entry is preserved until cleanup is safe." : "Inspect git worktree status/list from the source repo and retry after resolving the reported cleanup failure."
23165
+ };
23166
+ }
23167
+ }
22979
23168
  isCompletedHostedSession(record) {
22980
23169
  return record?.lifecycle === "stopped" || record?.lifecycle === "failed" || record?.lifecycle === "interrupted";
22981
23170
  }
@@ -23976,17 +24165,21 @@ var DaemonCommandRouter = class {
23976
24165
  sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode });
23977
24166
  if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
23978
24167
  }
23979
- if (node?.isLocalWorktree && node.workspace) {
23980
- try {
23981
- const sourceNode = node.clonedFromNodeId ? mesh?.nodes.find((n) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId) : mesh?.nodes.find((n) => !n.isLocalWorktree);
23982
- const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
23983
- if (repoRoot) {
23984
- const { removeWorktree: removeWorktree2 } = await Promise.resolve().then(() => (init_git_worktree(), git_worktree_exports));
23985
- await removeWorktree2(repoRoot, node.workspace);
23986
- }
23987
- } catch (e) {
23988
- LOG.warn("MeshNode", `Worktree cleanup failed for ${nodeId}: ${e.message}`);
24168
+ let worktreeCleanup;
24169
+ if (node?.isLocalWorktree) {
24170
+ const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId });
24171
+ if (cleanupResult.success === false) {
24172
+ return {
24173
+ success: false,
24174
+ removed: false,
24175
+ code: cleanupResult.code,
24176
+ error: cleanupResult.error,
24177
+ recoveryHint: cleanupResult.recoveryHint,
24178
+ ...sessionCleanup ? { sessionCleanup } : {},
24179
+ worktreeCleanup: cleanupResult
24180
+ };
23989
24181
  }
24182
+ worktreeCleanup = cleanupResult;
23990
24183
  }
23991
24184
  let removed = false;
23992
24185
  if (meshRecord?.inline) {
@@ -24012,7 +24205,7 @@ var DaemonCommandRouter = class {
24012
24205
  } catch {
24013
24206
  }
24014
24207
  }
24015
- return { success: true, removed, ...sessionCleanup ? { sessionCleanup } : {} };
24208
+ return { success: true, removed, ...sessionCleanup ? { sessionCleanup } : {}, ...worktreeCleanup ? { worktreeCleanup } : {} };
24016
24209
  } catch (e) {
24017
24210
  return { success: false, error: e.message };
24018
24211
  }