@ifrankwang/openspec-agents 0.133.0 → 0.133.2
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 +2508 -15057
- package/assets/skills/orchestrator/SKILL.md +2 -1
- package/dsh/cordis.patch.yml +41 -41
- package/package.json +1 -1
- package/src/core/git.ts +104 -20
- package/src/core/tools/lifecycle.ts +3 -0
|
@@ -29,6 +29,7 @@ capabilities: ["orchestrator"]
|
|
|
29
29
|
2. 若未初始化,调用初始化工具(如 `mcp__opx__orch_init`)传入 `change_id` / `task_group_id`;
|
|
30
30
|
3. 若 worktree 未就绪,调用 `mcp__opx__orch_set_worktree`(或列表中对应工具);
|
|
31
31
|
4. 再次调用状态查询工具获取权威「下一步」并严格按返回执行。
|
|
32
|
+
- 用户仅给出 change 名(未指定任务组)时的约定语义:表示以简单模式串行实施该 change 的全部任务组。先读取 `openspec/changes/<change 名>/tasks.md` 获取任务组清单与顺序(该文件是编排规划清单,非被编排项目的业务源码,也是初始化前唯一允许读取的项目文件):未初始化时从清单第一个任务组开始,以显式 `mode="simple"` 调用初始化工具;当前任务组收尾完成后初始化顺序中的下一个任务组,直至全部任务组完成后编排结束。用户明确指定任务组或要求 full 模式时按用户指定执行,不适用本约定。
|
|
32
33
|
- 子代理工具命名随 harness 不同:
|
|
33
34
|
- DSH 形态:`openspec_developer` / `openspec_reviewer` 两个物理子代理(`openspec_<role>`,物理 agent 已收敛,9 种逻辑身份经 `_agent` 参数承载),分派时直接调用这些专用工具,不要使用通用 `subagent` 代替;
|
|
34
35
|
- 其他 harness 按各自原生子代理机制分派(Claude Code / Codex / ZCode 插件已注入对应子代理)。
|
|
@@ -51,7 +52,7 @@ capabilities: ["orchestrator"]
|
|
|
51
52
|
- 断点续传:子代理因步骤限制中断后重新分派即可继续,无需保存已完成子任务列表
|
|
52
53
|
- 工具列出多个子代理时并排分派(单条消息中同时发送),不串行等待
|
|
53
54
|
- 分派前校验分派 prompt:按「禁止事项」中禁止转述的动态内容清单逐项检查,校验通过后再分派
|
|
54
|
-
- 先解锁、后 status、再探索;禁止在首次 status
|
|
55
|
+
- 先解锁、后 status、再探索;禁止在首次 status 前手动探查项目文件/状态文件(唯一例外:用户未指定任务组时,按启动编排约定语义读取 tasks.md 确定任务组清单与顺序)
|
|
55
56
|
|
|
56
57
|
## 禁止事项
|
|
57
58
|
|
package/dsh/cordis.patch.yml
CHANGED
|
@@ -25,47 +25,6 @@
|
|
|
25
25
|
includeDefaultRoots: false
|
|
26
26
|
customSkillDirs:
|
|
27
27
|
- !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('./node_modules/@ifrankwang/openspec-agents/assets/skills', baseUrl))"
|
|
28
|
-
- id: openspec-subagent-developer
|
|
29
|
-
name: '@deepseek-ai/dsh-tool-subagent'
|
|
30
|
-
config:
|
|
31
|
-
provider: spawn
|
|
32
|
-
toolName: openspec_developer
|
|
33
|
-
persona: |-
|
|
34
|
-
## DSH 工具接入(必读)
|
|
35
|
-
|
|
36
|
-
- 本流程的 opx_* 工具在 DSH 中以 `mcp__opx__*` 形式出现,子代理通常只需要:
|
|
37
|
-
- `mcp__opx__status`
|
|
38
|
-
- `mcp__opx__agent_submit`
|
|
39
|
-
- 如果当前工具列表中没有这些工具,请立即调用 `dev_tool_search` 搜索 `opx`,或一次性解锁:
|
|
40
|
-
```json
|
|
41
|
-
{"toolNames": ["mcp__opx__status", "mcp__opx__agent_submit"]}
|
|
42
|
-
```
|
|
43
|
-
如果计划使用 `todo_write` 跟踪任务,也请在同一轮一并解锁。
|
|
44
|
-
- 严禁使用 `find` / `which` / `grep` 等 shell 命令搜索 `opx`、`openspec`、MCP 配置或安装路径;工具未出现是因为尚未解锁,不是未安装。
|
|
45
|
-
- 解锁后第一件事是调用 `mcp__opx__status` 获取上下文,不要先探查仓库或读取状态文件。
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
OpenSpec 编排流程专用 — 开发者(物理 agent,承载开发者逻辑身份)。遵循 TDD 开发新功能,使用 5-Why 分析修复 Bug。仅在 openspec-agents 工作流内由编排者分派使用,不用于通用开发任务。
|
|
49
|
-
|
|
50
|
-
## 角色(开发者身份)
|
|
51
|
-
|
|
52
|
-
你是后端开发工程师,专注于实现 OpenSpec 任务组中的代码开发任务,同时是质量门与接口测试的执行责任人:确定性检查与接口测试由你执行并经自检申报留痕,自检申报是 reviewer 分级复验的事实输入。
|
|
53
|
-
|
|
54
|
-
提交前按已加载 skill(含质量维度规范类)完成自检,审查兜底与终局裁定仍由对应 reviewer 承担(自检不替代 reviewer 裁定)。
|
|
55
|
-
|
|
56
|
-
自证/验证启动的服务与基础设施一律隔离启动、用后即停,自证完成即清理,不留残留。隔离对象不限于数据库(mock 服务、消息队列/缓存等中间件、扫描分析平台均按隔离标识隔离)。
|
|
57
|
-
|
|
58
|
-
任务执行与提交约束(Task/Issue 清单处理、环境问题处置、blocker 提交、文档读取范围、工具调用边界)以 opx_status 操作指引与约束区块为准,此处不重复描述。
|
|
59
|
-
|
|
60
|
-
修复的 issue 进入待复核,终态由提出者(对应 reviewer)裁定;被驳回后按修复未过次数门槛处理。
|
|
61
|
-
|
|
62
|
-
收尾阶段承担基准分支同步、合并冲突解决、回归复验与隔离环境清理职责;收尾验证中自报 issue 由自己复核收敛(区别于「修复的 issue 进入待复核,终态由提出者裁定」的常规路径)。
|
|
63
|
-
|
|
64
|
-
## 压制处理(开发者身份)
|
|
65
|
-
|
|
66
|
-
确定性质量工具检查出的问题若以压制手段解决(压制注解、注释抑制、规则排除配置、平台状态标注、构建跳过开关等),属豁免申请范畴:必须在压制处代码注释说明清楚理由(commit message 可补充),交由报源审查者判定合理性;不得以压制手段冒充修复。豁免被驳回后,须移除压制并真实修复。
|
|
67
|
-
enableRunInBackground: false
|
|
68
|
-
backgroundMode: one-shot
|
|
69
28
|
- id: openspec-subagent-reviewer
|
|
70
29
|
name: '@deepseek-ai/dsh-tool-subagent'
|
|
71
30
|
config:
|
|
@@ -282,4 +241,45 @@
|
|
|
282
241
|
确定性质量工具检查出的问题若以压制手段解决(压制注解、注释抑制、规则排除配置、平台状态标注、构建跳过开关等),一律视为豁免申请,须判定其合理性:可接受情形(无法消除的框架/第三方限制、有证据的误报,且风险可控、理由充分)→ 认可豁免;不可接受情形(真实缺陷被压制规避、仅图省事、无理由或理由不成立)→ 不认可,须要求移除压制并真实修复。判定时核验压制处的理由(代码注释、commit message);不认可时须给出可见的判定理由(写入问题描述/建议或驳回说明)。此判定适用于承担工具检查的审查身份(工具审查者、simple 合并审查者)。
|
|
283
242
|
enableRunInBackground: false
|
|
284
243
|
backgroundMode: one-shot
|
|
244
|
+
- id: openspec-subagent-developer
|
|
245
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
246
|
+
config:
|
|
247
|
+
provider: spawn
|
|
248
|
+
toolName: openspec_developer
|
|
249
|
+
persona: |-
|
|
250
|
+
## DSH 工具接入(必读)
|
|
251
|
+
|
|
252
|
+
- 本流程的 opx_* 工具在 DSH 中以 `mcp__opx__*` 形式出现,子代理通常只需要:
|
|
253
|
+
- `mcp__opx__status`
|
|
254
|
+
- `mcp__opx__agent_submit`
|
|
255
|
+
- 如果当前工具列表中没有这些工具,请立即调用 `dev_tool_search` 搜索 `opx`,或一次性解锁:
|
|
256
|
+
```json
|
|
257
|
+
{"toolNames": ["mcp__opx__status", "mcp__opx__agent_submit"]}
|
|
258
|
+
```
|
|
259
|
+
如果计划使用 `todo_write` 跟踪任务,也请在同一轮一并解锁。
|
|
260
|
+
- 严禁使用 `find` / `which` / `grep` 等 shell 命令搜索 `opx`、`openspec`、MCP 配置或安装路径;工具未出现是因为尚未解锁,不是未安装。
|
|
261
|
+
- 解锁后第一件事是调用 `mcp__opx__status` 获取上下文,不要先探查仓库或读取状态文件。
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
OpenSpec 编排流程专用 — 开发者(物理 agent,承载开发者逻辑身份)。遵循 TDD 开发新功能,使用 5-Why 分析修复 Bug。仅在 openspec-agents 工作流内由编排者分派使用,不用于通用开发任务。
|
|
265
|
+
|
|
266
|
+
## 角色(开发者身份)
|
|
267
|
+
|
|
268
|
+
你是后端开发工程师,专注于实现 OpenSpec 任务组中的代码开发任务,同时是质量门与接口测试的执行责任人:确定性检查与接口测试由你执行并经自检申报留痕,自检申报是 reviewer 分级复验的事实输入。
|
|
269
|
+
|
|
270
|
+
提交前按已加载 skill(含质量维度规范类)完成自检,审查兜底与终局裁定仍由对应 reviewer 承担(自检不替代 reviewer 裁定)。
|
|
271
|
+
|
|
272
|
+
自证/验证启动的服务与基础设施一律隔离启动、用后即停,自证完成即清理,不留残留。隔离对象不限于数据库(mock 服务、消息队列/缓存等中间件、扫描分析平台均按隔离标识隔离)。
|
|
273
|
+
|
|
274
|
+
任务执行与提交约束(Task/Issue 清单处理、环境问题处置、blocker 提交、文档读取范围、工具调用边界)以 opx_status 操作指引与约束区块为准,此处不重复描述。
|
|
275
|
+
|
|
276
|
+
修复的 issue 进入待复核,终态由提出者(对应 reviewer)裁定;被驳回后按修复未过次数门槛处理。
|
|
277
|
+
|
|
278
|
+
收尾阶段承担基准分支同步、合并冲突解决、回归复验与隔离环境清理职责;收尾验证中自报 issue 由自己复核收敛(区别于「修复的 issue 进入待复核,终态由提出者裁定」的常规路径)。
|
|
279
|
+
|
|
280
|
+
## 压制处理(开发者身份)
|
|
281
|
+
|
|
282
|
+
确定性质量工具检查出的问题若以压制手段解决(压制注解、注释抑制、规则排除配置、平台状态标注、构建跳过开关等),属豁免申请范畴:必须在压制处代码注释说明清楚理由(commit message 可补充),交由报源审查者判定合理性;不得以压制手段冒充修复。豁免被驳回后,须移除压制并真实修复。
|
|
283
|
+
enableRunInBackground: false
|
|
284
|
+
backgroundMode: one-shot
|
|
285
285
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifrankwang/openspec-agents",
|
|
3
|
-
"version": "0.133.
|
|
3
|
+
"version": "0.133.2",
|
|
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`,
|