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

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.
@@ -90,6 +90,7 @@ export declare class DaemonCommandRouter {
90
90
  private removeInlineMeshNode;
91
91
  private normalizeMeshSessionCleanupMode;
92
92
  private sessionMatchesMeshNode;
93
+ private cleanupLocalWorktreeNode;
93
94
  private isCompletedHostedSession;
94
95
  private cleanupMeshSessions;
95
96
  private traceSessionHostAction;
@@ -36,6 +36,7 @@ export interface AddNodeOptions {
36
36
  workspace: string;
37
37
  repoRoot?: string;
38
38
  daemonId?: string;
39
+ machineId?: string;
39
40
  userOverrides?: Partial<RepoMeshNodeCapabilities>;
40
41
  policy?: RepoMeshNodePolicy;
41
42
  isLocalWorktree?: boolean;
@@ -31,6 +31,10 @@ export interface WorktreeEntry {
31
31
  branch: string | null;
32
32
  bare: boolean;
33
33
  }
34
+ export interface WorktreeRemoveOptions {
35
+ /** Refuse to remove a worktree with uncommitted or untracked changes. */
36
+ requireClean?: boolean;
37
+ }
34
38
  export interface WorktreeRemoveResult {
35
39
  success: true;
36
40
  removedPath: string;
@@ -49,9 +53,9 @@ export declare function createWorktree(opts: WorktreeCreateOptions): Promise<Wor
49
53
  /**
50
54
  * Remove a git worktree and clean up the directory.
51
55
  *
52
- * Runs: git worktree remove <worktreePath> --force
56
+ * Runs: git worktree remove <worktreePath>
53
57
  */
54
- export declare function removeWorktree(repoRoot: string, worktreePath: string): Promise<WorktreeRemoveResult>;
58
+ export declare function removeWorktree(repoRoot: string, worktreePath: string, opts?: WorktreeRemoveOptions): Promise<WorktreeRemoveResult>;
55
59
  /**
56
60
  * List all worktrees for a repository.
57
61
  *
package/dist/index.js 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 (!(0, import_node_fs2.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,
@@ -546,6 +558,7 @@ function addNode(meshId, opts) {
546
558
  workspace: opts.workspace.trim(),
547
559
  repoRoot: opts.repoRoot,
548
560
  daemonId: opts.daemonId,
561
+ machineId: opts.machineId,
549
562
  userOverrides: opts.userOverrides || {},
550
563
  policy: opts.policy || {},
551
564
  isLocalWorktree: opts.isLocalWorktree,
@@ -685,6 +698,7 @@ function buildRulesSection(coordinatorCliType) {
685
698
  - **Respect node capabilities.** Don't send build tasks to read-only nodes. Don't push from nodes that aren't allowed to.
686
699
  - **Never fabricate tool results.** Always call the actual tool; never pretend you did.
687
700
  - **Clean up worktree nodes.** After a worktree task completes and its changes are merged or checkpointed, call \`mesh_remove_node\` to free resources.
701
+ - **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.
688
702
  - **Name worktree branches meaningfully.** Use descriptive names like \`feat/auth-refactor\` or \`fix/build-123\`.${coordinatorNote}`;
689
703
  }
690
704
  var TOOLS_SECTION, TOOL_EXPOSURE_PREFLIGHT_SECTION, WORKFLOW_SECTION;
@@ -706,6 +720,7 @@ var init_coordinator_prompt = __esm({
706
720
  | \`mesh_checkpoint\` | Create a git checkpoint on a node |
707
721
  | \`mesh_approve\` | Approve/reject a pending agent action |
708
722
  | \`mesh_clone_node\` | Create a worktree node for isolated parallel branch work |
723
+ | \`mesh_refine_node\` | Validate and merge a completed worktree node back into its base branch |
709
724
  | \`mesh_remove_node\` | Remove a node (cleans up worktree if applicable) |`;
710
725
  TOOL_EXPOSURE_PREFLIGHT_SECTION = `## Tool Exposure Preflight
711
726
 
@@ -722,8 +737,9 @@ Before doing any coordinator work, confirm that the actual callable tool list in
722
737
  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\`.
723
738
  5. **Verify** \u2014 When a task reports completion or git work is visible, call \`mesh_git_status\` to verify changes were made.
724
739
  6. **Checkpoint** \u2014 Call \`mesh_checkpoint\` to save the work.
725
- 7. **Clean up** \u2014 Remove worktree nodes via \`mesh_remove_node\` after their work is merged or no longer needed.
726
- 8. **Report** \u2014 Summarize what was done, what changed, and any issues.
740
+ 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.
741
+ 8. **Clean up** \u2014 Remove worktree nodes via \`mesh_remove_node\` after their work is merged or no longer needed.
742
+ 9. **Report** \u2014 Summarize what was done, what changed, any issues, and the branch convergence state.
727
743
 
728
744
  ## Failure Recovery
729
745
 
@@ -2770,6 +2786,7 @@ var init_provider_cli_adapter = __esm({
2770
2786
  this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
2771
2787
  this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
2772
2788
  this.cliScripts = provider.scripts || {};
2789
+ this.scriptState = typeof this.cliScripts.createState === "function" ? this.cliScripts.createState() ?? null : null;
2773
2790
  const scriptNames = listCliScriptNames(this.cliScripts);
2774
2791
  if (scriptNames.length > 0) {
2775
2792
  LOG.info("CLI", `[${this.cliType}] CLI scripts: [${scriptNames.join(", ")}]`);
@@ -3039,7 +3056,7 @@ ${lastSnapshot}`;
3039
3056
  this.cliScripts = scripts;
3040
3057
  this.parsedStatusCache = null;
3041
3058
  this.parseErrorMessage = null;
3042
- this.scriptState = typeof scripts.createState === "function" ? scripts.createState() : null;
3059
+ this.scriptState = typeof scripts.createState === "function" ? scripts.createState() ?? null : null;
3043
3060
  const scriptNames = listCliScriptNames(scripts);
3044
3061
  LOG.info("CLI", `[${this.cliType}] CLI scripts injected: [${scriptNames.join(", ")}]`);
3045
3062
  }
@@ -4082,7 +4099,7 @@ ${lastSnapshot}`;
4082
4099
  scope: this.currentTurnScope,
4083
4100
  runtimeSettings: this.runtimeSettings
4084
4101
  });
4085
- return await Promise.resolve(fn(this.scriptState, {
4102
+ return await Promise.resolve(this.invokeCliScript(fn, {
4086
4103
  ...input,
4087
4104
  args: args && typeof args === "object" ? { ...args } : {}
4088
4105
  }));
@@ -16374,6 +16391,8 @@ function normalizeProviderSessionId(provider, providerSessionId) {
16374
16391
  }
16375
16392
 
16376
16393
  // src/providers/cli-provider-instance.ts
16394
+ var COMPLETED_FINALIZATION_RETRY_MS = 1e3;
16395
+ var COMPLETED_FINALIZATION_MAX_WAIT_MS = 3e4;
16377
16396
  var IMAGE_MIME_EXTENSIONS = {
16378
16397
  "image/png": ".png",
16379
16398
  "image/jpeg": ".jpg",
@@ -16437,6 +16456,13 @@ function cleanupStaleMaterializedImages(dir) {
16437
16456
  } catch {
16438
16457
  }
16439
16458
  }
16459
+ function hasNonEmptyCliModalButtons(activeModal) {
16460
+ const buttons = activeModal?.buttons;
16461
+ return Array.isArray(buttons) && buttons.some((button) => String(button || "").trim().length > 0);
16462
+ }
16463
+ function isCliGeneratingLikeStatus(status) {
16464
+ return status === "generating" || status === "streaming" || status === "long_generating" || status === "starting";
16465
+ }
16440
16466
  function buildCliStructuredInputPrompt(input, options = {}) {
16441
16467
  const promptParts = [];
16442
16468
  const imageRefs = [];
@@ -16721,10 +16747,12 @@ var CliProviderInstance = class {
16721
16747
  const mergedMessages = this.mergeConversationMessages(parsedMessages);
16722
16748
  const canonicalBackedHistory = this.syncCanonicalSavedHistoryIfNeeded();
16723
16749
  const dirName = this.workingDir.split("/").filter(Boolean).pop() || "session";
16750
+ const parsedChatStatus = typeof parsedStatus?.status === "string" && parsedStatus.status.trim() ? parsedStatus.status.trim() : void 0;
16751
+ const suppressStaleParsedBusyStatus = this.shouldSuppressStaleParsedBusyStatus(parsedStatus, adapterStatus);
16724
16752
  if (parsedMessages.length > 0) {
16725
16753
  const shouldSkipReplayPersist = this.suppressIdleHistoryReplay && adapterStatus.status === "idle" && parsedStatus?.status === "idle";
16726
16754
  let messagesToSave = parsedMessages;
16727
- if (parsedStatus?.status === "generating" || parsedStatus?.status === "long_generating") {
16755
+ if (!suppressStaleParsedBusyStatus && (parsedChatStatus === "generating" || parsedChatStatus === "long_generating")) {
16728
16756
  const lastIdx = messagesToSave.length - 1;
16729
16757
  if (lastIdx >= 0 && messagesToSave[lastIdx]?.role === "assistant") {
16730
16758
  messagesToSave = messagesToSave.slice(0, lastIdx);
@@ -16758,6 +16786,7 @@ var CliProviderInstance = class {
16758
16786
  summaryMetadata: this.summaryMetadata,
16759
16787
  controlValues: this.controlValues
16760
16788
  });
16789
+ const activeChatStatus = parseErrorMessage ? "error" : autoApproveActive && parsedStatus?.status === "waiting_approval" ? "generating" : adapterStatus.status !== "idle" ? visibleStatus : suppressStaleParsedBusyStatus ? visibleStatus : parsedChatStatus || visibleStatus;
16761
16790
  return {
16762
16791
  type: this.type,
16763
16792
  name: this.provider.name,
@@ -16767,7 +16796,7 @@ var CliProviderInstance = class {
16767
16796
  activeChat: {
16768
16797
  id: `${this.type}_${this.workingDir}`,
16769
16798
  title: parsedStatus?.title || dirName,
16770
- status: parseErrorMessage ? "error" : autoApproveActive && parsedStatus?.status === "waiting_approval" ? "generating" : adapterStatus.status !== "idle" ? visibleStatus : parsedStatus?.status || visibleStatus,
16799
+ status: activeChatStatus,
16771
16800
  messages: mergedMessages,
16772
16801
  activeModal: autoApproveActive ? null : parsedStatus?.activeModal ?? adapterStatus.activeModal,
16773
16802
  inputContent: ""
@@ -16897,6 +16926,102 @@ var CliProviderInstance = class {
16897
16926
  }
16898
16927
  this.applyProviderResponse(parsed.payload, { phase: "immediate" });
16899
16928
  }
16929
+ completionHasFinalAssistantMessage(messages) {
16930
+ const visibleMessages = (Array.isArray(messages) ? messages : []).filter((message) => isUserFacingChatMessage(message));
16931
+ const lastVisible = visibleMessages[visibleMessages.length - 1];
16932
+ const role = typeof lastVisible?.role === "string" ? lastVisible.role.trim().toLowerCase() : "";
16933
+ const content = lastVisible ? flattenContent(lastVisible.content).trim() : "";
16934
+ return role === "assistant" && !!content;
16935
+ }
16936
+ hasAdapterPendingResponse() {
16937
+ const adapterAny = this.adapter;
16938
+ if (adapterAny?.isWaitingForResponse === true) return true;
16939
+ if (adapterAny?.currentTurnScope) return true;
16940
+ try {
16941
+ if (typeof this.adapter.isProcessing === "function" && this.adapter.isProcessing()) return true;
16942
+ } catch {
16943
+ }
16944
+ try {
16945
+ const partial = typeof this.adapter.getPartialResponse === "function" ? this.adapter.getPartialResponse() : "";
16946
+ if (typeof partial === "string" && partial.trim()) return true;
16947
+ } catch {
16948
+ }
16949
+ return false;
16950
+ }
16951
+ shouldSuppressStaleParsedBusyStatus(parsedStatus, adapterStatus) {
16952
+ const parsedRawStatus = typeof parsedStatus?.status === "string" ? parsedStatus.status.trim() : "";
16953
+ const adapterRawStatus = typeof adapterStatus?.status === "string" ? adapterStatus.status.trim() : "";
16954
+ if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
16955
+ if (adapterRawStatus !== "idle") return false;
16956
+ if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
16957
+ return !this.hasAdapterPendingResponse();
16958
+ }
16959
+ getCompletedFinalizationBlockReason(latestVisibleStatus) {
16960
+ if (latestVisibleStatus !== "idle") return `status:${latestVisibleStatus}`;
16961
+ const adapterAny = this.adapter;
16962
+ if (adapterAny?.isWaitingForResponse === true) return "adapter_waiting_for_response";
16963
+ if (adapterAny?.currentTurnScope) return "adapter_turn_scope_active";
16964
+ const partial = typeof this.adapter.getPartialResponse === "function" ? this.adapter.getPartialResponse() : "";
16965
+ if (typeof partial === "string" && partial.trim()) return "partial_response_pending";
16966
+ let parsed;
16967
+ try {
16968
+ parsed = this.adapter.getScriptParsedStatus();
16969
+ } catch (error) {
16970
+ return `parse_error:${error?.message || String(error)}`;
16971
+ }
16972
+ const parsedStatus = typeof parsed?.status === "string" ? parsed.status : "unknown";
16973
+ if (parsedStatus !== "idle") return `parsed_status:${parsedStatus}`;
16974
+ if (parsed?.activeModal || parsed?.modal) return "parsed_modal_active";
16975
+ if (!this.completionHasFinalAssistantMessage(parsed?.messages)) return "missing_final_assistant";
16976
+ return null;
16977
+ }
16978
+ scheduleCompletedDebounceFlush(delayMs) {
16979
+ if (this.completedDebounceTimer) clearTimeout(this.completedDebounceTimer);
16980
+ this.completedDebounceTimer = setTimeout(() => this.flushCompletedDebounceIfFinalized(), delayMs);
16981
+ }
16982
+ flushCompletedDebounceIfFinalized() {
16983
+ const pending = this.completedDebouncePending;
16984
+ if (!pending) {
16985
+ this.completedDebounceTimer = null;
16986
+ return;
16987
+ }
16988
+ const latestStatus = this.adapter.getStatus({ allowParse: false });
16989
+ const latestAutoApproveActive = latestStatus.status === "waiting_approval" && this.shouldAutoApprove();
16990
+ const latestVisibleStatus = latestAutoApproveActive ? "generating" : latestStatus.status;
16991
+ if (latestVisibleStatus !== "idle") {
16992
+ LOG.info("CLI", `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
16993
+ this.completedDebouncePending = null;
16994
+ this.completedDebounceTimer = null;
16995
+ return;
16996
+ }
16997
+ const blockReason = this.getCompletedFinalizationBlockReason(latestVisibleStatus);
16998
+ if (blockReason) {
16999
+ const waitedMs = Date.now() - pending.firstObservedAt;
17000
+ if (waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
17001
+ if (pending.loggedBlockReason !== blockReason) {
17002
+ LOG.info("CLI", `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
17003
+ pending.loggedBlockReason = blockReason;
17004
+ }
17005
+ this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
17006
+ return;
17007
+ }
17008
+ LOG.warn("CLI", `[${this.type}] suppressed completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
17009
+ this.completedDebouncePending = null;
17010
+ this.completedDebounceTimer = null;
17011
+ this.generatingStartedAt = 0;
17012
+ return;
17013
+ }
17014
+ LOG.info("CLI", `[${this.type}] completed in ${pending.duration}s`);
17015
+ this.pushEvent({
17016
+ event: "agent:generating_completed",
17017
+ chatTitle: pending.chatTitle,
17018
+ duration: pending.duration,
17019
+ timestamp: pending.timestamp
17020
+ });
17021
+ this.completedDebouncePending = null;
17022
+ this.completedDebounceTimer = null;
17023
+ this.generatingStartedAt = 0;
17024
+ }
16900
17025
  maybeAutoApproveStatus(adapterStatus, now = Date.now()) {
16901
17026
  const autoApproveActive = adapterStatus?.status === "waiting_approval" && this.shouldAutoApprove();
16902
17027
  if (autoApproveActive && !this.autoApproveBusy) {
@@ -16994,26 +17119,8 @@ var CliProviderInstance = class {
16994
17119
  this.generatingDebouncePending = null;
16995
17120
  this.generatingStartedAt = 0;
16996
17121
  } else {
16997
- if (this.completedDebounceTimer) clearTimeout(this.completedDebounceTimer);
16998
- this.completedDebouncePending = { chatTitle, duration, timestamp: now };
16999
- this.completedDebounceTimer = setTimeout(() => {
17000
- if (this.completedDebouncePending) {
17001
- const latestStatus = this.adapter.getStatus({ allowParse: false });
17002
- const latestAutoApproveActive = latestStatus.status === "waiting_approval" && this.shouldAutoApprove();
17003
- const latestVisibleStatus = latestAutoApproveActive ? "generating" : latestStatus.status;
17004
- if (latestVisibleStatus !== "idle") {
17005
- LOG.info("CLI", `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
17006
- this.completedDebouncePending = null;
17007
- this.completedDebounceTimer = null;
17008
- return;
17009
- }
17010
- LOG.info("CLI", `[${this.type}] completed in ${this.completedDebouncePending.duration}s`);
17011
- this.pushEvent({ event: "agent:generating_completed", ...this.completedDebouncePending });
17012
- this.completedDebouncePending = null;
17013
- this.generatingStartedAt = 0;
17014
- }
17015
- this.completedDebounceTimer = null;
17016
- }, 3e3);
17122
+ this.completedDebouncePending = { chatTitle, duration, timestamp: now, firstObservedAt: now };
17123
+ this.scheduleCompletedDebounceFlush(3e3);
17017
17124
  }
17018
17125
  } else if (newStatus === "idle" && this.lastStatus === "starting") {
17019
17126
  this.pushEvent({ event: "agent:ready", chatTitle, timestamp: now });
@@ -23196,6 +23303,89 @@ var DaemonCommandRouter = class {
23196
23303
  if (record?.meta?.meshNodeId === nodeId) return true;
23197
23304
  return false;
23198
23305
  }
23306
+ async cleanupLocalWorktreeNode(args) {
23307
+ const workspace = typeof args.node?.workspace === "string" ? args.node.workspace.trim() : "";
23308
+ if (!workspace) {
23309
+ return {
23310
+ success: false,
23311
+ code: "mesh_worktree_cleanup_missing_workspace",
23312
+ error: `Worktree node '${args.nodeId}' is missing workspace metadata`,
23313
+ recoveryHint: "Inspect the mesh node record before removing it, or remove stale metadata manually only after confirming no managed worktree remains."
23314
+ };
23315
+ }
23316
+ const worktreeExists = fs10.existsSync(workspace);
23317
+ 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);
23318
+ const repoRoot = typeof sourceNode?.repoRoot === "string" && sourceNode.repoRoot.trim() ? sourceNode.repoRoot.trim() : typeof sourceNode?.workspace === "string" && sourceNode.workspace.trim() ? sourceNode.workspace.trim() : "";
23319
+ if (!worktreeExists) {
23320
+ return { success: true, skipped: true, removedPath: workspace, repoRoot: repoRoot || void 0, reason: "worktree_path_missing" };
23321
+ }
23322
+ if (!repoRoot || !fs10.existsSync(repoRoot)) {
23323
+ return {
23324
+ success: false,
23325
+ code: "mesh_worktree_cleanup_missing_source_repo",
23326
+ error: `Refusing to remove worktree '${workspace}' because the source repo root is unavailable`,
23327
+ recoveryHint: "Run mesh_remove_node from the machine that owns the source repo, or verify the source node metadata before retrying."
23328
+ };
23329
+ }
23330
+ if (typeof args.node?.worktreeBranch !== "string" || !args.node.worktreeBranch.trim()) {
23331
+ return {
23332
+ success: false,
23333
+ code: "mesh_worktree_cleanup_missing_branch",
23334
+ error: `Refusing to remove worktree '${workspace}' because worktreeBranch metadata is missing`,
23335
+ recoveryHint: "Confirm this is an ADHDev-managed worktree before removing it manually; managed worktree nodes include worktreeBranch metadata."
23336
+ };
23337
+ }
23338
+ const { resolveWorktreePath: resolveWorktreePath2, listWorktrees: listWorktrees2, removeWorktree: removeWorktree2 } = await Promise.resolve().then(() => (init_git_worktree(), git_worktree_exports));
23339
+ const normalizePath = (value) => {
23340
+ const resolved = (0, import_path6.resolve)(value);
23341
+ try {
23342
+ return fs10.realpathSync(resolved);
23343
+ } catch {
23344
+ return resolved;
23345
+ }
23346
+ };
23347
+ const expectedPath = normalizePath(resolveWorktreePath2(repoRoot, String(args.mesh?.name || args.mesh?.id || "mesh"), args.node.worktreeBranch));
23348
+ const actualPath = normalizePath(workspace);
23349
+ if (actualPath !== expectedPath) {
23350
+ return {
23351
+ success: false,
23352
+ code: "mesh_worktree_cleanup_unexpected_path",
23353
+ error: `Refusing to remove worktree '${workspace}' because it is not at the expected managed path '${expectedPath}'`,
23354
+ recoveryHint: "Use git worktree list/status to inspect the path. Retry only after confirming the mesh node metadata points to an ADHDev-managed worktree."
23355
+ };
23356
+ }
23357
+ const entries = await listWorktrees2(repoRoot);
23358
+ const managedEntry = entries.find((entry) => normalizePath(entry.path) === actualPath);
23359
+ if (!managedEntry) {
23360
+ return {
23361
+ success: false,
23362
+ code: "mesh_worktree_cleanup_not_registered",
23363
+ error: `Refusing to remove '${workspace}' because it is not registered in git worktree list for '${repoRoot}'`,
23364
+ recoveryHint: "Inspect git worktree list --porcelain from the source repo. If the path was already removed, prune git worktrees before retrying."
23365
+ };
23366
+ }
23367
+ if (managedEntry.branch && managedEntry.branch !== args.node.worktreeBranch) {
23368
+ return {
23369
+ success: false,
23370
+ code: "mesh_worktree_cleanup_branch_mismatch",
23371
+ error: `Refusing to remove '${workspace}' because git reports branch '${managedEntry.branch}', expected '${args.node.worktreeBranch}'`,
23372
+ recoveryHint: "Inspect the worktree branch and mesh metadata before retrying cleanup."
23373
+ };
23374
+ }
23375
+ try {
23376
+ const result = await removeWorktree2(repoRoot, workspace, { requireClean: true });
23377
+ return { success: true, removedPath: result.removedPath, repoRoot };
23378
+ } catch (e) {
23379
+ const message = String(e?.message || e || "worktree cleanup failed");
23380
+ const dirty = message.includes("dirty worktree") || message.includes("local changes");
23381
+ return {
23382
+ success: false,
23383
+ code: dirty ? "mesh_worktree_cleanup_dirty" : "mesh_worktree_cleanup_failed",
23384
+ error: message,
23385
+ 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."
23386
+ };
23387
+ }
23388
+ }
23199
23389
  isCompletedHostedSession(record) {
23200
23390
  return record?.lifecycle === "stopped" || record?.lifecycle === "failed" || record?.lifecycle === "interrupted";
23201
23391
  }
@@ -24196,17 +24386,21 @@ var DaemonCommandRouter = class {
24196
24386
  sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode });
24197
24387
  if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
24198
24388
  }
24199
- if (node?.isLocalWorktree && node.workspace) {
24200
- try {
24201
- const sourceNode = node.clonedFromNodeId ? mesh?.nodes.find((n) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId) : mesh?.nodes.find((n) => !n.isLocalWorktree);
24202
- const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
24203
- if (repoRoot) {
24204
- const { removeWorktree: removeWorktree2 } = await Promise.resolve().then(() => (init_git_worktree(), git_worktree_exports));
24205
- await removeWorktree2(repoRoot, node.workspace);
24206
- }
24207
- } catch (e) {
24208
- LOG.warn("MeshNode", `Worktree cleanup failed for ${nodeId}: ${e.message}`);
24389
+ let worktreeCleanup;
24390
+ if (node?.isLocalWorktree) {
24391
+ const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId });
24392
+ if (cleanupResult.success === false) {
24393
+ return {
24394
+ success: false,
24395
+ removed: false,
24396
+ code: cleanupResult.code,
24397
+ error: cleanupResult.error,
24398
+ recoveryHint: cleanupResult.recoveryHint,
24399
+ ...sessionCleanup ? { sessionCleanup } : {},
24400
+ worktreeCleanup: cleanupResult
24401
+ };
24209
24402
  }
24403
+ worktreeCleanup = cleanupResult;
24210
24404
  }
24211
24405
  let removed = false;
24212
24406
  if (meshRecord?.inline) {
@@ -24232,7 +24426,7 @@ var DaemonCommandRouter = class {
24232
24426
  } catch {
24233
24427
  }
24234
24428
  }
24235
- return { success: true, removed, ...sessionCleanup ? { sessionCleanup } : {} };
24429
+ return { success: true, removed, ...sessionCleanup ? { sessionCleanup } : {}, ...worktreeCleanup ? { worktreeCleanup } : {} };
24236
24430
  } catch (e) {
24237
24431
  return { success: false, error: e.message };
24238
24432
  }
@@ -24267,6 +24461,7 @@ var DaemonCommandRouter = class {
24267
24461
  workspace: result.worktreePath,
24268
24462
  repoRoot: result.worktreePath,
24269
24463
  daemonId: sourceNode.daemonId,
24464
+ machineId: sourceNode.machineId ?? sourceNode.machine_id,
24270
24465
  userOverrides: { ...sourceNode.userOverrides || {} },
24271
24466
  policy: { ...sourceNode.policy || {} },
24272
24467
  isLocalWorktree: true,
@@ -24280,6 +24475,7 @@ var DaemonCommandRouter = class {
24280
24475
  workspace: result.worktreePath,
24281
24476
  repoRoot: result.worktreePath,
24282
24477
  daemonId: sourceNode.daemonId,
24478
+ machineId: sourceNode.machineId ?? sourceNode.machine_id,
24283
24479
  userOverrides: { ...sourceNode.userOverrides || {} },
24284
24480
  isLocalWorktree: true,
24285
24481
  worktreeBranch: result.branch,