@ifrankwang/openspec-agents 0.133.0 → 0.133.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp-server/cli.mjs +85 -11
- package/package.json +1 -1
- package/src/core/git.ts +104 -20
- package/src/core/tools/lifecycle.ts +3 -0
package/.mcp-server/cli.mjs
CHANGED
|
@@ -34095,7 +34095,7 @@ function reviewLayerFromMetadata(child) {
|
|
|
34095
34095
|
// src/core/git.ts
|
|
34096
34096
|
import path from "path";
|
|
34097
34097
|
import { execFile } from "node:child_process";
|
|
34098
|
-
import { readFile, writeFile, stat, rm } from "node:fs/promises";
|
|
34098
|
+
import { readFile, writeFile, stat, rm, realpath } from "node:fs/promises";
|
|
34099
34099
|
async function pathExists(p) {
|
|
34100
34100
|
try {
|
|
34101
34101
|
await stat(p);
|
|
@@ -34251,13 +34251,53 @@ async function markTaskGroupCheckboxesComplete(worktree, changeId, taskGroupId)
|
|
|
34251
34251
|
}
|
|
34252
34252
|
}
|
|
34253
34253
|
async function mergeBranchToTarget(repoDir, sourceBranch, targetBranch) {
|
|
34254
|
-
const
|
|
34254
|
+
const mainRepo = await resolveMainRepoRoot(repoDir) ?? repoDir;
|
|
34255
|
+
const ancestor = await runGitChecked(mainRepo, ["merge-base", "--is-ancestor", sourceBranch, targetBranch]);
|
|
34255
34256
|
if (ancestor.success)
|
|
34256
34257
|
return { success: true, conflict: false };
|
|
34257
34258
|
if (ancestor.exitCode !== 1) {
|
|
34258
34259
|
throw new Error(`无法判断 "${sourceBranch}" 是否已并入 "${targetBranch}":${ancestor.stderr}`);
|
|
34259
34260
|
}
|
|
34260
|
-
const
|
|
34261
|
+
const checkedOutPath = await findWorktreeCheckingOut(mainRepo, targetBranch);
|
|
34262
|
+
if (checkedOutPath) {
|
|
34263
|
+
if (!await sameDir(checkedOutPath, mainRepo)) {
|
|
34264
|
+
return {
|
|
34265
|
+
success: false,
|
|
34266
|
+
conflict: false,
|
|
34267
|
+
blockedMessage: [
|
|
34268
|
+
`- **原因**: 目标分支 \`${targetBranch}\` 正被工作树 \`${checkedOutPath}\` 检出;自动推进分支指针会使该工作树的暂存区/工作区与分支引用失步,本次未执行任何合并动作。`,
|
|
34269
|
+
`- **处理**: 请在该工作树手动执行 \`git merge ${sourceBranch}\`,或移除该工作树(\`git worktree remove ${checkedOutPath}\`)后重试。`
|
|
34270
|
+
].join(`
|
|
34271
|
+
`)
|
|
34272
|
+
};
|
|
34273
|
+
}
|
|
34274
|
+
const status = await runGit(mainRepo, ["status", "--porcelain"]);
|
|
34275
|
+
if (status.trim().length > 0) {
|
|
34276
|
+
return {
|
|
34277
|
+
success: false,
|
|
34278
|
+
conflict: false,
|
|
34279
|
+
blockedMessage: [
|
|
34280
|
+
`- **原因**: 目标分支 \`${targetBranch}\` 正被主仓库检出,且主仓库存在未提交改动;为避免覆盖本地改动,本次未执行任何合并动作(分支引用未动、无半成品)。`,
|
|
34281
|
+
`- **处理**: 请先 commit 或 stash 主仓库改动后重试;或自行执行 \`git merge ${sourceBranch}\`(真实合并自带脏工作区保护)。`
|
|
34282
|
+
].join(`
|
|
34283
|
+
`)
|
|
34284
|
+
};
|
|
34285
|
+
}
|
|
34286
|
+
const mergeTree2 = await runGitChecked(mainRepo, ["merge-tree", "--write-tree", "--messages", targetBranch, sourceBranch]);
|
|
34287
|
+
if (!mergeTree2.success) {
|
|
34288
|
+
if (mergeTree2.exitCode !== 1) {
|
|
34289
|
+
throw new Error(`无法试算 "${sourceBranch}" → "${targetBranch}" 的合并:${mergeTree2.stderr}`);
|
|
34290
|
+
}
|
|
34291
|
+
return { success: false, conflict: true };
|
|
34292
|
+
}
|
|
34293
|
+
const mergeResult = await runGitChecked(mainRepo, ["merge", "--no-ff", sourceBranch]);
|
|
34294
|
+
if (!mergeResult.success) {
|
|
34295
|
+
await runGitChecked(mainRepo, ["merge", "--abort"]);
|
|
34296
|
+
return { success: false, conflict: true };
|
|
34297
|
+
}
|
|
34298
|
+
return { success: true, conflict: false };
|
|
34299
|
+
}
|
|
34300
|
+
const mergeTree = await runGitChecked(mainRepo, ["merge-tree", "--write-tree", "--messages", targetBranch, sourceBranch]);
|
|
34261
34301
|
if (!mergeTree.success) {
|
|
34262
34302
|
if (mergeTree.exitCode !== 1) {
|
|
34263
34303
|
throw new Error(`无法试算 "${sourceBranch}" → "${targetBranch}" 的合并:${mergeTree.stderr}`);
|
|
@@ -34268,12 +34308,12 @@ async function mergeBranchToTarget(repoDir, sourceBranch, targetBranch) {
|
|
|
34268
34308
|
`)[0].trim();
|
|
34269
34309
|
if (!treeOid)
|
|
34270
34310
|
throw new Error("merge-tree 未返回树对象,无法生成合并提交。");
|
|
34271
|
-
const targetOid = (await runGit(
|
|
34272
|
-
const sourceOid = (await runGit(
|
|
34311
|
+
const targetOid = (await runGit(mainRepo, ["rev-parse", targetBranch])).trim();
|
|
34312
|
+
const sourceOid = (await runGit(mainRepo, ["rev-parse", sourceBranch])).trim();
|
|
34273
34313
|
if (!targetOid || !sourceOid) {
|
|
34274
34314
|
throw new Error(`无法解析分支 OID:target="${targetOid}" source="${sourceOid}"`);
|
|
34275
34315
|
}
|
|
34276
|
-
const commitOid = (await runGit(
|
|
34316
|
+
const commitOid = (await runGit(mainRepo, [
|
|
34277
34317
|
"commit-tree",
|
|
34278
34318
|
treeOid,
|
|
34279
34319
|
"-p",
|
|
@@ -34285,7 +34325,7 @@ async function mergeBranchToTarget(repoDir, sourceBranch, targetBranch) {
|
|
|
34285
34325
|
])).trim();
|
|
34286
34326
|
if (!commitOid)
|
|
34287
34327
|
throw new Error("git commit-tree 失败:无法创建合并提交。");
|
|
34288
|
-
const updateRef = await runGitChecked(
|
|
34328
|
+
const updateRef = await runGitChecked(mainRepo, ["update-ref", `refs/heads/${targetBranch}`, commitOid, targetOid]);
|
|
34289
34329
|
if (!updateRef.success) {
|
|
34290
34330
|
throw new Error(`基础分支 "${targetBranch}" 已被并发推进,合并提交未写入(update-ref 旧值校验失败):${updateRef.stderr}`);
|
|
34291
34331
|
}
|
|
@@ -34335,14 +34375,44 @@ function parsePorcelainPaths(out) {
|
|
|
34335
34375
|
return f.replace(/^"+|"+$/g, "");
|
|
34336
34376
|
});
|
|
34337
34377
|
}
|
|
34338
|
-
async function
|
|
34339
|
-
let repoRoot;
|
|
34378
|
+
async function resolveMainRepoRoot(worktreePath) {
|
|
34340
34379
|
try {
|
|
34341
34380
|
const st = await stat(path.join(worktreePath, ".git"));
|
|
34342
|
-
|
|
34381
|
+
return st.isDirectory() ? worktreePath : await discoverRepoRoot(worktreePath);
|
|
34343
34382
|
} catch {
|
|
34344
34383
|
return null;
|
|
34345
34384
|
}
|
|
34385
|
+
}
|
|
34386
|
+
async function sameDir(a, b) {
|
|
34387
|
+
try {
|
|
34388
|
+
return await realpath(a) === await realpath(b);
|
|
34389
|
+
} catch {
|
|
34390
|
+
return path.resolve(a) === path.resolve(b);
|
|
34391
|
+
}
|
|
34392
|
+
}
|
|
34393
|
+
async function findWorktreeCheckingOut(repoRoot, branch) {
|
|
34394
|
+
const out = await runGit(repoRoot, ["worktree", "list", "--porcelain"]);
|
|
34395
|
+
for (const block of out.split(`
|
|
34396
|
+
|
|
34397
|
+
`)) {
|
|
34398
|
+
let wtPath = "";
|
|
34399
|
+
let wtBranch = "";
|
|
34400
|
+
for (const line of block.split(`
|
|
34401
|
+
`)) {
|
|
34402
|
+
if (line.startsWith("worktree "))
|
|
34403
|
+
wtPath = line.slice("worktree ".length).trim();
|
|
34404
|
+
else if (line.startsWith("branch "))
|
|
34405
|
+
wtBranch = line.slice("branch ".length).trim();
|
|
34406
|
+
}
|
|
34407
|
+
if (wtPath && wtBranch === `refs/heads/${branch}`)
|
|
34408
|
+
return wtPath;
|
|
34409
|
+
}
|
|
34410
|
+
return null;
|
|
34411
|
+
}
|
|
34412
|
+
async function detectMainRepoPollution(worktreePath) {
|
|
34413
|
+
const repoRoot = await resolveMainRepoRoot(worktreePath);
|
|
34414
|
+
if (!repoRoot)
|
|
34415
|
+
return null;
|
|
34346
34416
|
let out;
|
|
34347
34417
|
try {
|
|
34348
34418
|
out = await runGit(repoRoot, ["status", "--porcelain", "--", "openspec/"]);
|
|
@@ -41359,6 +41429,10 @@ async function completeTaskGroupExecute(params, ctx) {
|
|
|
41359
41429
|
const mergeTarget = state.baseBranch;
|
|
41360
41430
|
if (branchName && !isReviewNone) {
|
|
41361
41431
|
const mergeResult = await mergeBranchToTarget(ctx.worktree, branchName, mergeTarget);
|
|
41432
|
+
if (mergeResult.blockedMessage) {
|
|
41433
|
+
return [`- **status**: blocked`, mergeResult.blockedMessage].join(`
|
|
41434
|
+
`);
|
|
41435
|
+
}
|
|
41362
41436
|
if (!mergeResult.success) {
|
|
41363
41437
|
return [
|
|
41364
41438
|
`- **status**: blocked`,
|
|
@@ -42519,7 +42593,7 @@ async function ensureDefaultUnattended(args, ctx) {
|
|
|
42519
42593
|
}
|
|
42520
42594
|
} catch {}
|
|
42521
42595
|
}
|
|
42522
|
-
var PKG_VERSION = "0.133.
|
|
42596
|
+
var PKG_VERSION = "0.133.1";
|
|
42523
42597
|
function buildMcpServer(worktree, opts = {}) {
|
|
42524
42598
|
const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION });
|
|
42525
42599
|
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.133.
|
|
3
|
+
"version": "0.133.1",
|
|
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,6 @@
|
|
|
1
1
|
import path from "path"
|
|
2
2
|
import { execFile } from "node:child_process"
|
|
3
|
-
import { readFile, writeFile, stat, rm } from "node:fs/promises"
|
|
3
|
+
import { readFile, writeFile, stat, rm, realpath } from "node:fs/promises"
|
|
4
4
|
|
|
5
5
|
/** 判断路径是否存在于磁盘(fs 层判断,git 命令结果不作为存在性依据)。 */
|
|
6
6
|
async function pathExists(p: string): Promise<boolean> {
|
|
@@ -217,30 +217,79 @@ export async function markTaskGroupCheckboxesComplete(
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
/**
|
|
220
|
-
*
|
|
220
|
+
* 收尾合并:将任务组分支合并进基础分支。
|
|
221
221
|
*
|
|
222
|
-
*
|
|
223
|
-
* 序列:
|
|
222
|
+
* 先解析主仓库根(后续命令一律以主仓库根为 -C 目标,不沿用入参目录),再按目标分支的检出状态分流:
|
|
224
223
|
* 1. `merge-base --is-ancestor` 幂等检查:源分支已是目标分支祖先(如人工解决冲突后重调)直接成功,
|
|
225
224
|
* 不产生空合并提交;
|
|
226
|
-
* 2.
|
|
227
|
-
*
|
|
225
|
+
* 2. 检出检测(`worktree list --porcelain`,按目标分支匹配 `branch refs/heads/<X>` 行):
|
|
226
|
+
* - 目标分支正被其它 linked worktree 检出,或正被主仓库检出且主仓库有未提交改动 →
|
|
227
|
+
* 返回 blockedMessage(未执行任何 git 变更动作,由上层转 blocked 文案)——
|
|
228
|
+
* 单靠 update-ref 推进分支指针会使检出方暂存区/工作区与分支引用失步(status 显示整套反向改动);
|
|
229
|
+
* - 目标分支正被主仓库检出且干净:仍先 `merge-tree --write-tree` 内存试算(冲突零副作用返回),
|
|
230
|
+
* 无冲突则执行真实 `git merge --no-ff`(由 git 维护检出方 index/工作区;失败时 `merge --abort`
|
|
231
|
+
* 兜底恢复后按冲突形态返回,理论上仅竞态会走到此分支);
|
|
232
|
+
* 3. 无人检出(worktreeless 底层命令,主仓库工作区状态不受影响;冲突时目标分支引用未动、磁盘无半成品):
|
|
233
|
+
* `merge-tree --write-tree`(需 git ≥ 2.38)内存试算合并树:退出码 1 = 冲突,直接返回;
|
|
234
|
+
* `commit-tree` 生成双父合并提交,`update-ref` 以试算前目标分支 oid 作旧值校验推进(CAS,防并发覆盖)。
|
|
228
235
|
*
|
|
229
|
-
* @param repoDir
|
|
236
|
+
* @param repoDir 仓库内任意目录(作为主仓库根解析起点)
|
|
230
237
|
*/
|
|
231
238
|
export async function mergeBranchToTarget(
|
|
232
239
|
repoDir: string,
|
|
233
240
|
sourceBranch: string,
|
|
234
241
|
targetBranch: string
|
|
235
|
-
): Promise<{ success: boolean; conflict: boolean }> {
|
|
236
|
-
const
|
|
242
|
+
): Promise<{ success: boolean; conflict: boolean; blockedMessage?: string }> {
|
|
243
|
+
const mainRepo = (await resolveMainRepoRoot(repoDir)) ?? repoDir
|
|
244
|
+
|
|
245
|
+
const ancestor = await runGitChecked(mainRepo, ["merge-base", "--is-ancestor", sourceBranch, targetBranch])
|
|
237
246
|
if (ancestor.success) return { success: true, conflict: false }
|
|
238
247
|
// is-ancestor 退出码 1 = 尚未并入(继续合并);其余为分支缺失等真实错误
|
|
239
248
|
if (ancestor.exitCode !== 1) {
|
|
240
249
|
throw new Error(`无法判断 "${sourceBranch}" 是否已并入 "${targetBranch}":${ancestor.stderr}`)
|
|
241
250
|
}
|
|
242
251
|
|
|
243
|
-
const
|
|
252
|
+
const checkedOutPath = await findWorktreeCheckingOut(mainRepo, targetBranch)
|
|
253
|
+
if (checkedOutPath) {
|
|
254
|
+
if (!(await sameDir(checkedOutPath, mainRepo))) {
|
|
255
|
+
return {
|
|
256
|
+
success: false,
|
|
257
|
+
conflict: false,
|
|
258
|
+
blockedMessage: [
|
|
259
|
+
`- **原因**: 目标分支 \`${targetBranch}\` 正被工作树 \`${checkedOutPath}\` 检出;自动推进分支指针会使该工作树的暂存区/工作区与分支引用失步,本次未执行任何合并动作。`,
|
|
260
|
+
`- **处理**: 请在该工作树手动执行 \`git merge ${sourceBranch}\`,或移除该工作树(\`git worktree remove ${checkedOutPath}\`)后重试。`,
|
|
261
|
+
].join("\n"),
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const status = await runGit(mainRepo, ["status", "--porcelain"])
|
|
265
|
+
if (status.trim().length > 0) {
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
conflict: false,
|
|
269
|
+
blockedMessage: [
|
|
270
|
+
`- **原因**: 目标分支 \`${targetBranch}\` 正被主仓库检出,且主仓库存在未提交改动;为避免覆盖本地改动,本次未执行任何合并动作(分支引用未动、无半成品)。`,
|
|
271
|
+
`- **处理**: 请先 commit 或 stash 主仓库改动后重试;或自行执行 \`git merge ${sourceBranch}\`(真实合并自带脏工作区保护)。`,
|
|
272
|
+
].join("\n"),
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const mergeTree = await runGitChecked(mainRepo, ["merge-tree", "--write-tree", "--messages", targetBranch, sourceBranch])
|
|
276
|
+
if (!mergeTree.success) {
|
|
277
|
+
// merge-tree 退出码 1 = 冲突;其余为分支缺失等真实错误
|
|
278
|
+
if (mergeTree.exitCode !== 1) {
|
|
279
|
+
throw new Error(`无法试算 "${sourceBranch}" → "${targetBranch}" 的合并:${mergeTree.stderr}`)
|
|
280
|
+
}
|
|
281
|
+
return { success: false, conflict: true }
|
|
282
|
+
}
|
|
283
|
+
const mergeResult = await runGitChecked(mainRepo, ["merge", "--no-ff", sourceBranch])
|
|
284
|
+
if (!mergeResult.success) {
|
|
285
|
+
// 理论上仅竞态会走到此分支:兜底中止,避免主仓库停留在合并中间态
|
|
286
|
+
await runGitChecked(mainRepo, ["merge", "--abort"])
|
|
287
|
+
return { success: false, conflict: true }
|
|
288
|
+
}
|
|
289
|
+
return { success: true, conflict: false }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const mergeTree = await runGitChecked(mainRepo, ["merge-tree", "--write-tree", "--messages", targetBranch, sourceBranch])
|
|
244
293
|
if (!mergeTree.success) {
|
|
245
294
|
// merge-tree 退出码 1 = 冲突;其余为分支缺失等真实错误
|
|
246
295
|
if (mergeTree.exitCode !== 1) {
|
|
@@ -251,17 +300,17 @@ export async function mergeBranchToTarget(
|
|
|
251
300
|
const treeOid = mergeTree.stdout.split("\n")[0].trim()
|
|
252
301
|
if (!treeOid) throw new Error("merge-tree 未返回树对象,无法生成合并提交。")
|
|
253
302
|
|
|
254
|
-
const targetOid = (await runGit(
|
|
255
|
-
const sourceOid = (await runGit(
|
|
303
|
+
const targetOid = (await runGit(mainRepo, ["rev-parse", targetBranch])).trim()
|
|
304
|
+
const sourceOid = (await runGit(mainRepo, ["rev-parse", sourceBranch])).trim()
|
|
256
305
|
if (!targetOid || !sourceOid) {
|
|
257
306
|
throw new Error(`无法解析分支 OID:target="${targetOid}" source="${sourceOid}"`)
|
|
258
307
|
}
|
|
259
|
-
const commitOid = (await runGit(
|
|
308
|
+
const commitOid = (await runGit(mainRepo, [
|
|
260
309
|
"commit-tree", treeOid, "-p", targetOid, "-p", sourceOid, "-m", `Merge branch '${sourceBranch}'`,
|
|
261
310
|
])).trim()
|
|
262
311
|
if (!commitOid) throw new Error("git commit-tree 失败:无法创建合并提交。")
|
|
263
312
|
|
|
264
|
-
const updateRef = await runGitChecked(
|
|
313
|
+
const updateRef = await runGitChecked(mainRepo, ["update-ref", `refs/heads/${targetBranch}`, commitOid, targetOid])
|
|
265
314
|
if (!updateRef.success) {
|
|
266
315
|
throw new Error(`基础分支 "${targetBranch}" 已被并发推进,合并提交未写入(update-ref 旧值校验失败):${updateRef.stderr}`)
|
|
267
316
|
}
|
|
@@ -328,18 +377,53 @@ function parsePorcelainPaths(out: string): string[] {
|
|
|
328
377
|
}
|
|
329
378
|
|
|
330
379
|
/**
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
* 无法解析主仓库路径或主仓库干净时返回 null。
|
|
380
|
+
* 解析主仓库根:`.git` 为目录则该目录即主仓库根;`.git` 为文件(linked worktree)时经 discoverRepoRoot 推导。
|
|
381
|
+
* 解析失败(非 git 仓库等)返回 null,降级行为由调用方决定。
|
|
334
382
|
*/
|
|
335
|
-
|
|
336
|
-
let repoRoot: string
|
|
383
|
+
async function resolveMainRepoRoot(worktreePath: string): Promise<string | null> {
|
|
337
384
|
try {
|
|
338
385
|
const st = await stat(path.join(worktreePath, ".git"))
|
|
339
|
-
|
|
386
|
+
return st.isDirectory() ? worktreePath : await discoverRepoRoot(worktreePath)
|
|
340
387
|
} catch {
|
|
341
388
|
return null
|
|
342
389
|
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** 跨 symlink 比较两路径是否指向同一目录(macOS /tmp → /private/tmp 等形态);realpath 失败退回字面 resolve 比较。 */
|
|
393
|
+
async function sameDir(a: string, b: string): Promise<boolean> {
|
|
394
|
+
try {
|
|
395
|
+
return (await realpath(a)) === (await realpath(b))
|
|
396
|
+
} catch {
|
|
397
|
+
return path.resolve(a) === path.resolve(b)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 在 `worktree list --porcelain` 输出中查找检出指定分支的 worktree 路径。
|
|
403
|
+
* porcelain 以空行分块,每块 `worktree <path>` 开头;bare 或 detached 块无 `branch` 行,跳过。
|
|
404
|
+
*/
|
|
405
|
+
async function findWorktreeCheckingOut(repoRoot: string, branch: string): Promise<string | null> {
|
|
406
|
+
const out = await runGit(repoRoot, ["worktree", "list", "--porcelain"])
|
|
407
|
+
for (const block of out.split("\n\n")) {
|
|
408
|
+
let wtPath = ""
|
|
409
|
+
let wtBranch = ""
|
|
410
|
+
for (const line of block.split("\n")) {
|
|
411
|
+
if (line.startsWith("worktree ")) wtPath = line.slice("worktree ".length).trim()
|
|
412
|
+
else if (line.startsWith("branch ")) wtBranch = line.slice("branch ".length).trim()
|
|
413
|
+
}
|
|
414
|
+
if (wtPath && wtBranch === `refs/heads/${branch}`) return wtPath
|
|
415
|
+
}
|
|
416
|
+
return null
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 检测主仓库下 openspec 文档是否存在未提交变更(修改/新增/改名),用于编排者视图的主分支污染诊断。
|
|
421
|
+
* 兼容两种形态:`.git` 为目录(入参即主仓库,repoRoot 取自身)与 `.git` 为文件(走 discoverRepoRoot)。
|
|
422
|
+
* 无法解析主仓库路径或主仓库干净时返回 null。
|
|
423
|
+
*/
|
|
424
|
+
export async function detectMainRepoPollution(worktreePath: string): Promise<{ repoRoot: string; files: string[] } | null> {
|
|
425
|
+
const repoRoot = await resolveMainRepoRoot(worktreePath)
|
|
426
|
+
if (!repoRoot) return null
|
|
343
427
|
// 健壮性:git 调用失败(如非 git 仓库/权限/损坏)时返回 null,不使 opx_status 整体抛错;
|
|
344
428
|
// 污染诊断属编排者视图的辅助信息,缺失不应阻断状态视图渲染。
|
|
345
429
|
let out: string
|
|
@@ -1144,6 +1144,9 @@ export async function completeTaskGroupExecute(params: { change_id: string }, ct
|
|
|
1144
1144
|
const mergeTarget = state.baseBranch
|
|
1145
1145
|
if (branchName && !isReviewNone) {
|
|
1146
1146
|
const mergeResult = await mergeBranchToTarget(ctx.worktree, branchName, mergeTarget)
|
|
1147
|
+
if (mergeResult.blockedMessage) {
|
|
1148
|
+
return [`- **status**: blocked`, mergeResult.blockedMessage].join("\n")
|
|
1149
|
+
}
|
|
1147
1150
|
if (!mergeResult.success) {
|
|
1148
1151
|
return [
|
|
1149
1152
|
`- **status**: blocked`,
|