@ifrankwang/openspec-agents 0.130.0 → 0.131.0

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.
@@ -29686,6 +29686,7 @@ var EMPTY_COMPLETION_RESULT = {
29686
29686
 
29687
29687
  // src/core/tools/lifecycle.ts
29688
29688
  import path6 from "path";
29689
+ import { rmdir } from "node:fs/promises";
29689
29690
 
29690
29691
  // src/core/types.ts
29691
29692
  var CODE_DIMENSIONS = ["style", "architecture", "performance", "security", "maintainability"];
@@ -29742,7 +29743,15 @@ function reviewLayerFromMetadata(child) {
29742
29743
  // src/core/git.ts
29743
29744
  import path from "path";
29744
29745
  import { execFile } from "node:child_process";
29745
- import { readFile, writeFile, stat } from "node:fs/promises";
29746
+ import { readFile, writeFile, stat, rm } from "node:fs/promises";
29747
+ async function pathExists(p) {
29748
+ try {
29749
+ await stat(p);
29750
+ return true;
29751
+ } catch {
29752
+ return false;
29753
+ }
29754
+ }
29746
29755
  function execGit(args) {
29747
29756
  return new Promise((resolve) => {
29748
29757
  const child = execFile("git", args, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
@@ -29775,6 +29784,55 @@ async function runGit(worktree, args) {
29775
29784
  async function runGitChecked(worktree, args) {
29776
29785
  return gitRunner.runChecked(worktree, args);
29777
29786
  }
29787
+ async function removeTaskGroupWorktree(repoRoot, wtPath, opts = {}) {
29788
+ const result = { dirResolved: false, dirResolvedByFallback: false, pruned: false, branchDeleted: null, errors: [] };
29789
+ if (!await pathExists(wtPath)) {
29790
+ const pruneRes = await runGitChecked(repoRoot, ["worktree", "prune"]);
29791
+ if (pruneRes.success)
29792
+ result.pruned = true;
29793
+ else
29794
+ result.errors.push(`worktree prune 失败:${pruneRes.stderr}`);
29795
+ result.dirResolved = true;
29796
+ } else {
29797
+ const rmRes = await runGitChecked(repoRoot, ["worktree", "remove", wtPath, "--force"]);
29798
+ if (!rmRes.success)
29799
+ result.errors.push(`git worktree remove 失败:${rmRes.stderr}`);
29800
+ if (await pathExists(wtPath)) {
29801
+ try {
29802
+ await rm(wtPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
29803
+ } catch (e) {
29804
+ result.errors.push(`文件系统兜底删除 "${wtPath}" 失败:${e instanceof Error ? e.message : String(e)}`);
29805
+ }
29806
+ const pruneRes = await runGitChecked(repoRoot, ["worktree", "prune"]);
29807
+ if (pruneRes.success)
29808
+ result.pruned = true;
29809
+ else
29810
+ result.errors.push(`worktree prune 失败:${pruneRes.stderr}`);
29811
+ if (!await pathExists(wtPath)) {
29812
+ result.dirResolved = true;
29813
+ result.dirResolvedByFallback = true;
29814
+ }
29815
+ } else {
29816
+ result.dirResolved = true;
29817
+ }
29818
+ }
29819
+ if (!result.dirResolved) {
29820
+ if (opts.branchName) {
29821
+ result.errors.push(`目录 "${wtPath}" 未能删除,跳过分支 "${opts.branchName}" 的删除以保留排查线索`);
29822
+ }
29823
+ return result;
29824
+ }
29825
+ if (opts.branchName) {
29826
+ const branchRes = await runGitChecked(repoRoot, ["branch", "-D", opts.branchName]);
29827
+ if (branchRes.success)
29828
+ result.branchDeleted = true;
29829
+ else {
29830
+ result.branchDeleted = false;
29831
+ result.errors.push(`删除分支 "${opts.branchName}" 失败:${branchRes.stderr}`);
29832
+ }
29833
+ }
29834
+ return result;
29835
+ }
29778
29836
  async function getCurrentHead(worktree) {
29779
29837
  return runGit(worktree, ["rev-parse", "HEAD"]);
29780
29838
  }
@@ -32695,16 +32753,21 @@ function requireLoader() {
32695
32753
  state.result += _result;
32696
32754
  }
32697
32755
  }
32756
+ function chargeMergeWork(state) {
32757
+ state.totalMergeKeys++;
32758
+ if (state.maxTotalMergeKeys !== -1 && state.totalMergeKeys > state.maxTotalMergeKeys) {
32759
+ throwError(state, "merge keys exceeded maxTotalMergeKeys (" + state.maxTotalMergeKeys + ")");
32760
+ }
32761
+ }
32698
32762
  function mergeMappings(state, destination, source, overridableKeys) {
32699
32763
  if (!common2.isObject(source)) {
32700
32764
  throwError(state, "cannot merge mappings; the provided source object is unacceptable");
32701
32765
  }
32766
+ chargeMergeWork(state);
32702
32767
  const sourceKeys = Object.keys(source);
32703
32768
  for (let index = 0, quantity = sourceKeys.length;index < quantity; index += 1) {
32704
32769
  const key = sourceKeys[index];
32705
- if (state.maxTotalMergeKeys !== -1 && ++state.totalMergeKeys > state.maxTotalMergeKeys) {
32706
- throwError(state, "merge keys exceeded maxTotalMergeKeys (" + state.maxTotalMergeKeys + ")");
32707
- }
32770
+ chargeMergeWork(state);
32708
32771
  if (!_hasOwnProperty.call(destination, key)) {
32709
32772
  setProperty(destination, key, source[key]);
32710
32773
  overridableKeys[key] = true;
@@ -32732,6 +32795,9 @@ function requireLoader() {
32732
32795
  }
32733
32796
  if (keyTag === "tag:yaml.org,2002:merge") {
32734
32797
  if (Array.isArray(valueNode)) {
32798
+ if (valueNode.length > 100) {
32799
+ throwError(state, "abnormal merge sequence size");
32800
+ }
32735
32801
  for (let index = 0, quantity = valueNode.length;index < quantity; index += 1) {
32736
32802
  mergeMappings(state, _result, valueNode[index], overridableKeys);
32737
32803
  }
@@ -36329,13 +36395,15 @@ async function setWorktreeExecute(params, ctx) {
36329
36395
  await bindWorktreeRefs(item, existingPath, branch, state.baseBranch);
36330
36396
  reused = true;
36331
36397
  } else {
36332
- const rmResult = await runGitChecked(repoRoot, ["worktree", "remove", existingPath, "--force"]);
36333
- if (!rmResult.success) {
36334
- throw new Error(`无法清理已有 worktree "${existingPath}":${rmResult.stderr}`);
36335
- }
36336
- const branchRmResult = await runGitChecked(repoRoot, ["branch", "-D", branch]);
36337
- if (!branchRmResult.success) {
36338
- throw new Error(`无法清理已有分支 "${branch}":${branchRmResult.stderr}`);
36398
+ const cleanup = await removeTaskGroupWorktree(repoRoot, existingPath, { branchName: branch });
36399
+ if (!cleanup.dirResolved || cleanup.branchDeleted === false) {
36400
+ const problems = [];
36401
+ if (!cleanup.dirResolved)
36402
+ problems.push(`无法清理已有 worktree "${existingPath}"`);
36403
+ if (cleanup.branchDeleted === false)
36404
+ problems.push(`无法清理已有分支 "${branch}"`);
36405
+ throw new Error(`${problems.join(";")}:${cleanup.errors.join(";") || "原因未知"}
36406
+ 请手动处理后重试。`);
36339
36407
  }
36340
36408
  }
36341
36409
  }
@@ -36399,6 +36467,19 @@ async function statusExecute(params, ctx) {
36399
36467
  const exemptionItems = (await readExemptions(ctx.worktree)).items;
36400
36468
  return renderWorkflowStatusView(item, workflow, rec, { agent, orchestrator: ctx.orchestrator, identityDeclared: ctx.identityDeclared }, { state, tg, mainPollution, toolChanges, exemptedHits, exemptionItems });
36401
36469
  }
36470
+ async function sweepEmptyWorktreeParents(repoRoot, removedDir, changeId) {
36471
+ const repoRootAbs = path6.resolve(repoRoot);
36472
+ const groupLevelDir = path6.join(repoRootAbs, ".worktree", changeId);
36473
+ const worktreeLevelDir = path6.join(repoRootAbs, ".worktree");
36474
+ if (path6.dirname(path6.resolve(removedDir)) === groupLevelDir) {
36475
+ try {
36476
+ await rmdir(groupLevelDir);
36477
+ } catch {}
36478
+ }
36479
+ try {
36480
+ await rmdir(worktreeLevelDir);
36481
+ } catch {}
36482
+ }
36402
36483
  async function completeTaskGroupExecute(params, ctx) {
36403
36484
  assertOrchestrator(ctx, "opx_orch_complete_task_group");
36404
36485
  const state = await readStateByWorktree(ctx.worktree, params.change_id);
@@ -36453,17 +36534,42 @@ async function completeTaskGroupExecute(params, ctx) {
36453
36534
  `);
36454
36535
  }
36455
36536
  }
36456
- if (worktreePath && branchName) {
36457
- try {
36458
- await runGit(ctx.worktree, ["worktree", "remove", worktreePath, "--force"]);
36459
- await runGit(ctx.worktree, ["branch", "-D", branchName]);
36460
- } catch {}
36537
+ let cleanupNote = "";
36538
+ let cleanupResidualWarning = "";
36539
+ if (worktreePath) {
36540
+ const cleanup = await removeTaskGroupWorktree(ctx.worktree, worktreePath, { branchName });
36541
+ if (cleanup.dirResolved) {
36542
+ await sweepEmptyWorktreeParents(ctx.worktree, worktreePath, params.change_id);
36543
+ const successNotes = [];
36544
+ if (cleanup.dirResolvedByFallback) {
36545
+ successNotes.push("- **cleanup**: git worktree remove 未直接移除目录,已按文件系统兜底删除并 prune 管理记录。");
36546
+ }
36547
+ if (cleanup.errors.length > 0) {
36548
+ successNotes.push(`- **cleanup 部分告警**: ${cleanup.errors.join(";")}`);
36549
+ }
36550
+ cleanupNote = successNotes.join(`
36551
+ `);
36552
+ } else {
36553
+ item.metadata["cleanup_residual"] = { worktree_path: worktreePath, errors: cleanup.errors };
36554
+ cleanupResidualWarning = [
36555
+ "",
36556
+ "## ⚠️ worktree 清理残留(不影响收尾)",
36557
+ `- **残留路径**: \`${worktreePath}\``,
36558
+ "- **原因**:",
36559
+ ...cleanup.errors.map((e) => ` - ${e}`),
36560
+ `- **处理**: 请人工执行 \`rm -rf '${worktreePath}' && git worktree prune\` 清理残留目录与 git 管理记录。`,
36561
+ branchName ? `- **分支**: 分支 "${branchName}" 未删除。` : ""
36562
+ ].filter(Boolean).join(`
36563
+ `);
36564
+ }
36461
36565
  }
36462
36566
  item.metadata["completed_at"] = new Date().toISOString();
36463
36567
  await writeState(ctx.worktree, state);
36464
36568
  const doneMessage = `任务组已完成并合并到 "${mergeTarget}"。`;
36465
- return checkboxWarning ? `${doneMessage}
36466
- ${checkboxWarning}` : doneMessage;
36569
+ const notes = [cleanupNote, checkboxWarning, cleanupResidualWarning].filter(Boolean);
36570
+ return notes.length > 0 ? `${doneMessage}
36571
+ ${notes.join(`
36572
+ `)}` : doneMessage;
36467
36573
  }
36468
36574
  async function setUnattendedExecute(params, ctx) {
36469
36575
  assertOrchestrator(ctx, "opx_orch_set_unattended");
@@ -37507,7 +37613,7 @@ async function ensureDefaultUnattended(args, ctx) {
37507
37613
  }
37508
37614
  } catch {}
37509
37615
  }
37510
- var PKG_VERSION = "0.130.0";
37616
+ var PKG_VERSION = "0.131.0";
37511
37617
  function buildMcpServer(worktree, opts = {}) {
37512
37618
  const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION });
37513
37619
  for (const [name, spec] of Object.entries(TOOL_SPECS)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ifrankwang/openspec-agents",
3
- "version": "0.130.0",
3
+ "version": "0.131.0",
4
4
  "description": "OpenSpec change orchestration plugin for AI coding agents (Claude Code, Codex, ZCode, DeepSeek Harness, OpenCode)",
5
5
  "author": "ifrankwang",
6
6
  "license": "MIT",
package/src/core/git.ts CHANGED
@@ -1,6 +1,16 @@
1
1
  import path from "path"
2
2
  import { execFile } from "node:child_process"
3
- import { readFile, writeFile, stat } from "node:fs/promises"
3
+ import { readFile, writeFile, stat, rm } from "node:fs/promises"
4
+
5
+ /** 判断路径是否存在于磁盘(fs 层判断,git 命令结果不作为存在性依据)。 */
6
+ async function pathExists(p: string): Promise<boolean> {
7
+ try {
8
+ await stat(p)
9
+ return true
10
+ } catch {
11
+ return false
12
+ }
13
+ }
4
14
 
5
15
  export interface GitRunner {
6
16
  run(worktree: string, args: string[]): Promise<string>
@@ -54,6 +64,85 @@ export async function runGitChecked(
54
64
  return gitRunner.runChecked(worktree, args)
55
65
  }
56
66
 
67
+ /** worktree 清理结果:成功判定以目录从磁盘真实消失为准,不信任单条 git 命令的退出码。 */
68
+ export interface WorktreeRemovalResult {
69
+ /** 目录最终是否已从磁盘消失。 */
70
+ dirResolved: boolean
71
+ /** 目录是否经文件系统兜底删除后才消失(true 表示主路径未直接达成,发生了补救)。 */
72
+ dirResolvedByFallback: boolean
73
+ /** worktree prune 是否执行成功(幽灵管理记录清理)。 */
74
+ pruned: boolean
75
+ /** 分支删除结果:null=未指定分支或因目录残留被跳过;true/false=删除断言结果。 */
76
+ branchDeleted: boolean | null
77
+ /** 各环节失败原因聚合(人类可读),全部成功时为空数组。 */
78
+ errors: string[]
79
+ }
80
+
81
+ /**
82
+ * 删除任务组 worktree 并清理关联管理记录(收尾清理与 set_worktree 复用失败重建共用)。
83
+ *
84
+ * 序列:
85
+ * 1. 目录不存在 → 直接 prune(清理只残留管理记录的幽灵 worktree);
86
+ * 2. 目录存在 → `git worktree remove --force`;命令失败或目录仍在磁盘时走补救链:
87
+ * fs 递归删除 + prune(prune 失败仅记入 errors,不阻断);
88
+ * 3. 目录确认消失后才 `git branch -D`;仍残留则跳过删分支并记入 errors,
89
+ * 防止孤儿分支掩盖问题、以及向不完整状态发出被 git 拒绝的命令。
90
+ *
91
+ * @param repoRoot 主仓库根路径(worktree 管理命令的 -C 目标)
92
+ * @param wtPath 待删除的 worktree 路径
93
+ * @param opts.branchName 关联分支名;缺省/null 时只做 worktree 侧清理
94
+ */
95
+ export async function removeTaskGroupWorktree(
96
+ repoRoot: string,
97
+ wtPath: string,
98
+ opts: { branchName?: string | null } = {},
99
+ ): Promise<WorktreeRemovalResult> {
100
+ const result: WorktreeRemovalResult = { dirResolved: false, dirResolvedByFallback: false, pruned: false, branchDeleted: null, errors: [] }
101
+
102
+ if (!(await pathExists(wtPath))) {
103
+ const pruneRes = await runGitChecked(repoRoot, ["worktree", "prune"])
104
+ if (pruneRes.success) result.pruned = true
105
+ else result.errors.push(`worktree prune 失败:${pruneRes.stderr}`)
106
+ result.dirResolved = true
107
+ } else {
108
+ const rmRes = await runGitChecked(repoRoot, ["worktree", "remove", wtPath, "--force"])
109
+ if (!rmRes.success) result.errors.push(`git worktree remove 失败:${rmRes.stderr}`)
110
+ if (await pathExists(wtPath)) {
111
+ // 补救链:fs 物理删除目录 + prune 清理残留管理记录
112
+ try {
113
+ await rm(wtPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 })
114
+ } catch (e) {
115
+ result.errors.push(`文件系统兜底删除 "${wtPath}" 失败:${e instanceof Error ? e.message : String(e)}`)
116
+ }
117
+ const pruneRes = await runGitChecked(repoRoot, ["worktree", "prune"])
118
+ if (pruneRes.success) result.pruned = true
119
+ else result.errors.push(`worktree prune 失败:${pruneRes.stderr}`)
120
+ if (!(await pathExists(wtPath))) {
121
+ result.dirResolved = true
122
+ result.dirResolvedByFallback = true
123
+ }
124
+ } else {
125
+ result.dirResolved = true
126
+ }
127
+ }
128
+
129
+ if (!result.dirResolved) {
130
+ if (opts.branchName) {
131
+ result.errors.push(`目录 "${wtPath}" 未能删除,跳过分支 "${opts.branchName}" 的删除以保留排查线索`)
132
+ }
133
+ return result
134
+ }
135
+ if (opts.branchName) {
136
+ const branchRes = await runGitChecked(repoRoot, ["branch", "-D", opts.branchName])
137
+ if (branchRes.success) result.branchDeleted = true
138
+ else {
139
+ result.branchDeleted = false
140
+ result.errors.push(`删除分支 "${opts.branchName}" 失败:${branchRes.stderr}`)
141
+ }
142
+ }
143
+ return result
144
+ }
145
+
57
146
  export async function getCurrentHead(worktree: string): Promise<string> {
58
147
  return runGit(worktree, ["rev-parse", "HEAD"])
59
148
  }
@@ -1,8 +1,9 @@
1
1
  import path from "path"
2
+ import { rmdir } from "node:fs/promises"
2
3
  import type { OrchestrateState, TaskItem, TaskStatus, WorkflowMode } from "../types.ts"
3
4
  import { BUILD_PHASE_TARGETS, REVIEW_LAYERS, REVIEW_VERIFY_STEPS } from "../types.ts"
4
5
  import { agentToReviewLayer } from "../constants.ts"
5
- import { runGit, runGitChecked, getCurrentBranch, getMergeBase, isWorktreeClean, markTaskGroupCheckboxesComplete, mergeBranchToTarget, discoverDiskWorktrees, detectMainRepoPollution, detectChanges, type DetectChangesResult } from "../git.ts"
6
+ import { runGit, runGitChecked, getCurrentBranch, getMergeBase, isWorktreeClean, markTaskGroupCheckboxesComplete, mergeBranchToTarget, discoverDiskWorktrees, detectMainRepoPollution, detectChanges, removeTaskGroupWorktree, type DetectChangesResult } from "../git.ts"
6
7
  import { readStateByWorktree, readStateByChangeId, writeState, writeContextToWorktree } from "../state.ts"
7
8
  import { generateIsolationNamespace } from "../namespace.ts"
8
9
  import { readExemptions } from "../exemptions.ts"
@@ -576,13 +577,13 @@ export async function setWorktreeExecute(params: SetWorktreeParams, ctx: ToolCon
576
577
  await bindWorktreeRefs(item, existingPath, branch, state.baseBranch)
577
578
  reused = true
578
579
  } else {
579
- const rmResult = await runGitChecked(repoRoot, ["worktree", "remove", existingPath, "--force"])
580
- if (!rmResult.success) {
581
- throw new Error(`无法清理已有 worktree "${existingPath}":${rmResult.stderr}`)
582
- }
583
- const branchRmResult = await runGitChecked(repoRoot, ["branch", "-D", branch])
584
- if (!branchRmResult.success) {
585
- throw new Error(`无法清理已有分支 "${branch}":${branchRmResult.stderr}`)
580
+ // 复用失败的清理走共享清理函数:目录侧(git remove + fs 兜底 + prune)与分支删除统一语义
581
+ const cleanup = await removeTaskGroupWorktree(repoRoot, existingPath, { branchName: branch })
582
+ if (!cleanup.dirResolved || cleanup.branchDeleted === false) {
583
+ const problems: string[] = []
584
+ if (!cleanup.dirResolved) problems.push(`无法清理已有 worktree "${existingPath}"`)
585
+ if (cleanup.branchDeleted === false) problems.push(`无法清理已有分支 "${branch}"`)
586
+ throw new Error(`${problems.join(";")}:${cleanup.errors.join(";") || "原因未知"}\n请手动处理后重试。`)
586
587
  }
587
588
  }
588
589
  }
@@ -667,6 +668,22 @@ export async function statusExecute(params: StatusParams, ctx: ToolContext): Pro
667
668
  return renderWorkflowStatusView(item, workflow, rec, { agent, orchestrator: ctx.orchestrator, identityDeclared: ctx.identityDeclared }, { state, tg, mainPollution, toolChanges, exemptedHits, exemptionItems })
668
669
  }
669
670
 
671
+ /**
672
+ * 收尾后清扫默认布局的空父目录(`.worktree/<changeId>` 与 `.worktree`):
673
+ * 仅对这两级字面路径做非递归 rmdir(空目录才会成功),任一失败静默忽略。
674
+ * 自定义 worktree_path 时以「被删目录的实际 dirname === `.worktree/<changeId>`」判定是否可扫第一级,
675
+ * 第二级固定只试 `.worktree` 本身;其余任意父路径一律不做处理。
676
+ */
677
+ async function sweepEmptyWorktreeParents(repoRoot: string, removedDir: string, changeId: string): Promise<void> {
678
+ const repoRootAbs = path.resolve(repoRoot)
679
+ const groupLevelDir = path.join(repoRootAbs, ".worktree", changeId)
680
+ const worktreeLevelDir = path.join(repoRootAbs, ".worktree")
681
+ if (path.dirname(path.resolve(removedDir)) === groupLevelDir) {
682
+ try { await rmdir(groupLevelDir) } catch {}
683
+ }
684
+ try { await rmdir(worktreeLevelDir) } catch {}
685
+ }
686
+
670
687
  export async function completeTaskGroupExecute(params: { change_id: string }, ctx: ToolContext): Promise<string> {
671
688
  assertOrchestrator(ctx, "opx_orch_complete_task_group")
672
689
  const state = await readStateByWorktree(ctx.worktree, params.change_id)
@@ -731,17 +748,40 @@ export async function completeTaskGroupExecute(params: { change_id: string }, ct
731
748
  ].join("\n")
732
749
  }
733
750
  }
734
- if (worktreePath && branchName) {
735
- try {
736
- await runGit(ctx.worktree, ["worktree", "remove", worktreePath, "--force"])
737
- await runGit(ctx.worktree, ["branch", "-D", branchName])
738
- } catch {
751
+ // 收尾清理:只要记录了 worktree_path 就执行;branch_name 缺省时仅做 worktree 侧清理。
752
+ // 残留不阻断收尾:completed_at 照写,残留信息写入 metadata.cleanup_residual 并在返回体给出人工处理命令。
753
+ let cleanupNote = ""
754
+ let cleanupResidualWarning = ""
755
+ if (worktreePath) {
756
+ const cleanup = await removeTaskGroupWorktree(ctx.worktree, worktreePath, { branchName })
757
+ if (cleanup.dirResolved) {
758
+ await sweepEmptyWorktreeParents(ctx.worktree, worktreePath, params.change_id)
759
+ const successNotes: string[] = []
760
+ if (cleanup.dirResolvedByFallback) {
761
+ successNotes.push("- **cleanup**: git worktree remove 未直接移除目录,已按文件系统兜底删除并 prune 管理记录。")
762
+ }
763
+ if (cleanup.errors.length > 0) {
764
+ successNotes.push(`- **cleanup 部分告警**: ${cleanup.errors.join(";")}`)
765
+ }
766
+ cleanupNote = successNotes.join("\n")
767
+ } else {
768
+ item.metadata["cleanup_residual"] = { worktree_path: worktreePath, errors: cleanup.errors }
769
+ cleanupResidualWarning = [
770
+ "",
771
+ "## ⚠️ worktree 清理残留(不影响收尾)",
772
+ `- **残留路径**: \`${worktreePath}\``,
773
+ "- **原因**:",
774
+ ...cleanup.errors.map((e) => ` - ${e}`),
775
+ `- **处理**: 请人工执行 \`rm -rf '${worktreePath}' && git worktree prune\` 清理残留目录与 git 管理记录。`,
776
+ branchName ? `- **分支**: 分支 "${branchName}" 未删除。` : "",
777
+ ].filter(Boolean).join("\n")
739
778
  }
740
779
  }
741
780
  item.metadata["completed_at"] = new Date().toISOString()
742
781
  await writeState(ctx.worktree, state)
743
782
  const doneMessage = `任务组已完成并合并到 "${mergeTarget}"。`
744
- return checkboxWarning ? `${doneMessage}\n${checkboxWarning}` : doneMessage
783
+ const notes = [cleanupNote, checkboxWarning, cleanupResidualWarning].filter(Boolean)
784
+ return notes.length > 0 ? `${doneMessage}\n${notes.join("\n")}` : doneMessage
745
785
  }
746
786
 
747
787
  export async function setUnattendedExecute(params: UnattendedParams, ctx: ToolContext): Promise<string> {