@ifrankwang/openspec-agents 0.136.0 → 0.137.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 +277 -79
- package/README.md +3 -2
- package/assets/workflows/task-simple.yaml +23 -3
- package/assets/workflows/task.yaml +1 -1
- package/package.json +1 -1
- package/src/adapters/mcp-common/server.ts +2 -2
- package/src/core/git.ts +19 -4
- package/src/core/tools/lifecycle.ts +355 -80
- package/src/core/tools/schemas.ts +4 -4
- package/src/core/types.ts +2 -1
- package/src/core/views.ts +1 -1
- package/src/core/workflow/engine.ts +8 -0
- package/src/core/workflow/status.ts +17 -9
package/.mcp-server/cli.mjs
CHANGED
|
@@ -21663,6 +21663,14 @@ async function getCurrentBranch(worktree) {
|
|
|
21663
21663
|
async function getMergeBase(worktree, baseBranch) {
|
|
21664
21664
|
return runGit(worktree, ["merge-base", "HEAD", baseBranch]);
|
|
21665
21665
|
}
|
|
21666
|
+
async function isAncestor(worktree, a, b) {
|
|
21667
|
+
const r = await runGitChecked(worktree, ["merge-base", "--is-ancestor", a, b]);
|
|
21668
|
+
if (r.success)
|
|
21669
|
+
return true;
|
|
21670
|
+
if (r.exitCode === 1)
|
|
21671
|
+
return false;
|
|
21672
|
+
throw new Error(`无法判断 "${a}" 是否为 "${b}" 的祖先:${r.stderr}`);
|
|
21673
|
+
}
|
|
21666
21674
|
async function isLocalBranch(worktree, branch) {
|
|
21667
21675
|
const r = await runGitChecked(worktree, ["rev-parse", "--verify", `refs/heads/${branch}`]);
|
|
21668
21676
|
return r.success;
|
|
@@ -21799,7 +21807,7 @@ async function mergeBranchToTarget(repoDir, sourceBranch, targetBranch) {
|
|
|
21799
21807
|
success: false,
|
|
21800
21808
|
conflict: false,
|
|
21801
21809
|
blockedMessage: [
|
|
21802
|
-
`- **原因**: 目标分支 \`${targetBranch}\`
|
|
21810
|
+
`- **原因**: 目标分支 \`${targetBranch}\` 正被主仓库检出,且以下本地改动文件与变更分支合并将写入的文件重合;为避免覆盖本地改动,本次未执行任何合并动作(分支引用未动、无半成品):`,
|
|
21803
21811
|
...overlap.map((f) => ` - \`${f}\``),
|
|
21804
21812
|
`- **处理**: 请先 commit/stash/移除上述文件的本地改动后重试;或自行执行 \`git merge ${sourceBranch}\`(真实合并自带脏工作区保护)。`
|
|
21805
21813
|
].join(`
|
|
@@ -21883,6 +21891,7 @@ async function mergeBranchToTarget(repoDir, sourceBranch, targetBranch) {
|
|
|
21883
21891
|
}
|
|
21884
21892
|
return { success: true, conflict: false };
|
|
21885
21893
|
}
|
|
21894
|
+
var RESTORABLE_WORKTREE_BRANCH_PREFIXES = ["change/", "review/", "task-group/"];
|
|
21886
21895
|
async function discoverDiskWorktrees(worktree) {
|
|
21887
21896
|
const result = [];
|
|
21888
21897
|
const wtList = await runGit(worktree, ["worktree", "list"]);
|
|
@@ -21891,7 +21900,7 @@ async function discoverDiskWorktrees(worktree) {
|
|
|
21891
21900
|
const m = line.match(/^(\S+)\s+[0-9a-f]+\s+\[(.+?)\]/);
|
|
21892
21901
|
if (m) {
|
|
21893
21902
|
const branch = m[2].trim();
|
|
21894
|
-
if (
|
|
21903
|
+
if (RESTORABLE_WORKTREE_BRANCH_PREFIXES.some((p) => branch.startsWith(p))) {
|
|
21895
21904
|
result.push({ branch, path: m[1].trim() });
|
|
21896
21905
|
}
|
|
21897
21906
|
}
|
|
@@ -22195,6 +22204,9 @@ function isTaskGroupSettled(item) {
|
|
|
22195
22204
|
const childrenSettled = item.children.filter((c) => c.type === "task").every((c) => c.phase === "todo" || isTerminalPhase(c.phase));
|
|
22196
22205
|
return noTags && childrenSettled;
|
|
22197
22206
|
}
|
|
22207
|
+
function isFinalTaskGroup(state, item) {
|
|
22208
|
+
return state.workItems.filter((w) => w.id.startsWith("task:") && w.id !== item.id).every(isTaskGroupSettled);
|
|
22209
|
+
}
|
|
22198
22210
|
function isBlockingSeverity(severity) {
|
|
22199
22211
|
return severity !== undefined && BLOCKING_SEVERITIES.includes(severity);
|
|
22200
22212
|
}
|
|
@@ -27032,7 +27044,7 @@ function renderWorktreeSection(state, tg, opts) {
|
|
|
27032
27044
|
lines.push("- **审查范围**: 全量代码库审查(无区间界定,工具检查与审查覆盖整个代码库)");
|
|
27033
27045
|
}
|
|
27034
27046
|
} else if (tg.baseRef) {
|
|
27035
|
-
lines.push(`- **变更范围**: 用 \`git -C ${tg.worktreePath} diff --name-only ${tg.baseRef}..HEAD\`
|
|
27047
|
+
lines.push(`- **变更范围**: 用 \`git -C ${tg.worktreePath} diff --name-only ${tg.baseRef}..HEAD\` 查询本任务组的已提交变更文件`);
|
|
27036
27048
|
}
|
|
27037
27049
|
lines.push("- **⚠️ 约束**: 所有读写和 git 操作均在此目录下进行;严禁直接修改主仓库/主分支路径下的文件(如 `<repo>/openspec/...`)");
|
|
27038
27050
|
lines.push("- **路径解析**: 推荐阅读文档均为相对 worktree 路径的引用,一律以 worktree 路径为基准解析,禁止从主仓库根目录解析");
|
|
@@ -27348,13 +27360,14 @@ function renderTerminalPhase(item, state, caller) {
|
|
|
27348
27360
|
return lines.join(`
|
|
27349
27361
|
`);
|
|
27350
27362
|
}
|
|
27363
|
+
const finalGroup = state ? isFinalTaskGroup(state, item) : true;
|
|
27351
27364
|
if (item.metadata["completed_at"] !== undefined) {
|
|
27352
27365
|
return [
|
|
27353
27366
|
"# ✅ 任务组已完成",
|
|
27354
27367
|
"",
|
|
27355
27368
|
`- **完成时间**: ${item.metadata["completed_at"]}`,
|
|
27356
27369
|
"",
|
|
27357
|
-
"
|
|
27370
|
+
finalGroup ? "本任务组为最后一个收口任务组,change 收口已完成:变更分支已合并回基准分支,编排已完成并收尾。" : "本任务组已完成,变更保留在 change 分支上,待全部任务组完成后统一收口合并。",
|
|
27358
27371
|
"",
|
|
27359
27372
|
...renderDeploymentNotesSection(state, caller)
|
|
27360
27373
|
].join(`
|
|
@@ -27363,7 +27376,8 @@ function renderTerminalPhase(item, state, caller) {
|
|
|
27363
27376
|
return [
|
|
27364
27377
|
"# \uD83C\uDFC1 任务组已完成,待收尾",
|
|
27365
27378
|
"",
|
|
27366
|
-
"
|
|
27379
|
+
"全部审核层已通过。",
|
|
27380
|
+
finalGroup ? "调用 `opx_orch_complete_task_group` 完成 change 收口(把变更分支合并回基准分支并清理)。" : "调用 `opx_orch_complete_task_group` 完成任务组收尾(变更保留在 change 分支,统一收口时合并)。",
|
|
27367
27381
|
"",
|
|
27368
27382
|
...renderDeploymentNotesSection(state, caller)
|
|
27369
27383
|
].join(`
|
|
@@ -27380,7 +27394,7 @@ function renderDeploymentNotesSection(state, caller) {
|
|
|
27380
27394
|
"",
|
|
27381
27395
|
"向用户汇报本次变更的部署注意事项:",
|
|
27382
27396
|
"",
|
|
27383
|
-
`- 阅读主仓库 \`openspec/changes/${state.changeId}/\` 下规划文档(design.md、tasks.md、proposal.md、specs
|
|
27397
|
+
`- 阅读主仓库 \`openspec/changes/${state.changeId}/\` 下规划文档(design.md、tasks.md、proposal.md、specs/)提取部署要点:数据/结构变更脚本执行、消息队列/缓存等中间件配置调整、新增配置项或功能开关、外部接口契约变化、依赖清单/构建配置变更(合并后需在主仓库重装依赖、重新构建)`,
|
|
27384
27398
|
"- 无规划文档时跳过文档提取,仅转述下方实施补充点",
|
|
27385
27399
|
""
|
|
27386
27400
|
];
|
|
@@ -27764,7 +27778,7 @@ function renderAgentWorking(item, rec, step, common, ctxAgent, state, tg, toolCh
|
|
|
27764
27778
|
lines.push(...eff.lines);
|
|
27765
27779
|
n = eff.nextNum;
|
|
27766
27780
|
if (tg.worktreePath && tg.baseRef) {
|
|
27767
|
-
lines.push(`${n++}.
|
|
27781
|
+
lines.push(`${n++}. 用上方「变更范围」命令获取本任务组的已提交变更文件清单,作为本次实施/修改范围;审查/报告范围以当前 step 指令为准`);
|
|
27768
27782
|
}
|
|
27769
27783
|
lines.push(`${n++}. 执行当前 step(\`${rec.stepId}\`)职责范围内的全部工作——遵循所有已加载 skill 的全部规范与约束`);
|
|
27770
27784
|
lines.push(`${n++}. 逐项检视所有已加载 skill 的 MUST 规范,确认全部满足(不满足则补做,不得跳过)`);
|
|
@@ -27977,7 +27991,7 @@ function renderToolChangesEvidence(item, tg, toolChanges, kind) {
|
|
|
27977
27991
|
`)];
|
|
27978
27992
|
}
|
|
27979
27993
|
if (checkpoint) {
|
|
27980
|
-
lines.push(`- **口径**: 本次为「${label.title}(${checkpoint}..HEAD)」的增量区间(含已提交与未提交的非 openspec 变更);` + `与上方「变更范围」(${baseRef ?? "(无)"}..HEAD
|
|
27994
|
+
lines.push(`- **口径**: 本次为「${label.title}(${checkpoint}..HEAD)」的增量区间(含已提交与未提交的非 openspec 变更);` + `与上方「变更范围」(${baseRef ?? "(无)"}..HEAD 本任务组累计口径)不同`, "");
|
|
27981
27995
|
} else if (baseRef) {
|
|
27982
27996
|
lines.push(`- **口径**: 本次区间以基线(${baseRef}..HEAD)兜底,与上方「变更范围」一致(首次进入,无${label.lastCheck}记录;` + "含已提交与未提交的非 openspec 变更)", "");
|
|
27983
27997
|
} else {
|
|
@@ -28313,7 +28327,16 @@ function applyRecoveryState(item, recovery, parsedTasks, state) {
|
|
|
28313
28327
|
if ((recovery?.reset_steps ?? []).includes("quality_review")) {
|
|
28314
28328
|
clearStepTags(item, "quality_review");
|
|
28315
28329
|
}
|
|
28316
|
-
|
|
28330
|
+
const workflow = loadWorkflowFile(resolveWorkflowPath(state));
|
|
28331
|
+
item.currentStep = firstUnpassedReviewStep(item, workflow);
|
|
28332
|
+
if (item.currentStep === null) {
|
|
28333
|
+
const unfinishedTasks = item.children.filter((child) => child.type === "task" && !isTerminalPhase(child.phase));
|
|
28334
|
+
if (unfinishedTasks.length === 0) {
|
|
28335
|
+
item.phase = "done";
|
|
28336
|
+
} else {
|
|
28337
|
+
item.currentStep = "quality_review";
|
|
28338
|
+
}
|
|
28339
|
+
}
|
|
28317
28340
|
syncTaskChildren(item, parsedTasks, { defaultStatus: "done" });
|
|
28318
28341
|
return;
|
|
28319
28342
|
}
|
|
@@ -28798,17 +28821,75 @@ async function bindReviewWorktreeRefs(item, worktreePath, branch, state) {
|
|
|
28798
28821
|
}
|
|
28799
28822
|
delete item.metadata["base_ref"];
|
|
28800
28823
|
}
|
|
28801
|
-
async function bindWorktreeRefs(item, worktreePath, branch
|
|
28824
|
+
async function bindWorktreeRefs(item, worktreePath, branch) {
|
|
28802
28825
|
item.metadata["worktree_path"] = worktreePath;
|
|
28803
28826
|
item.metadata["branch_name"] = branch;
|
|
28804
|
-
const
|
|
28805
|
-
if (!
|
|
28806
|
-
|
|
28807
|
-
|
|
28808
|
-
|
|
28809
|
-
|
|
28827
|
+
const existing = item.metadata["scope_start_oid"];
|
|
28828
|
+
if (typeof existing !== "string" || !await isAncestor(worktreePath, existing, branch)) {
|
|
28829
|
+
const head = (await runGit(worktreePath, ["rev-parse", branch])).trim();
|
|
28830
|
+
if (!head)
|
|
28831
|
+
throw new Error(`worktree 绑定成功但无法获取 ${branch} 的 HEAD:${worktreePath}`);
|
|
28832
|
+
item.metadata["scope_start_oid"] = head;
|
|
28810
28833
|
}
|
|
28811
|
-
item.metadata["base_ref"] =
|
|
28834
|
+
item.metadata["base_ref"] = item.metadata["scope_start_oid"];
|
|
28835
|
+
}
|
|
28836
|
+
function deriveChangeBranch(changeId) {
|
|
28837
|
+
return `change/${normalizeSessionToken(changeId)}`;
|
|
28838
|
+
}
|
|
28839
|
+
async function ensureChangeBranch(repoRoot, branch, baseBranch) {
|
|
28840
|
+
if (await isLocalBranch(repoRoot, branch))
|
|
28841
|
+
return false;
|
|
28842
|
+
const created = await runGitChecked(repoRoot, ["branch", branch, baseBranch]);
|
|
28843
|
+
if (!created.success)
|
|
28844
|
+
throw new Error(`创建分支 "${branch}"(fork 自 ${baseBranch})失败:${created.stderr}`);
|
|
28845
|
+
return true;
|
|
28846
|
+
}
|
|
28847
|
+
async function ensureChangeWorktree(repoRoot, state, item, branch, defaultPath) {
|
|
28848
|
+
await ensureChangeBranch(repoRoot, branch, state.baseBranch);
|
|
28849
|
+
const wtList = await runGit(repoRoot, ["worktree", "list"]);
|
|
28850
|
+
const existingLine = wtList.split(`
|
|
28851
|
+
`).find((l) => {
|
|
28852
|
+
const m = l.match(/^(\S+)\s+[0-9a-f]+\s+\[(.+?)\]/);
|
|
28853
|
+
return m && m[2].trim() === branch;
|
|
28854
|
+
});
|
|
28855
|
+
const registeredPath = existingLine ? existingLine.match(/^(\S+)/)?.[1] : undefined;
|
|
28856
|
+
const targetPath = registeredPath ?? defaultPath;
|
|
28857
|
+
if (!await pathExists(targetPath)) {
|
|
28858
|
+
await runGitChecked(repoRoot, ["worktree", "prune"]);
|
|
28859
|
+
await runGit(repoRoot, ["worktree", "add", targetPath, branch]);
|
|
28860
|
+
await bindWorktreeRefs(item, targetPath, branch);
|
|
28861
|
+
return { path: targetPath, reused: false };
|
|
28862
|
+
}
|
|
28863
|
+
if (!registeredPath) {
|
|
28864
|
+
const addRes = await runGitChecked(repoRoot, ["worktree", "add", targetPath, branch]);
|
|
28865
|
+
if (!addRes.success) {
|
|
28866
|
+
throw new Error(`无法在 "${targetPath}" 就位 worktree:目录已存在且不是检出分支 "${branch}" 的 git worktree(git: ${addRes.stderr})。
|
|
28867
|
+
` + `请人工确认该目录内容并处理(删除或迁移)后重试。`);
|
|
28868
|
+
}
|
|
28869
|
+
await bindWorktreeRefs(item, targetPath, branch);
|
|
28870
|
+
return { path: targetPath, reused: false };
|
|
28871
|
+
}
|
|
28872
|
+
const statusOut = await runGit(targetPath, ["status", "--porcelain"]);
|
|
28873
|
+
const dirtyPaths = statusOut.split(`
|
|
28874
|
+
`).map((l) => l.trim()).filter(Boolean).map((l) => {
|
|
28875
|
+
const f = l.replace(/^\S+\s+/, "");
|
|
28876
|
+
return (f.includes(" -> ") ? f.split(" -> ").pop() : f).replace(/^"+|"+$/g, "");
|
|
28877
|
+
}).filter(Boolean);
|
|
28878
|
+
const codeDirty = dirtyPaths.filter((p) => !p.startsWith("openspec/"));
|
|
28879
|
+
if (codeDirty.length > 0) {
|
|
28880
|
+
throw new Error(`已有 worktree "${targetPath}" 存在未提交的代码文件变更(${codeDirty.join("、")}),无法自动处理。
|
|
28881
|
+
` + `请先在该 worktree 内 commit 或 stash 后重试。`);
|
|
28882
|
+
}
|
|
28883
|
+
if (dirtyPaths.length > 0) {
|
|
28884
|
+
const addResult = await runGitChecked(targetPath, ["add", "--", ...dirtyPaths]);
|
|
28885
|
+
if (!addResult.success)
|
|
28886
|
+
throw new Error(`worktree openspec 目录 git add 失败:${addResult.stderr}`);
|
|
28887
|
+
const commitResult = await runGitChecked(targetPath, ["commit", "-m", "docs(openspec): auto-commit before worktree reuse"]);
|
|
28888
|
+
if (!commitResult.success)
|
|
28889
|
+
throw new Error(`worktree openspec 目录 git commit 失败:${commitResult.stderr}`);
|
|
28890
|
+
}
|
|
28891
|
+
await bindWorktreeRefs(item, targetPath, branch);
|
|
28892
|
+
return { path: targetPath, reused: true };
|
|
28812
28893
|
}
|
|
28813
28894
|
async function setWorktreeExecute(params, ctx) {
|
|
28814
28895
|
assertOrchestrator(ctx, "opx_orch_set_worktree");
|
|
@@ -28819,6 +28900,7 @@ async function setWorktreeExecute(params, ctx) {
|
|
|
28819
28900
|
if (!item)
|
|
28820
28901
|
throw new Error(`工作项 "task:${state.taskGroupId}" 缺失,请重新调用 opx_orch_init。`);
|
|
28821
28902
|
const repoRoot = ctx.worktree;
|
|
28903
|
+
const isReview = state.kind === "review";
|
|
28822
28904
|
const rawBranch = params.branch_name ?? "";
|
|
28823
28905
|
if (rawBranch !== "") {
|
|
28824
28906
|
const check = await runGitChecked(repoRoot, ["check-ref-format", "--branch", rawBranch]);
|
|
@@ -28826,13 +28908,18 @@ async function setWorktreeExecute(params, ctx) {
|
|
|
28826
28908
|
throw new Error(`分支名 "${rawBranch}" 不合法,请修正后重试。`);
|
|
28827
28909
|
}
|
|
28828
28910
|
}
|
|
28829
|
-
const
|
|
28830
|
-
|
|
28911
|
+
const branch = rawBranch || (isReview ? `review/${state.changeId}` : deriveChangeBranch(state.changeId));
|
|
28912
|
+
if (!isReview) {
|
|
28913
|
+
const check = await runGitChecked(repoRoot, ["check-ref-format", "--branch", branch]);
|
|
28914
|
+
if (!check.success) {
|
|
28915
|
+
throw new Error(`派生分支名 "${branch}" 不合法(change_id="${state.changeId}"),请修正 change_id 或显式传入 branch_name。`);
|
|
28916
|
+
}
|
|
28917
|
+
}
|
|
28831
28918
|
let wtPath;
|
|
28832
28919
|
if (params.worktree_path) {
|
|
28833
28920
|
wtPath = assertPathWithin(repoRoot, params.worktree_path, "worktree_path");
|
|
28834
28921
|
} else {
|
|
28835
|
-
wtPath = isReview ? path6.join(repoRoot, ".worktree", state.changeId, "review") : path6.join(repoRoot, ".worktree", state.changeId,
|
|
28922
|
+
wtPath = isReview ? path6.join(repoRoot, ".worktree", state.changeId, "review") : path6.join(repoRoot, ".worktree", state.changeId, "ws");
|
|
28836
28923
|
}
|
|
28837
28924
|
const changeStatus = await runGit(repoRoot, ["status", "--porcelain", `openspec/changes/${state.changeId}/`]);
|
|
28838
28925
|
if (changeStatus.trim().length > 0) {
|
|
@@ -28843,61 +28930,57 @@ async function setWorktreeExecute(params, ctx) {
|
|
|
28843
28930
|
if (!commitResult.success)
|
|
28844
28931
|
throw new Error(`change 目录 git commit 失败:${commitResult.stderr}`);
|
|
28845
28932
|
}
|
|
28846
|
-
const wtList = await runGit(repoRoot, ["worktree", "list"]);
|
|
28847
|
-
const existingLine = wtList.split(`
|
|
28848
|
-
`).find((l) => {
|
|
28849
|
-
const m = l.match(/^(\S+)\s+[0-9a-f]+\s+\[(.+?)\]/);
|
|
28850
|
-
return m && m[2].trim() === branch;
|
|
28851
|
-
});
|
|
28852
|
-
const existingPath = existingLine ? existingLine.match(/^(\S+)/)?.[1] : undefined;
|
|
28853
28933
|
let reused = false;
|
|
28854
|
-
if (
|
|
28855
|
-
const
|
|
28856
|
-
const
|
|
28857
|
-
|
|
28858
|
-
|
|
28934
|
+
if (isReview) {
|
|
28935
|
+
const wtList = await runGit(repoRoot, ["worktree", "list"]);
|
|
28936
|
+
const existingLine = wtList.split(`
|
|
28937
|
+
`).find((l) => {
|
|
28938
|
+
const m = l.match(/^(\S+)\s+[0-9a-f]+\s+\[(.+?)\]/);
|
|
28939
|
+
return m && m[2].trim() === branch;
|
|
28940
|
+
});
|
|
28941
|
+
const existingPath = existingLine ? existingLine.match(/^(\S+)/)?.[1] : undefined;
|
|
28942
|
+
if (existingPath) {
|
|
28943
|
+
const baseHead = await runGit(repoRoot, ["rev-parse", state.baseBranch]);
|
|
28944
|
+
const mergeResult = await runGitChecked(existingPath, ["merge", "--ff-only", baseHead]);
|
|
28945
|
+
if (mergeResult.success) {
|
|
28859
28946
|
await bindReviewWorktreeRefs(item, existingPath, branch, state);
|
|
28860
|
-
else
|
|
28861
|
-
await bindWorktreeRefs(item, existingPath, branch, state.baseBranch);
|
|
28862
|
-
reused = true;
|
|
28863
|
-
} else {
|
|
28864
|
-
const clean = await isWorktreeClean(existingPath);
|
|
28865
|
-
if (!clean) {
|
|
28866
|
-
throw new Error(`已有 worktree "${existingPath}" 与 ${state.baseBranch} 分叉且有未提交变更,无法自动 fast-forward。
|
|
28867
|
-
` + `请手动处理后重试。`);
|
|
28868
|
-
}
|
|
28869
|
-
const localCommitCount = parseInt(await runGit(existingPath, ["rev-list", "--count", `${state.baseBranch}..HEAD`]), 10);
|
|
28870
|
-
if (localCommitCount > 0 || Number.isNaN(localCommitCount)) {
|
|
28871
|
-
if (isReview)
|
|
28872
|
-
await bindReviewWorktreeRefs(item, existingPath, branch, state);
|
|
28873
|
-
else
|
|
28874
|
-
await bindWorktreeRefs(item, existingPath, branch, state.baseBranch);
|
|
28875
28947
|
reused = true;
|
|
28876
28948
|
} else {
|
|
28877
|
-
const
|
|
28878
|
-
if (!
|
|
28879
|
-
|
|
28880
|
-
|
|
28881
|
-
|
|
28882
|
-
|
|
28883
|
-
|
|
28884
|
-
|
|
28949
|
+
const clean = await isWorktreeClean(existingPath);
|
|
28950
|
+
if (!clean) {
|
|
28951
|
+
throw new Error(`已有 worktree "${existingPath}" 与 ${state.baseBranch} 分叉且有未提交变更,无法自动 fast-forward。
|
|
28952
|
+
` + `请手动处理后重试。`);
|
|
28953
|
+
}
|
|
28954
|
+
const localCommitCount = parseInt(await runGit(existingPath, ["rev-list", "--count", `${state.baseBranch}..HEAD`]), 10);
|
|
28955
|
+
if (localCommitCount > 0 || Number.isNaN(localCommitCount)) {
|
|
28956
|
+
await bindReviewWorktreeRefs(item, existingPath, branch, state);
|
|
28957
|
+
reused = true;
|
|
28958
|
+
} else {
|
|
28959
|
+
const cleanup = await removeTaskGroupWorktree(repoRoot, existingPath, { branchName: branch });
|
|
28960
|
+
if (!cleanup.dirResolved || cleanup.branchDeleted === false) {
|
|
28961
|
+
const problems = [];
|
|
28962
|
+
if (!cleanup.dirResolved)
|
|
28963
|
+
problems.push(`无法清理已有 worktree "${existingPath}"`);
|
|
28964
|
+
if (cleanup.branchDeleted === false)
|
|
28965
|
+
problems.push(`无法清理已有分支 "${branch}"`);
|
|
28966
|
+
throw new Error(`${problems.join(";")}:${cleanup.errors.join(";") || "原因未知"}
|
|
28885
28967
|
请手动处理后重试。`);
|
|
28968
|
+
}
|
|
28886
28969
|
}
|
|
28887
28970
|
}
|
|
28888
28971
|
}
|
|
28889
|
-
|
|
28890
|
-
|
|
28891
|
-
|
|
28892
|
-
await runGit(repoRoot, ["worktree", "add", "-b", branch, wtPath, forkBranch]);
|
|
28893
|
-
if (isReview) {
|
|
28972
|
+
if (!reused) {
|
|
28973
|
+
const forkBranch = state.baseBranch;
|
|
28974
|
+
await runGit(repoRoot, ["worktree", "add", "-b", branch, wtPath, forkBranch]);
|
|
28894
28975
|
await bindReviewWorktreeRefs(item, wtPath, branch, state);
|
|
28895
28976
|
if (state.reviewScope?.scopeType === "pr" && !item.metadata["base_ref"]) {
|
|
28896
28977
|
throw new Error(`worktree 创建成功但无法获取 ${state.reviewScope.baseRef} 与 ${state.baseBranch} 的 merge-base:${wtPath}`);
|
|
28897
28978
|
}
|
|
28898
|
-
} else {
|
|
28899
|
-
await bindWorktreeRefs(item, wtPath, branch, forkBranch, { requireBaseRef: true });
|
|
28900
28979
|
}
|
|
28980
|
+
} else {
|
|
28981
|
+
const ensured = await ensureChangeWorktree(repoRoot, state, item, branch, wtPath);
|
|
28982
|
+
wtPath = ensured.path;
|
|
28983
|
+
reused = ensured.reused;
|
|
28901
28984
|
}
|
|
28902
28985
|
await writeState(ctx.worktree, state);
|
|
28903
28986
|
const storedPath = typeof item.metadata["worktree_path"] === "string" ? item.metadata["worktree_path"] : null;
|
|
@@ -28998,8 +29081,43 @@ function renderReviewIssueSummary(item) {
|
|
|
28998
29081
|
return lines.join(`
|
|
28999
29082
|
`);
|
|
29000
29083
|
}
|
|
29084
|
+
function rollbackToCleanupStep(item, workflow) {
|
|
29085
|
+
if (!workflow.stepMap.has("verify_cleanup"))
|
|
29086
|
+
return false;
|
|
29087
|
+
delete item.metadata["_advance_block_reason"];
|
|
29088
|
+
delete item.metadata["_checkpoint"];
|
|
29089
|
+
resetInternalRetryCount(item);
|
|
29090
|
+
clearStepTags(item, "verify_cleanup");
|
|
29091
|
+
delete item.metadata["scope_end_oid"];
|
|
29092
|
+
item.phase = "review";
|
|
29093
|
+
item.currentStep = "verify_cleanup";
|
|
29094
|
+
return true;
|
|
29095
|
+
}
|
|
29096
|
+
function renderFinalizeBlocked(reasons, handlings, opts) {
|
|
29097
|
+
const lines = [
|
|
29098
|
+
`- **status**: blocked`,
|
|
29099
|
+
...reasons.map((r) => `- **原因**: ${r}`),
|
|
29100
|
+
`- **说明**: worktree 与变更分支 \`${opts.branchName}\` 已保留;未执行合并,无半成品状态。属多 change 并行推进的预期行为。` + (opts.rolledBack ? "本任务组状态已回退到收尾验证(verify_cleanup)。" : ""),
|
|
29101
|
+
...handlings.map((h) => `- **处理**: ${h}`),
|
|
29102
|
+
"",
|
|
29103
|
+
"重新查询 opx_status 获取分派指引。"
|
|
29104
|
+
];
|
|
29105
|
+
if (opts.checkboxWarning)
|
|
29106
|
+
lines.push(opts.checkboxWarning);
|
|
29107
|
+
return lines.join(`
|
|
29108
|
+
`);
|
|
29109
|
+
}
|
|
29001
29110
|
async function completeTaskGroupExecute(params, ctx) {
|
|
29002
29111
|
assertOrchestrator(ctx, "opx_orch_complete_task_group");
|
|
29112
|
+
const lockPath = await getLockPath(ctx.worktree, params.change_id);
|
|
29113
|
+
await acquireLock(lockPath);
|
|
29114
|
+
try {
|
|
29115
|
+
return await completeTaskGroupLocked(params, ctx);
|
|
29116
|
+
} finally {
|
|
29117
|
+
releaseLock(lockPath);
|
|
29118
|
+
}
|
|
29119
|
+
}
|
|
29120
|
+
async function completeTaskGroupLocked(params, ctx) {
|
|
29003
29121
|
const state = await readStateByWorktree(ctx.worktree, params.change_id);
|
|
29004
29122
|
if (!state)
|
|
29005
29123
|
throw new Error("编排会话未初始化。请先调用 opx_orch_init。");
|
|
@@ -29018,7 +29136,8 @@ async function completeTaskGroupExecute(params, ctx) {
|
|
|
29018
29136
|
if (!clean)
|
|
29019
29137
|
throw new Error(`worktree "${worktreePath}" 存在未 commit 内容,请先 commit 再完成任务组。`);
|
|
29020
29138
|
}
|
|
29021
|
-
const
|
|
29139
|
+
const isReview = state.kind === "review";
|
|
29140
|
+
const isReviewNone = isReview && state.reviewScope?.fix === "none";
|
|
29022
29141
|
const openIssues = item.children.filter((c) => isBlockingSeverity(c.severity) && !isTerminalPhase(c.phase));
|
|
29023
29142
|
if (!isReviewNone && openIssues.length > 0) {
|
|
29024
29143
|
throw new Error(`存在 ${openIssues.length} 个 Low 及以上的未解决 issue 未处理,请先修复或申请豁免。`);
|
|
@@ -29040,6 +29159,91 @@ async function completeTaskGroupExecute(params, ctx) {
|
|
|
29040
29159
|
checkboxWarning = `- **tasks.md 复选框勾选失败**: ${e instanceof Error ? e.message : String(e)}(不影响收尾)`;
|
|
29041
29160
|
}
|
|
29042
29161
|
}
|
|
29162
|
+
if (!isReview && worktreePath && branchName) {
|
|
29163
|
+
const endOid = (await runGit(worktreePath, ["rev-parse", branchName])).trim();
|
|
29164
|
+
if (endOid)
|
|
29165
|
+
item.metadata["scope_end_oid"] = endOid;
|
|
29166
|
+
}
|
|
29167
|
+
if (isReview) {
|
|
29168
|
+
return finalizeReviewSession(state, item, params, ctx, { worktreePath, branchName, isReviewNone, checkboxWarning });
|
|
29169
|
+
}
|
|
29170
|
+
if (!isFinalTaskGroup(state, item)) {
|
|
29171
|
+
item.metadata["completed_at"] = new Date().toISOString();
|
|
29172
|
+
await writeState(ctx.worktree, state);
|
|
29173
|
+
const doneMessage = "任务组已完成。本任务组变更保留在 change 分支上,待全部任务组完成后统一收口合并(本次不合并、不销毁 worktree)。";
|
|
29174
|
+
return checkboxWarning ? `${doneMessage}
|
|
29175
|
+
${checkboxWarning}` : doneMessage;
|
|
29176
|
+
}
|
|
29177
|
+
const mergeTarget = state.baseBranch;
|
|
29178
|
+
if (branchName) {
|
|
29179
|
+
await ensureChangeBranch(ctx.worktree, branchName, mergeTarget);
|
|
29180
|
+
if (!await isAncestor(ctx.worktree, mergeTarget, branchName)) {
|
|
29181
|
+
const workflow = loadWorkflowFile(resolveWorkflowPath(state));
|
|
29182
|
+
const rolledBack = rollbackToCleanupStep(item, workflow);
|
|
29183
|
+
if (rolledBack)
|
|
29184
|
+
await writeState(ctx.worktree, state);
|
|
29185
|
+
return renderFinalizeBlocked([
|
|
29186
|
+
`基准分支 \`${mergeTarget}\` 已推进(与变更分支 \`${branchName}\` 存在漂移):基准分支最新提交未包含在变更分支历史中,直接合并会遗漏基准分支新内容。`
|
|
29187
|
+
], [
|
|
29188
|
+
`在 worktree 内执行 \`git merge ${mergeTarget}\` 合入基准分支最新代码并解决冲突,完成回归验证后重新提交收尾验证(opx_agent_submit,step_id="verify_cleanup"),通过后再调用 opx_orch_complete_task_group 重新收口。`
|
|
29189
|
+
], { branchName, mergeTarget, rolledBack, checkboxWarning });
|
|
29190
|
+
}
|
|
29191
|
+
const mergeResult = await mergeBranchToTarget(ctx.worktree, branchName, mergeTarget);
|
|
29192
|
+
if (mergeResult.blockedMessage) {
|
|
29193
|
+
return [`- **status**: blocked`, mergeResult.blockedMessage].join(`
|
|
29194
|
+
`);
|
|
29195
|
+
}
|
|
29196
|
+
if (!mergeResult.success) {
|
|
29197
|
+
const workflow = loadWorkflowFile(resolveWorkflowPath(state));
|
|
29198
|
+
const rolledBack = rollbackToCleanupStep(item, workflow);
|
|
29199
|
+
if (rolledBack)
|
|
29200
|
+
await writeState(ctx.worktree, state);
|
|
29201
|
+
return renderFinalizeBlocked([
|
|
29202
|
+
`变更分支 \`${branchName}\` 合并到 \`${mergeTarget}\` 时发生冲突,未产生任何变更(分支引用未动,无半成品合并)。`
|
|
29203
|
+
], [
|
|
29204
|
+
`在 worktree 内执行 \`git merge ${mergeTarget}\` 合入基准分支最新代码解决冲突并提交,完成回归验证后重新提交收尾验证(opx_agent_submit,step_id="verify_cleanup"),通过后再调用 opx_orch_complete_task_group 重新收口。`
|
|
29205
|
+
], { branchName, mergeTarget, rolledBack, checkboxWarning });
|
|
29206
|
+
}
|
|
29207
|
+
}
|
|
29208
|
+
let cleanupNote = "";
|
|
29209
|
+
let cleanupResidualWarning = "";
|
|
29210
|
+
if (worktreePath) {
|
|
29211
|
+
const cleanup = await removeTaskGroupWorktree(ctx.worktree, worktreePath, { branchName });
|
|
29212
|
+
if (cleanup.dirResolved) {
|
|
29213
|
+
await sweepEmptyWorktreeParents(ctx.worktree, worktreePath, params.change_id);
|
|
29214
|
+
const successNotes = [];
|
|
29215
|
+
if (cleanup.dirResolvedByFallback) {
|
|
29216
|
+
successNotes.push("- **cleanup**: git worktree remove 未直接移除目录,已按文件系统兜底删除并 prune 管理记录。");
|
|
29217
|
+
}
|
|
29218
|
+
if (cleanup.errors.length > 0) {
|
|
29219
|
+
successNotes.push(`- **cleanup 部分告警**: ${cleanup.errors.join(";")}`);
|
|
29220
|
+
}
|
|
29221
|
+
cleanupNote = successNotes.join(`
|
|
29222
|
+
`);
|
|
29223
|
+
} else {
|
|
29224
|
+
item.metadata["cleanup_residual"] = { worktree_path: worktreePath, errors: cleanup.errors };
|
|
29225
|
+
cleanupResidualWarning = [
|
|
29226
|
+
"",
|
|
29227
|
+
"## ⚠️ worktree 清理残留(不影响收口)",
|
|
29228
|
+
`- **残留路径**: \`${worktreePath}\``,
|
|
29229
|
+
"- **原因**:",
|
|
29230
|
+
...cleanup.errors.map((e) => ` - ${e}`),
|
|
29231
|
+
`- **处理**: 请人工执行 \`rm -rf '${worktreePath}' && git worktree prune\` 清理残留目录与 git 管理记录。`,
|
|
29232
|
+
branchName ? `- **分支**: 分支 "${branchName}" 未删除。` : ""
|
|
29233
|
+
].filter(Boolean).join(`
|
|
29234
|
+
`);
|
|
29235
|
+
}
|
|
29236
|
+
}
|
|
29237
|
+
item.metadata["completed_at"] = new Date().toISOString();
|
|
29238
|
+
await writeState(ctx.worktree, state);
|
|
29239
|
+
const notes = [cleanupNote, checkboxWarning, cleanupResidualWarning].filter(Boolean);
|
|
29240
|
+
const doneMessage = branchName ? `任务组已完成并合并到 "${mergeTarget}"。` : "任务组已完成(无变更分支引用,跳过合并)。";
|
|
29241
|
+
return notes.length > 0 ? `${doneMessage}
|
|
29242
|
+
${notes.join(`
|
|
29243
|
+
`)}` : doneMessage;
|
|
29244
|
+
}
|
|
29245
|
+
async function finalizeReviewSession(state, item, params, ctx, refs) {
|
|
29246
|
+
const { worktreePath, branchName, isReviewNone, checkboxWarning } = refs;
|
|
29043
29247
|
const mergeTarget = state.baseBranch;
|
|
29044
29248
|
if (branchName && !isReviewNone) {
|
|
29045
29249
|
const mergeResult = await mergeBranchToTarget(ctx.worktree, branchName, mergeTarget);
|
|
@@ -29089,19 +29293,13 @@ async function completeTaskGroupExecute(params, ctx) {
|
|
|
29089
29293
|
item.metadata["completed_at"] = new Date().toISOString();
|
|
29090
29294
|
await writeState(ctx.worktree, state);
|
|
29091
29295
|
const notes = [cleanupNote, checkboxWarning, cleanupResidualWarning].filter(Boolean);
|
|
29092
|
-
|
|
29093
|
-
|
|
29094
|
-
|
|
29095
|
-
return notes.length > 0 ? `${doneMessage}
|
|
29296
|
+
const summary = renderReviewIssueSummary(item);
|
|
29297
|
+
const doneMessage = isReviewNone ? `独立审查会话已完成(只审模式,未合并,worktree 与分支已销毁)。` : `独立审查会话已完成并合并到 "${mergeTarget}"。`;
|
|
29298
|
+
return notes.length > 0 ? `${doneMessage}
|
|
29096
29299
|
${notes.join(`
|
|
29097
29300
|
`)}
|
|
29098
29301
|
${summary}` : `${doneMessage}
|
|
29099
29302
|
${summary}`;
|
|
29100
|
-
}
|
|
29101
|
-
const doneMessage = `任务组已完成并合并到 "${mergeTarget}"。`;
|
|
29102
|
-
return notes.length > 0 ? `${doneMessage}
|
|
29103
|
-
${notes.join(`
|
|
29104
|
-
`)}` : doneMessage;
|
|
29105
29303
|
}
|
|
29106
29304
|
async function setUnattendedExecute(params, ctx) {
|
|
29107
29305
|
assertOrchestrator(ctx, "opx_orch_set_unattended");
|
|
@@ -29914,13 +30112,13 @@ var orchInitSchema = {
|
|
|
29914
30112
|
},
|
|
29915
30113
|
base_branch: {
|
|
29916
30114
|
type: "string",
|
|
29917
|
-
description: "基准分支名(如 main、develop
|
|
30115
|
+
description: "基准分支名(如 main、develop),作为 change 分支(change/{changeId})的 fork 源与收口合并目标。未传则自动从当前 git 分支推导。仅 change 会话入口有效。"
|
|
29918
30116
|
},
|
|
29919
30117
|
recovery: recoverySchema,
|
|
29920
30118
|
mode: {
|
|
29921
30119
|
type: "string",
|
|
29922
30120
|
enum: ["full", "simple"],
|
|
29923
|
-
description: "流程模式选择(仅 change 会话入口有效,独立审查会话不使用):full=完整流程(analyze→implement→三重审查+收尾验证);simple=精简流程(implement→quality_review→done
|
|
30121
|
+
description: "流程模式选择(仅 change 会话入口有效,独立审查会话不使用):full=完整流程(analyze→implement→三重审查+收尾验证);simple=精简流程(implement→quality_review→done,verify_cleanup 仅作收口漂移/冲突回退落点,正常流转不经过,缺省)。首次新建编排状态时固化;已开始的变更仅在允许窗口内可更新:切换任务组(其他任务组均已完成或从未激活)或 recovery.phase=task_analysis 重制当前组(其他任务组同样须已完成或从未激活);其余场景传不同 mode 将报错。"
|
|
29924
30122
|
},
|
|
29925
30123
|
review_scope: {
|
|
29926
30124
|
type: "object",
|
|
@@ -29962,8 +30160,8 @@ var setWorktreeSchema = {
|
|
|
29962
30160
|
type: "object",
|
|
29963
30161
|
properties: {
|
|
29964
30162
|
change_id: { type: "string", minLength: 1, description: "change ID" },
|
|
29965
|
-
worktree_path: { type: "string", description: "git worktree
|
|
29966
|
-
branch_name: { type: "string", description: "worktree
|
|
30163
|
+
worktree_path: { type: "string", description: "git worktree 的绝对路径(可选,不传则按规范自动生成 .worktree/{changeId}/ws)" },
|
|
30164
|
+
branch_name: { type: "string", description: "worktree 对应的分支名(可选,不传则按 change 模型确定性派生:change/{changeId},整个 change 串行复用同一分支与常驻 worktree)" }
|
|
29967
30165
|
},
|
|
29968
30166
|
required: ["change_id"],
|
|
29969
30167
|
additionalProperties: false
|
|
@@ -30152,7 +30350,7 @@ var TOOL_SPECS = {
|
|
|
30152
30350
|
execute: (args, ctx) => initExecute(args, ctx)
|
|
30153
30351
|
},
|
|
30154
30352
|
opx_orch_set_worktree: {
|
|
30155
|
-
description: "确保目标组的 git worktree
|
|
30353
|
+
description: "确保目标组的 git worktree 就绪。change 会话按 change 模型 create-or-reuse:分支 change/{changeId} 不存在时从基准分支 tip 创建,worktree 常驻于 .worktree/{changeId}/ws,全部任务组串行复用(复用校验:目录缺失自愈重建、openspec 文档脏自动提交、代码文件脏拒绝)。只补齐资源,不改变阶段。",
|
|
30156
30354
|
schema: setWorktreeSchema,
|
|
30157
30355
|
execute: (args, ctx) => setWorktreeExecute(args, ctx)
|
|
30158
30356
|
},
|
|
@@ -30162,7 +30360,7 @@ var TOOL_SPECS = {
|
|
|
30162
30360
|
execute: (args, ctx) => statusExecute({ change_id: args.change_id }, ctx)
|
|
30163
30361
|
},
|
|
30164
30362
|
opx_orch_complete_task_group: {
|
|
30165
|
-
description: "
|
|
30363
|
+
description: "完成任务组收尾。非最后任务组仅做门禁与范围标记(不合并、不销毁);最后一个任务组收口时把 change 分支(change/{changeId})一次性合并回 baseBranch(基准分支漂移或文本冲突时回退到收尾验证 verify_cleanup 并返回 blocked),成功后销毁 worktree 并删分支。须在收尾验证(verify_cleanup)通过后调用。主仓库本地改动文件与合并写入文件重合、或存在部分暂存文件时中止并返回 blocked(保留 worktree/分支);主仓库无关脏文件不阻塞合并。",
|
|
30166
30364
|
schema: completeTaskGroupSchema,
|
|
30167
30365
|
execute: (args, ctx) => completeTaskGroupExecute(args, ctx)
|
|
30168
30366
|
},
|
|
@@ -30207,7 +30405,7 @@ async function ensureDefaultUnattended(args, ctx) {
|
|
|
30207
30405
|
}
|
|
30208
30406
|
} catch {}
|
|
30209
30407
|
}
|
|
30210
|
-
var PKG_VERSION = "0.
|
|
30408
|
+
var PKG_VERSION = "0.137.0";
|
|
30211
30409
|
function buildMcpServer(worktree, opts = {}) {
|
|
30212
30410
|
const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION });
|
|
30213
30411
|
for (const [name, spec] of Object.entries(TOOL_SPECS)) {
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
- **提案标准化**:内置 OpenSpec 提案标准(关联场景清点、契约变更详列、动笔前访谈与用户确认),一键初始化到任意项目,在提 change 阶段防漏考虑、防拆分割裂。
|
|
15
15
|
- **智能体团队**:不再是单个 AI 从头写到尾,而是「实现者 + 审查者 + 质量把关者」分工协作,结果更稳。
|
|
16
16
|
- **质量门禁**:实现之后经过工具检查、任务验证与多维度质量审查;发现的问题自动回退修复,也可按规则豁免并留痕。任务验证层(任务审查 / 合并审查)以通过(passed)提交时必须逐项确认全部已申报任务(verified_tasks 全覆盖为工具强制门禁);已通过但漏带确认导致阻塞时,审查者可补交确认结果解除阻塞。simple 模式下验证按性质分流——开发者执行确定性检查并申报,审查者分级复验(低成本项实跑、高成本项核验申报加抽样重放)并专注对抗性判断。
|
|
17
|
-
-
|
|
17
|
+
- **隔离执行**:每个变更使用一条独立分支 `change/{changeId}` 与一个常驻 worktree(`.worktree/{changeId}/ws`),全部任务组在同一 worktree 内串行实施,互不干扰;收口合并前主仓库基准分支上看不到该变更代码(change 级进度可用 `git log change/{changeId}` 查看),完成后统一合并并清理。
|
|
18
18
|
- **过程可审计**:进度、结论、豁免与恢复点全程记录,随时可以接着上次的进度继续。人工执行或环境不可验证的任务经 blocker 上报、用户确认留痕(blocker_updates)后凭记录申报完成;恢复参数 reset_steps 按流程模式(full / simple)校验各自的审查步骤值域后生效。
|
|
19
19
|
|
|
20
20
|
## 快速开始
|
|
@@ -92,7 +92,8 @@ npm install -D @ifrankwang/openspec-agents
|
|
|
92
92
|
1. **输入**:项目中的 OpenSpec 变更规范(变更说明、设计、任务清单、验收规格)。
|
|
93
93
|
2. **分派**:主智能体把任务交给专职智能体——实现者负责写代码,审查者负责检查,质量把关者负责多维验证。
|
|
94
94
|
3. **门禁**:每一步的结论通过检查后才进入下一步;不通过的结论自动回到实现环节修复。
|
|
95
|
-
4.
|
|
95
|
+
4. **收尾合并**:任务组完成只做门禁校验并标记变更范围,不合并、不销毁 worktree;最后一个任务组收口时把 change 分支一次性合并回基准分支,随后销毁 worktree 并删除 change 分支。合并前若基准分支已推进,先做基线漂移检查,发现漂移或冲突则把该任务组回退到收尾验证环节(合并基准分支最新代码并回归复验)后再收口——验收仅在漂移时动态发生,正常路径零额外环节。
|
|
96
|
+
5. **部署注意事项**:change 的全部任务组完成后,主智能体按状态视图指引汇总部署注意事项(规划文档提取 + 开发者实施补充点,含依赖清单/构建配置变更等合并后需在主仓库重装依赖、重新构建的事项)向用户汇报。全程留痕可审计。
|
|
96
97
|
|
|
97
98
|
## 独立审查会话
|
|
98
99
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# simple task workflow:simple 模式的 OpenSpec change 任务流转
|
|
2
|
-
# 对应编排阶段:实现(in_progress)→ 合并审查(review)→ 完成(done);无 analyze / verify_*
|
|
2
|
+
# 对应编排阶段:实现(in_progress)→ 合并审查(review)→ 完成(done);无 analyze / verify_* 环节。
|
|
3
|
+
# verify_cleanup 仅作收口漂移/冲突回退落点(quality_review on_pass 直达 done,正常流转不经过)
|
|
3
4
|
# 语义归属:step 级 instructions/constraints 定义各 step 专属语义;
|
|
4
5
|
# 跨所有 step 共享内容收敛到顶层 common(step 自动继承);非 step 归属的引擎状态文案不进配置。
|
|
5
6
|
# 占位符 {{key}} 经 interpolateText 注入动态值,缺值保留原文。
|
|
6
7
|
# 注意:simple 无 analyze 环节,execution_boundary 永不写入,step constraints 不得含
|
|
7
8
|
# {{allowed_directories}} / {{allowed_packages}} / {{notes}} 边界占位符(缺值会原样渲染),执行边界默认整个 worktree。
|
|
8
|
-
# implement/quality_review 的压制与隔离/清理约束与 task.yaml 同步维护
|
|
9
|
+
# implement/quality_review/verify_cleanup 的压制与隔离/清理约束与 task.yaml 同步维护
|
|
9
10
|
id: task-simple
|
|
10
11
|
name: Simple Task Workflow
|
|
11
12
|
max_retries: 10
|
|
@@ -43,7 +44,7 @@ phases:
|
|
|
43
44
|
- "提交前完成自检并经 self_check_results 汇总:每项检查须含执行命令与结果摘要(证据可核验,供审查者分级复验对照与核验申报),禁止仅报结论不附证据"
|
|
44
45
|
- "提交前按已加载的维度规范类 skill(style/architecture/performance/security/maintainability 能力类别)对本次变更文件逐项自检(聚焦本轮变更文件,不扩散全量代码),自检结论与工具检查结果一并汇总进 self_check_results;修复被驳回 issue 后再次提交前同样须按维度规范类 skill 对变更文件自检后再提交"
|
|
45
46
|
- "自检须覆盖已加载的维度规范类 skill 的全部检查点,self_check_results 按维度逐项列结论(通过/遗留及理由),禁止仅汇总工具检查结果"
|
|
46
|
-
- "
|
|
47
|
+
- "实施产出的部署注意点(数据/结构变更脚本执行、消息队列/缓存等中间件配置调整、新增配置项或功能开关、外部接口契约变化、依赖清单/构建配置变更(合并后需在主仓库重装依赖、重新构建))凡属规划文档(design.md/tasks.md/proposal.md/specs/)未记载的,随提交在 self_check_results 中以 `部署注意点:` 开头的独立行逐条申报(每个注意点独占一行);规划文档已记载的不重复申报;修复轮重新提交时保留既有部署注意点行"
|
|
47
48
|
- "修复完成的 issue 经 fixed_issue_ids 上报后进入待复核(review)状态,终态由报源 reviewer 复核裁定;不可修 issue 经 exempt_issue_ids 申请豁免(申请后进入待裁定(review)状态,由报源 reviewer 在后续审查轮次裁定,被驳回的 issue 回待修复);Info 级 issue 不阻塞提交,无需申请豁免;全部 blocking issue 均已上报修复或申请豁免后才可 passed 提交推进"
|
|
48
49
|
- "确定性质量工具检查出的问题不得以压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等)静默规避:确需压制时,必须在压制处代码注释说明清楚理由(commit message 可补充),并经 exempt_issue_ids 申请豁免由报源 reviewer 裁定;不得以 fixed_issue_ids 冒充修复;被驳回后须移除压制并真实修复"
|
|
49
50
|
- "遇外部依赖、凭证、真实输入或必须 stub/降级/跳过验收才能继续时,以 verdict=failed 提交 blocker(含 source_role、task_id、category、description、evidence、attempted_actions、options)"
|
|
@@ -98,3 +99,22 @@ phases:
|
|
|
98
99
|
- "禁止以 SonarQube 平台状态修改(SAFE/WONTFIX/False Positive/Confirmed 等)作为豁免手段:平台状态仅作用于当前 SonarQube 项目实例,每次 change 使用独立项目 key,不跨 change 生效;存量问题跨 change 豁免必须经 exempt_issue_ids 申请 → 报源 reviewer 裁定 dismissed 的流程"
|
|
99
100
|
- "压制类 issue 严重级别至少 Low"
|
|
100
101
|
- "有实质变更时,低成本实跑项的工具检查及首轮对抗审查覆盖全量代码;非本轮变更文件的工具违规同样必须映射为 issue(命中项目级豁免清单的按 Info,不重新豁免;其余非本轮引入问题按原严重级别上报),不得以不在变更清单或未越界修改为由丢弃;高成本项按核验申报口径执行(复核轮按轮次口径收敛,不重走首轮全量)"
|
|
102
|
+
- id: verify_cleanup
|
|
103
|
+
agents:
|
|
104
|
+
- id: openspec-developer
|
|
105
|
+
capability_tags: [cleanup, api-testing, dev-practices, efficiency]
|
|
106
|
+
transitions:
|
|
107
|
+
on_pass: done
|
|
108
|
+
on_fail: verify_cleanup
|
|
109
|
+
instructions:
|
|
110
|
+
- "将基准分支({{base_branch}})最新代码合并进当前工作分支(git merge 基准分支),解决合并冲突;修复范围仅限合并冲突涉及文件与回归失败定位文件(改动仅用于解决合并引入的问题,禁止借机扩大范围)"
|
|
111
|
+
- "合并后回归复验:按已加载的开发实践规范执行构建验证;按已加载的接口测试规范启动服务并执行接口测试(含关键数据产出/搬迁回归断言);全部通过才算通过,不分失败来源(含基准分支新代码引入的失败)"
|
|
112
|
+
- "按已加载的清理规范执行幂等清理:停止残留的服务实例、清理本 change 的隔离环境、删除残留的扫描分析项目;无对应 skill 规范时跳过并在申报中注明"
|
|
113
|
+
- "清理后确认工作分支 git status 干净(含未跟踪文件)"
|
|
114
|
+
- "逐项申报 validation_steps(completed + 证据;未执行项以 completed=false + 结构化 skip_reason 申报:{\"item\":\"<必做项>\",\"category\":\"<降级类别>\",\"adjudication\":\"user_response|unattended_auto|env_unavailable\",\"note\":\"<说明>\"})"
|
|
115
|
+
- "自报 issue 必须在本 step 内收敛至终态(修复后自行复核通过,或申请豁免后自行裁定)后再提交;报 issue 时显式声明归因维度 dimension(该维度仅作归因标签,不代表对应维度 reviewer 会复核此 issue)"
|
|
116
|
+
- "存在无法解决的残留或回归失败 → verdict=failed 并报 Low+ issue(显式声明归因维度 dimension)"
|
|
117
|
+
- "完成后调用 opx_agent_submit 提交裁决"
|
|
118
|
+
constraints:
|
|
119
|
+
- "可 bash 执行构建/测试/清理命令;禁止 edit/write 任何 {{worktree_path}} 之外的文件"
|
|
120
|
+
- "工具调用边界:仅可调用 opx_status、opx_agent_submit、question"
|