@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.
- package/.mcp-server/cli.mjs +125 -19
- package/package.json +1 -1
- package/src/core/git.ts +90 -1
- package/src/core/tools/lifecycle.ts +54 -14
package/.mcp-server/cli.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
36333
|
-
if (!
|
|
36334
|
-
|
|
36335
|
-
|
|
36336
|
-
|
|
36337
|
-
|
|
36338
|
-
|
|
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
|
-
|
|
36457
|
-
|
|
36458
|
-
|
|
36459
|
-
|
|
36460
|
-
|
|
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
|
-
|
|
36466
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
throw new Error(
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
|
|
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> {
|