@ifrankwang/openspec-agents 0.136.0 → 0.138.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 +296 -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 +29 -4
- package/src/core/tools/lifecycle.ts +377 -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,20 @@ 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
|
+
}
|
|
21674
|
+
async function listBranchDriftFiles(worktree, sourceBranch, targetBranch) {
|
|
21675
|
+
const r = await runGitChecked(worktree, ["diff", "--name-only", "--no-renames", `${sourceBranch}...${targetBranch}`]);
|
|
21676
|
+
if (!r.success)
|
|
21677
|
+
return null;
|
|
21678
|
+
return parseDiffNameOnly(r.stdout);
|
|
21679
|
+
}
|
|
21666
21680
|
async function isLocalBranch(worktree, branch) {
|
|
21667
21681
|
const r = await runGitChecked(worktree, ["rev-parse", "--verify", `refs/heads/${branch}`]);
|
|
21668
21682
|
return r.success;
|
|
@@ -21799,7 +21813,7 @@ async function mergeBranchToTarget(repoDir, sourceBranch, targetBranch) {
|
|
|
21799
21813
|
success: false,
|
|
21800
21814
|
conflict: false,
|
|
21801
21815
|
blockedMessage: [
|
|
21802
|
-
`- **原因**: 目标分支 \`${targetBranch}\`
|
|
21816
|
+
`- **原因**: 目标分支 \`${targetBranch}\` 正被主仓库检出,且以下本地改动文件与变更分支合并将写入的文件重合;为避免覆盖本地改动,本次未执行任何合并动作(分支引用未动、无半成品):`,
|
|
21803
21817
|
...overlap.map((f) => ` - \`${f}\``),
|
|
21804
21818
|
`- **处理**: 请先 commit/stash/移除上述文件的本地改动后重试;或自行执行 \`git merge ${sourceBranch}\`(真实合并自带脏工作区保护)。`
|
|
21805
21819
|
].join(`
|
|
@@ -21883,6 +21897,7 @@ async function mergeBranchToTarget(repoDir, sourceBranch, targetBranch) {
|
|
|
21883
21897
|
}
|
|
21884
21898
|
return { success: true, conflict: false };
|
|
21885
21899
|
}
|
|
21900
|
+
var RESTORABLE_WORKTREE_BRANCH_PREFIXES = ["change/", "review/", "task-group/"];
|
|
21886
21901
|
async function discoverDiskWorktrees(worktree) {
|
|
21887
21902
|
const result = [];
|
|
21888
21903
|
const wtList = await runGit(worktree, ["worktree", "list"]);
|
|
@@ -21891,7 +21906,7 @@ async function discoverDiskWorktrees(worktree) {
|
|
|
21891
21906
|
const m = line.match(/^(\S+)\s+[0-9a-f]+\s+\[(.+?)\]/);
|
|
21892
21907
|
if (m) {
|
|
21893
21908
|
const branch = m[2].trim();
|
|
21894
|
-
if (
|
|
21909
|
+
if (RESTORABLE_WORKTREE_BRANCH_PREFIXES.some((p) => branch.startsWith(p))) {
|
|
21895
21910
|
result.push({ branch, path: m[1].trim() });
|
|
21896
21911
|
}
|
|
21897
21912
|
}
|
|
@@ -22195,6 +22210,9 @@ function isTaskGroupSettled(item) {
|
|
|
22195
22210
|
const childrenSettled = item.children.filter((c) => c.type === "task").every((c) => c.phase === "todo" || isTerminalPhase(c.phase));
|
|
22196
22211
|
return noTags && childrenSettled;
|
|
22197
22212
|
}
|
|
22213
|
+
function isFinalTaskGroup(state, item) {
|
|
22214
|
+
return state.workItems.filter((w) => w.id.startsWith("task:") && w.id !== item.id).every(isTaskGroupSettled);
|
|
22215
|
+
}
|
|
22198
22216
|
function isBlockingSeverity(severity) {
|
|
22199
22217
|
return severity !== undefined && BLOCKING_SEVERITIES.includes(severity);
|
|
22200
22218
|
}
|
|
@@ -27032,7 +27050,7 @@ function renderWorktreeSection(state, tg, opts) {
|
|
|
27032
27050
|
lines.push("- **审查范围**: 全量代码库审查(无区间界定,工具检查与审查覆盖整个代码库)");
|
|
27033
27051
|
}
|
|
27034
27052
|
} else if (tg.baseRef) {
|
|
27035
|
-
lines.push(`- **变更范围**: 用 \`git -C ${tg.worktreePath} diff --name-only ${tg.baseRef}..HEAD\`
|
|
27053
|
+
lines.push(`- **变更范围**: 用 \`git -C ${tg.worktreePath} diff --name-only ${tg.baseRef}..HEAD\` 查询本任务组的已提交变更文件`);
|
|
27036
27054
|
}
|
|
27037
27055
|
lines.push("- **⚠️ 约束**: 所有读写和 git 操作均在此目录下进行;严禁直接修改主仓库/主分支路径下的文件(如 `<repo>/openspec/...`)");
|
|
27038
27056
|
lines.push("- **路径解析**: 推荐阅读文档均为相对 worktree 路径的引用,一律以 worktree 路径为基准解析,禁止从主仓库根目录解析");
|
|
@@ -27348,13 +27366,14 @@ function renderTerminalPhase(item, state, caller) {
|
|
|
27348
27366
|
return lines.join(`
|
|
27349
27367
|
`);
|
|
27350
27368
|
}
|
|
27369
|
+
const finalGroup = state ? isFinalTaskGroup(state, item) : true;
|
|
27351
27370
|
if (item.metadata["completed_at"] !== undefined) {
|
|
27352
27371
|
return [
|
|
27353
27372
|
"# ✅ 任务组已完成",
|
|
27354
27373
|
"",
|
|
27355
27374
|
`- **完成时间**: ${item.metadata["completed_at"]}`,
|
|
27356
27375
|
"",
|
|
27357
|
-
"
|
|
27376
|
+
finalGroup ? "本任务组为最后一个收口任务组,change 收口已完成:变更分支已合并回基准分支,编排已完成并收尾。" : "本任务组已完成,变更保留在 change 分支上,待全部任务组完成后统一收口合并。",
|
|
27358
27377
|
"",
|
|
27359
27378
|
...renderDeploymentNotesSection(state, caller)
|
|
27360
27379
|
].join(`
|
|
@@ -27363,7 +27382,8 @@ function renderTerminalPhase(item, state, caller) {
|
|
|
27363
27382
|
return [
|
|
27364
27383
|
"# \uD83C\uDFC1 任务组已完成,待收尾",
|
|
27365
27384
|
"",
|
|
27366
|
-
"
|
|
27385
|
+
"全部审核层已通过。",
|
|
27386
|
+
finalGroup ? "调用 `opx_orch_complete_task_group` 完成 change 收口(把变更分支合并回基准分支并清理)。" : "调用 `opx_orch_complete_task_group` 完成任务组收尾(变更保留在 change 分支,统一收口时合并)。",
|
|
27367
27387
|
"",
|
|
27368
27388
|
...renderDeploymentNotesSection(state, caller)
|
|
27369
27389
|
].join(`
|
|
@@ -27380,7 +27400,7 @@ function renderDeploymentNotesSection(state, caller) {
|
|
|
27380
27400
|
"",
|
|
27381
27401
|
"向用户汇报本次变更的部署注意事项:",
|
|
27382
27402
|
"",
|
|
27383
|
-
`- 阅读主仓库 \`openspec/changes/${state.changeId}/\` 下规划文档(design.md、tasks.md、proposal.md、specs
|
|
27403
|
+
`- 阅读主仓库 \`openspec/changes/${state.changeId}/\` 下规划文档(design.md、tasks.md、proposal.md、specs/)提取部署要点:数据/结构变更脚本执行、消息队列/缓存等中间件配置调整、新增配置项或功能开关、外部接口契约变化、依赖清单/构建配置变更(合并后需在主仓库重装依赖、重新构建)`,
|
|
27384
27404
|
"- 无规划文档时跳过文档提取,仅转述下方实施补充点",
|
|
27385
27405
|
""
|
|
27386
27406
|
];
|
|
@@ -27764,7 +27784,7 @@ function renderAgentWorking(item, rec, step, common, ctxAgent, state, tg, toolCh
|
|
|
27764
27784
|
lines.push(...eff.lines);
|
|
27765
27785
|
n = eff.nextNum;
|
|
27766
27786
|
if (tg.worktreePath && tg.baseRef) {
|
|
27767
|
-
lines.push(`${n++}.
|
|
27787
|
+
lines.push(`${n++}. 用上方「变更范围」命令获取本任务组的已提交变更文件清单,作为本次实施/修改范围;审查/报告范围以当前 step 指令为准`);
|
|
27768
27788
|
}
|
|
27769
27789
|
lines.push(`${n++}. 执行当前 step(\`${rec.stepId}\`)职责范围内的全部工作——遵循所有已加载 skill 的全部规范与约束`);
|
|
27770
27790
|
lines.push(`${n++}. 逐项检视所有已加载 skill 的 MUST 规范,确认全部满足(不满足则补做,不得跳过)`);
|
|
@@ -27977,7 +27997,7 @@ function renderToolChangesEvidence(item, tg, toolChanges, kind) {
|
|
|
27977
27997
|
`)];
|
|
27978
27998
|
}
|
|
27979
27999
|
if (checkpoint) {
|
|
27980
|
-
lines.push(`- **口径**: 本次为「${label.title}(${checkpoint}..HEAD)」的增量区间(含已提交与未提交的非 openspec 变更);` + `与上方「变更范围」(${baseRef ?? "(无)"}..HEAD
|
|
28000
|
+
lines.push(`- **口径**: 本次为「${label.title}(${checkpoint}..HEAD)」的增量区间(含已提交与未提交的非 openspec 变更);` + `与上方「变更范围」(${baseRef ?? "(无)"}..HEAD 本任务组累计口径)不同`, "");
|
|
27981
28001
|
} else if (baseRef) {
|
|
27982
28002
|
lines.push(`- **口径**: 本次区间以基线(${baseRef}..HEAD)兜底,与上方「变更范围」一致(首次进入,无${label.lastCheck}记录;` + "含已提交与未提交的非 openspec 变更)", "");
|
|
27983
28003
|
} else {
|
|
@@ -28313,7 +28333,16 @@ function applyRecoveryState(item, recovery, parsedTasks, state) {
|
|
|
28313
28333
|
if ((recovery?.reset_steps ?? []).includes("quality_review")) {
|
|
28314
28334
|
clearStepTags(item, "quality_review");
|
|
28315
28335
|
}
|
|
28316
|
-
|
|
28336
|
+
const workflow = loadWorkflowFile(resolveWorkflowPath(state));
|
|
28337
|
+
item.currentStep = firstUnpassedReviewStep(item, workflow);
|
|
28338
|
+
if (item.currentStep === null) {
|
|
28339
|
+
const unfinishedTasks = item.children.filter((child) => child.type === "task" && !isTerminalPhase(child.phase));
|
|
28340
|
+
if (unfinishedTasks.length === 0) {
|
|
28341
|
+
item.phase = "done";
|
|
28342
|
+
} else {
|
|
28343
|
+
item.currentStep = "quality_review";
|
|
28344
|
+
}
|
|
28345
|
+
}
|
|
28317
28346
|
syncTaskChildren(item, parsedTasks, { defaultStatus: "done" });
|
|
28318
28347
|
return;
|
|
28319
28348
|
}
|
|
@@ -28798,17 +28827,75 @@ async function bindReviewWorktreeRefs(item, worktreePath, branch, state) {
|
|
|
28798
28827
|
}
|
|
28799
28828
|
delete item.metadata["base_ref"];
|
|
28800
28829
|
}
|
|
28801
|
-
async function bindWorktreeRefs(item, worktreePath, branch
|
|
28830
|
+
async function bindWorktreeRefs(item, worktreePath, branch) {
|
|
28802
28831
|
item.metadata["worktree_path"] = worktreePath;
|
|
28803
28832
|
item.metadata["branch_name"] = branch;
|
|
28804
|
-
const
|
|
28805
|
-
if (!
|
|
28806
|
-
|
|
28807
|
-
|
|
28808
|
-
|
|
28809
|
-
|
|
28833
|
+
const existing = item.metadata["scope_start_oid"];
|
|
28834
|
+
if (typeof existing !== "string" || !await isAncestor(worktreePath, existing, branch)) {
|
|
28835
|
+
const head = (await runGit(worktreePath, ["rev-parse", branch])).trim();
|
|
28836
|
+
if (!head)
|
|
28837
|
+
throw new Error(`worktree 绑定成功但无法获取 ${branch} 的 HEAD:${worktreePath}`);
|
|
28838
|
+
item.metadata["scope_start_oid"] = head;
|
|
28839
|
+
}
|
|
28840
|
+
item.metadata["base_ref"] = item.metadata["scope_start_oid"];
|
|
28841
|
+
}
|
|
28842
|
+
function deriveChangeBranch(changeId) {
|
|
28843
|
+
return `change/${normalizeSessionToken(changeId)}`;
|
|
28844
|
+
}
|
|
28845
|
+
async function ensureChangeBranch(repoRoot, branch, baseBranch) {
|
|
28846
|
+
if (await isLocalBranch(repoRoot, branch))
|
|
28847
|
+
return false;
|
|
28848
|
+
const created = await runGitChecked(repoRoot, ["branch", branch, baseBranch]);
|
|
28849
|
+
if (!created.success)
|
|
28850
|
+
throw new Error(`创建分支 "${branch}"(fork 自 ${baseBranch})失败:${created.stderr}`);
|
|
28851
|
+
return true;
|
|
28852
|
+
}
|
|
28853
|
+
async function ensureChangeWorktree(repoRoot, state, item, branch, defaultPath) {
|
|
28854
|
+
await ensureChangeBranch(repoRoot, branch, state.baseBranch);
|
|
28855
|
+
const wtList = await runGit(repoRoot, ["worktree", "list"]);
|
|
28856
|
+
const existingLine = wtList.split(`
|
|
28857
|
+
`).find((l) => {
|
|
28858
|
+
const m = l.match(/^(\S+)\s+[0-9a-f]+\s+\[(.+?)\]/);
|
|
28859
|
+
return m && m[2].trim() === branch;
|
|
28860
|
+
});
|
|
28861
|
+
const registeredPath = existingLine ? existingLine.match(/^(\S+)/)?.[1] : undefined;
|
|
28862
|
+
const targetPath = registeredPath ?? defaultPath;
|
|
28863
|
+
if (!await pathExists(targetPath)) {
|
|
28864
|
+
await runGitChecked(repoRoot, ["worktree", "prune"]);
|
|
28865
|
+
await runGit(repoRoot, ["worktree", "add", targetPath, branch]);
|
|
28866
|
+
await bindWorktreeRefs(item, targetPath, branch);
|
|
28867
|
+
return { path: targetPath, reused: false };
|
|
28868
|
+
}
|
|
28869
|
+
if (!registeredPath) {
|
|
28870
|
+
const addRes = await runGitChecked(repoRoot, ["worktree", "add", targetPath, branch]);
|
|
28871
|
+
if (!addRes.success) {
|
|
28872
|
+
throw new Error(`无法在 "${targetPath}" 就位 worktree:目录已存在且不是检出分支 "${branch}" 的 git worktree(git: ${addRes.stderr})。
|
|
28873
|
+
` + `请人工确认该目录内容并处理(删除或迁移)后重试。`);
|
|
28874
|
+
}
|
|
28875
|
+
await bindWorktreeRefs(item, targetPath, branch);
|
|
28876
|
+
return { path: targetPath, reused: false };
|
|
28877
|
+
}
|
|
28878
|
+
const statusOut = await runGit(targetPath, ["status", "--porcelain"]);
|
|
28879
|
+
const dirtyPaths = statusOut.split(`
|
|
28880
|
+
`).map((l) => l.trim()).filter(Boolean).map((l) => {
|
|
28881
|
+
const f = l.replace(/^\S+\s+/, "");
|
|
28882
|
+
return (f.includes(" -> ") ? f.split(" -> ").pop() : f).replace(/^"+|"+$/g, "");
|
|
28883
|
+
}).filter(Boolean);
|
|
28884
|
+
const codeDirty = dirtyPaths.filter((p) => !p.startsWith("openspec/"));
|
|
28885
|
+
if (codeDirty.length > 0) {
|
|
28886
|
+
throw new Error(`已有 worktree "${targetPath}" 存在未提交的代码文件变更(${codeDirty.join("、")}),无法自动处理。
|
|
28887
|
+
` + `请先在该 worktree 内 commit 或 stash 后重试。`);
|
|
28888
|
+
}
|
|
28889
|
+
if (dirtyPaths.length > 0) {
|
|
28890
|
+
const addResult = await runGitChecked(targetPath, ["add", "--", ...dirtyPaths]);
|
|
28891
|
+
if (!addResult.success)
|
|
28892
|
+
throw new Error(`worktree openspec 目录 git add 失败:${addResult.stderr}`);
|
|
28893
|
+
const commitResult = await runGitChecked(targetPath, ["commit", "-m", "docs(openspec): auto-commit before worktree reuse"]);
|
|
28894
|
+
if (!commitResult.success)
|
|
28895
|
+
throw new Error(`worktree openspec 目录 git commit 失败:${commitResult.stderr}`);
|
|
28810
28896
|
}
|
|
28811
|
-
item
|
|
28897
|
+
await bindWorktreeRefs(item, targetPath, branch);
|
|
28898
|
+
return { path: targetPath, reused: true };
|
|
28812
28899
|
}
|
|
28813
28900
|
async function setWorktreeExecute(params, ctx) {
|
|
28814
28901
|
assertOrchestrator(ctx, "opx_orch_set_worktree");
|
|
@@ -28819,6 +28906,7 @@ async function setWorktreeExecute(params, ctx) {
|
|
|
28819
28906
|
if (!item)
|
|
28820
28907
|
throw new Error(`工作项 "task:${state.taskGroupId}" 缺失,请重新调用 opx_orch_init。`);
|
|
28821
28908
|
const repoRoot = ctx.worktree;
|
|
28909
|
+
const isReview = state.kind === "review";
|
|
28822
28910
|
const rawBranch = params.branch_name ?? "";
|
|
28823
28911
|
if (rawBranch !== "") {
|
|
28824
28912
|
const check = await runGitChecked(repoRoot, ["check-ref-format", "--branch", rawBranch]);
|
|
@@ -28826,13 +28914,18 @@ async function setWorktreeExecute(params, ctx) {
|
|
|
28826
28914
|
throw new Error(`分支名 "${rawBranch}" 不合法,请修正后重试。`);
|
|
28827
28915
|
}
|
|
28828
28916
|
}
|
|
28829
|
-
const
|
|
28830
|
-
|
|
28917
|
+
const branch = rawBranch || (isReview ? `review/${state.changeId}` : deriveChangeBranch(state.changeId));
|
|
28918
|
+
if (!isReview) {
|
|
28919
|
+
const check = await runGitChecked(repoRoot, ["check-ref-format", "--branch", branch]);
|
|
28920
|
+
if (!check.success) {
|
|
28921
|
+
throw new Error(`派生分支名 "${branch}" 不合法(change_id="${state.changeId}"),请修正 change_id 或显式传入 branch_name。`);
|
|
28922
|
+
}
|
|
28923
|
+
}
|
|
28831
28924
|
let wtPath;
|
|
28832
28925
|
if (params.worktree_path) {
|
|
28833
28926
|
wtPath = assertPathWithin(repoRoot, params.worktree_path, "worktree_path");
|
|
28834
28927
|
} else {
|
|
28835
|
-
wtPath = isReview ? path6.join(repoRoot, ".worktree", state.changeId, "review") : path6.join(repoRoot, ".worktree", state.changeId,
|
|
28928
|
+
wtPath = isReview ? path6.join(repoRoot, ".worktree", state.changeId, "review") : path6.join(repoRoot, ".worktree", state.changeId, "ws");
|
|
28836
28929
|
}
|
|
28837
28930
|
const changeStatus = await runGit(repoRoot, ["status", "--porcelain", `openspec/changes/${state.changeId}/`]);
|
|
28838
28931
|
if (changeStatus.trim().length > 0) {
|
|
@@ -28843,61 +28936,57 @@ async function setWorktreeExecute(params, ctx) {
|
|
|
28843
28936
|
if (!commitResult.success)
|
|
28844
28937
|
throw new Error(`change 目录 git commit 失败:${commitResult.stderr}`);
|
|
28845
28938
|
}
|
|
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
28939
|
let reused = false;
|
|
28854
|
-
if (
|
|
28855
|
-
const
|
|
28856
|
-
const
|
|
28857
|
-
|
|
28858
|
-
|
|
28940
|
+
if (isReview) {
|
|
28941
|
+
const wtList = await runGit(repoRoot, ["worktree", "list"]);
|
|
28942
|
+
const existingLine = wtList.split(`
|
|
28943
|
+
`).find((l) => {
|
|
28944
|
+
const m = l.match(/^(\S+)\s+[0-9a-f]+\s+\[(.+?)\]/);
|
|
28945
|
+
return m && m[2].trim() === branch;
|
|
28946
|
+
});
|
|
28947
|
+
const existingPath = existingLine ? existingLine.match(/^(\S+)/)?.[1] : undefined;
|
|
28948
|
+
if (existingPath) {
|
|
28949
|
+
const baseHead = await runGit(repoRoot, ["rev-parse", state.baseBranch]);
|
|
28950
|
+
const mergeResult = await runGitChecked(existingPath, ["merge", "--ff-only", baseHead]);
|
|
28951
|
+
if (mergeResult.success) {
|
|
28859
28952
|
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
28953
|
reused = true;
|
|
28876
28954
|
} else {
|
|
28877
|
-
const
|
|
28878
|
-
if (!
|
|
28879
|
-
|
|
28880
|
-
|
|
28881
|
-
|
|
28882
|
-
|
|
28883
|
-
|
|
28884
|
-
|
|
28955
|
+
const clean = await isWorktreeClean(existingPath);
|
|
28956
|
+
if (!clean) {
|
|
28957
|
+
throw new Error(`已有 worktree "${existingPath}" 与 ${state.baseBranch} 分叉且有未提交变更,无法自动 fast-forward。
|
|
28958
|
+
` + `请手动处理后重试。`);
|
|
28959
|
+
}
|
|
28960
|
+
const localCommitCount = parseInt(await runGit(existingPath, ["rev-list", "--count", `${state.baseBranch}..HEAD`]), 10);
|
|
28961
|
+
if (localCommitCount > 0 || Number.isNaN(localCommitCount)) {
|
|
28962
|
+
await bindReviewWorktreeRefs(item, existingPath, branch, state);
|
|
28963
|
+
reused = true;
|
|
28964
|
+
} else {
|
|
28965
|
+
const cleanup = await removeTaskGroupWorktree(repoRoot, existingPath, { branchName: branch });
|
|
28966
|
+
if (!cleanup.dirResolved || cleanup.branchDeleted === false) {
|
|
28967
|
+
const problems = [];
|
|
28968
|
+
if (!cleanup.dirResolved)
|
|
28969
|
+
problems.push(`无法清理已有 worktree "${existingPath}"`);
|
|
28970
|
+
if (cleanup.branchDeleted === false)
|
|
28971
|
+
problems.push(`无法清理已有分支 "${branch}"`);
|
|
28972
|
+
throw new Error(`${problems.join(";")}:${cleanup.errors.join(";") || "原因未知"}
|
|
28885
28973
|
请手动处理后重试。`);
|
|
28974
|
+
}
|
|
28886
28975
|
}
|
|
28887
28976
|
}
|
|
28888
28977
|
}
|
|
28889
|
-
|
|
28890
|
-
|
|
28891
|
-
|
|
28892
|
-
await runGit(repoRoot, ["worktree", "add", "-b", branch, wtPath, forkBranch]);
|
|
28893
|
-
if (isReview) {
|
|
28978
|
+
if (!reused) {
|
|
28979
|
+
const forkBranch = state.baseBranch;
|
|
28980
|
+
await runGit(repoRoot, ["worktree", "add", "-b", branch, wtPath, forkBranch]);
|
|
28894
28981
|
await bindReviewWorktreeRefs(item, wtPath, branch, state);
|
|
28895
28982
|
if (state.reviewScope?.scopeType === "pr" && !item.metadata["base_ref"]) {
|
|
28896
28983
|
throw new Error(`worktree 创建成功但无法获取 ${state.reviewScope.baseRef} 与 ${state.baseBranch} 的 merge-base:${wtPath}`);
|
|
28897
28984
|
}
|
|
28898
|
-
} else {
|
|
28899
|
-
await bindWorktreeRefs(item, wtPath, branch, forkBranch, { requireBaseRef: true });
|
|
28900
28985
|
}
|
|
28986
|
+
} else {
|
|
28987
|
+
const ensured = await ensureChangeWorktree(repoRoot, state, item, branch, wtPath);
|
|
28988
|
+
wtPath = ensured.path;
|
|
28989
|
+
reused = ensured.reused;
|
|
28901
28990
|
}
|
|
28902
28991
|
await writeState(ctx.worktree, state);
|
|
28903
28992
|
const storedPath = typeof item.metadata["worktree_path"] === "string" ? item.metadata["worktree_path"] : null;
|
|
@@ -28998,8 +29087,49 @@ function renderReviewIssueSummary(item) {
|
|
|
28998
29087
|
return lines.join(`
|
|
28999
29088
|
`);
|
|
29000
29089
|
}
|
|
29090
|
+
var DOCUMENTATION_EXTENSIONS = [".md", ".mdx", ".markdown", ".rst", ".adoc"];
|
|
29091
|
+
function isDocumentationOnly(files) {
|
|
29092
|
+
if (files === null)
|
|
29093
|
+
return false;
|
|
29094
|
+
return files.every((f) => DOCUMENTATION_EXTENSIONS.some((ext) => f.endsWith(ext)));
|
|
29095
|
+
}
|
|
29096
|
+
function rollbackToCleanupStep(item, workflow) {
|
|
29097
|
+
if (!workflow.stepMap.has("verify_cleanup"))
|
|
29098
|
+
return false;
|
|
29099
|
+
delete item.metadata["_advance_block_reason"];
|
|
29100
|
+
delete item.metadata["_checkpoint"];
|
|
29101
|
+
resetInternalRetryCount(item);
|
|
29102
|
+
clearStepTags(item, "verify_cleanup");
|
|
29103
|
+
delete item.metadata["scope_end_oid"];
|
|
29104
|
+
item.phase = "review";
|
|
29105
|
+
item.currentStep = "verify_cleanup";
|
|
29106
|
+
return true;
|
|
29107
|
+
}
|
|
29108
|
+
function renderFinalizeBlocked(reasons, handlings, opts) {
|
|
29109
|
+
const lines = [
|
|
29110
|
+
`- **status**: blocked`,
|
|
29111
|
+
...reasons.map((r) => `- **原因**: ${r}`),
|
|
29112
|
+
`- **说明**: worktree 与变更分支 \`${opts.branchName}\` 已保留;未执行合并,无半成品状态。属多 change 并行推进的预期行为。` + (opts.rolledBack ? "本任务组状态已回退到收尾验证(verify_cleanup)。" : ""),
|
|
29113
|
+
...handlings.map((h) => `- **处理**: ${h}`),
|
|
29114
|
+
"",
|
|
29115
|
+
"重新查询 opx_status 获取分派指引。"
|
|
29116
|
+
];
|
|
29117
|
+
if (opts.checkboxWarning)
|
|
29118
|
+
lines.push(opts.checkboxWarning);
|
|
29119
|
+
return lines.join(`
|
|
29120
|
+
`);
|
|
29121
|
+
}
|
|
29001
29122
|
async function completeTaskGroupExecute(params, ctx) {
|
|
29002
29123
|
assertOrchestrator(ctx, "opx_orch_complete_task_group");
|
|
29124
|
+
const lockPath = await getLockPath(ctx.worktree, params.change_id);
|
|
29125
|
+
await acquireLock(lockPath);
|
|
29126
|
+
try {
|
|
29127
|
+
return await completeTaskGroupLocked(params, ctx);
|
|
29128
|
+
} finally {
|
|
29129
|
+
releaseLock(lockPath);
|
|
29130
|
+
}
|
|
29131
|
+
}
|
|
29132
|
+
async function completeTaskGroupLocked(params, ctx) {
|
|
29003
29133
|
const state = await readStateByWorktree(ctx.worktree, params.change_id);
|
|
29004
29134
|
if (!state)
|
|
29005
29135
|
throw new Error("编排会话未初始化。请先调用 opx_orch_init。");
|
|
@@ -29018,7 +29148,8 @@ async function completeTaskGroupExecute(params, ctx) {
|
|
|
29018
29148
|
if (!clean)
|
|
29019
29149
|
throw new Error(`worktree "${worktreePath}" 存在未 commit 内容,请先 commit 再完成任务组。`);
|
|
29020
29150
|
}
|
|
29021
|
-
const
|
|
29151
|
+
const isReview = state.kind === "review";
|
|
29152
|
+
const isReviewNone = isReview && state.reviewScope?.fix === "none";
|
|
29022
29153
|
const openIssues = item.children.filter((c) => isBlockingSeverity(c.severity) && !isTerminalPhase(c.phase));
|
|
29023
29154
|
if (!isReviewNone && openIssues.length > 0) {
|
|
29024
29155
|
throw new Error(`存在 ${openIssues.length} 个 Low 及以上的未解决 issue 未处理,请先修复或申请豁免。`);
|
|
@@ -29040,6 +29171,98 @@ async function completeTaskGroupExecute(params, ctx) {
|
|
|
29040
29171
|
checkboxWarning = `- **tasks.md 复选框勾选失败**: ${e instanceof Error ? e.message : String(e)}(不影响收尾)`;
|
|
29041
29172
|
}
|
|
29042
29173
|
}
|
|
29174
|
+
if (!isReview && worktreePath && branchName) {
|
|
29175
|
+
const endOid = (await runGit(worktreePath, ["rev-parse", branchName])).trim();
|
|
29176
|
+
if (endOid)
|
|
29177
|
+
item.metadata["scope_end_oid"] = endOid;
|
|
29178
|
+
}
|
|
29179
|
+
if (isReview) {
|
|
29180
|
+
return finalizeReviewSession(state, item, params, ctx, { worktreePath, branchName, isReviewNone, checkboxWarning });
|
|
29181
|
+
}
|
|
29182
|
+
if (!isFinalTaskGroup(state, item)) {
|
|
29183
|
+
item.metadata["completed_at"] = new Date().toISOString();
|
|
29184
|
+
await writeState(ctx.worktree, state);
|
|
29185
|
+
const doneMessage = "任务组已完成。本任务组变更保留在 change 分支上,待全部任务组完成后统一收口合并(本次不合并、不销毁 worktree)。";
|
|
29186
|
+
return checkboxWarning ? `${doneMessage}
|
|
29187
|
+
${checkboxWarning}` : doneMessage;
|
|
29188
|
+
}
|
|
29189
|
+
const mergeTarget = state.baseBranch;
|
|
29190
|
+
let docDriftNote = "";
|
|
29191
|
+
if (branchName) {
|
|
29192
|
+
await ensureChangeBranch(ctx.worktree, branchName, mergeTarget);
|
|
29193
|
+
if (!await isAncestor(ctx.worktree, mergeTarget, branchName)) {
|
|
29194
|
+
const driftFiles = await listBranchDriftFiles(ctx.worktree, branchName, mergeTarget);
|
|
29195
|
+
if (!isDocumentationOnly(driftFiles)) {
|
|
29196
|
+
const workflow = loadWorkflowFile(resolveWorkflowPath(state));
|
|
29197
|
+
const rolledBack = rollbackToCleanupStep(item, workflow);
|
|
29198
|
+
if (rolledBack)
|
|
29199
|
+
await writeState(ctx.worktree, state);
|
|
29200
|
+
return renderFinalizeBlocked([
|
|
29201
|
+
`基准分支 \`${mergeTarget}\` 已推进(与变更分支 \`${branchName}\` 存在漂移):基准分支最新提交未包含在变更分支历史中,直接合并会遗漏基准分支新内容。`
|
|
29202
|
+
], [
|
|
29203
|
+
`在 worktree 内执行 \`git merge ${mergeTarget}\` 合入基准分支最新代码并解决冲突,完成回归验证后重新提交收尾验证(opx_agent_submit,step_id="verify_cleanup"),通过后再调用 opx_orch_complete_task_group 重新收口。`
|
|
29204
|
+
], { branchName, mergeTarget, rolledBack, checkboxWarning });
|
|
29205
|
+
}
|
|
29206
|
+
const files = driftFiles ?? [];
|
|
29207
|
+
const driftList = files.length > 10 ? `${files.slice(0, 10).join(", ")} 等 ${files.length} 个` : files.join(", ");
|
|
29208
|
+
docDriftNote = `- **基准漂移(纯文档)**: 基准分支 \`${mergeTarget}\` 相对变更分支切出点净变化 ${files.length} 个文件` + (files.length > 0 ? `(${driftList})` : "") + ",均为文档,不影响代码语义,未回退收尾验证直接合并。";
|
|
29209
|
+
}
|
|
29210
|
+
const mergeResult = await mergeBranchToTarget(ctx.worktree, branchName, mergeTarget);
|
|
29211
|
+
if (mergeResult.blockedMessage) {
|
|
29212
|
+
return [`- **status**: blocked`, mergeResult.blockedMessage].join(`
|
|
29213
|
+
`);
|
|
29214
|
+
}
|
|
29215
|
+
if (!mergeResult.success) {
|
|
29216
|
+
const workflow = loadWorkflowFile(resolveWorkflowPath(state));
|
|
29217
|
+
const rolledBack = rollbackToCleanupStep(item, workflow);
|
|
29218
|
+
if (rolledBack)
|
|
29219
|
+
await writeState(ctx.worktree, state);
|
|
29220
|
+
return renderFinalizeBlocked([
|
|
29221
|
+
`变更分支 \`${branchName}\` 合并到 \`${mergeTarget}\` 时发生冲突,未产生任何变更(分支引用未动,无半成品合并)。`
|
|
29222
|
+
], [
|
|
29223
|
+
`在 worktree 内执行 \`git merge ${mergeTarget}\` 合入基准分支最新代码解决冲突并提交,完成回归验证后重新提交收尾验证(opx_agent_submit,step_id="verify_cleanup"),通过后再调用 opx_orch_complete_task_group 重新收口。`
|
|
29224
|
+
], { branchName, mergeTarget, rolledBack, checkboxWarning });
|
|
29225
|
+
}
|
|
29226
|
+
}
|
|
29227
|
+
let cleanupNote = "";
|
|
29228
|
+
let cleanupResidualWarning = "";
|
|
29229
|
+
if (worktreePath) {
|
|
29230
|
+
const cleanup = await removeTaskGroupWorktree(ctx.worktree, worktreePath, { branchName });
|
|
29231
|
+
if (cleanup.dirResolved) {
|
|
29232
|
+
await sweepEmptyWorktreeParents(ctx.worktree, worktreePath, params.change_id);
|
|
29233
|
+
const successNotes = [];
|
|
29234
|
+
if (cleanup.dirResolvedByFallback) {
|
|
29235
|
+
successNotes.push("- **cleanup**: git worktree remove 未直接移除目录,已按文件系统兜底删除并 prune 管理记录。");
|
|
29236
|
+
}
|
|
29237
|
+
if (cleanup.errors.length > 0) {
|
|
29238
|
+
successNotes.push(`- **cleanup 部分告警**: ${cleanup.errors.join(";")}`);
|
|
29239
|
+
}
|
|
29240
|
+
cleanupNote = successNotes.join(`
|
|
29241
|
+
`);
|
|
29242
|
+
} else {
|
|
29243
|
+
item.metadata["cleanup_residual"] = { worktree_path: worktreePath, errors: cleanup.errors };
|
|
29244
|
+
cleanupResidualWarning = [
|
|
29245
|
+
"",
|
|
29246
|
+
"## ⚠️ worktree 清理残留(不影响收口)",
|
|
29247
|
+
`- **残留路径**: \`${worktreePath}\``,
|
|
29248
|
+
"- **原因**:",
|
|
29249
|
+
...cleanup.errors.map((e) => ` - ${e}`),
|
|
29250
|
+
`- **处理**: 请人工执行 \`rm -rf '${worktreePath}' && git worktree prune\` 清理残留目录与 git 管理记录。`,
|
|
29251
|
+
branchName ? `- **分支**: 分支 "${branchName}" 未删除。` : ""
|
|
29252
|
+
].filter(Boolean).join(`
|
|
29253
|
+
`);
|
|
29254
|
+
}
|
|
29255
|
+
}
|
|
29256
|
+
item.metadata["completed_at"] = new Date().toISOString();
|
|
29257
|
+
await writeState(ctx.worktree, state);
|
|
29258
|
+
const notes = [docDriftNote, cleanupNote, checkboxWarning, cleanupResidualWarning].filter(Boolean);
|
|
29259
|
+
const doneMessage = branchName ? `任务组已完成并合并到 "${mergeTarget}"。` : "任务组已完成(无变更分支引用,跳过合并)。";
|
|
29260
|
+
return notes.length > 0 ? `${doneMessage}
|
|
29261
|
+
${notes.join(`
|
|
29262
|
+
`)}` : doneMessage;
|
|
29263
|
+
}
|
|
29264
|
+
async function finalizeReviewSession(state, item, params, ctx, refs) {
|
|
29265
|
+
const { worktreePath, branchName, isReviewNone, checkboxWarning } = refs;
|
|
29043
29266
|
const mergeTarget = state.baseBranch;
|
|
29044
29267
|
if (branchName && !isReviewNone) {
|
|
29045
29268
|
const mergeResult = await mergeBranchToTarget(ctx.worktree, branchName, mergeTarget);
|
|
@@ -29089,19 +29312,13 @@ async function completeTaskGroupExecute(params, ctx) {
|
|
|
29089
29312
|
item.metadata["completed_at"] = new Date().toISOString();
|
|
29090
29313
|
await writeState(ctx.worktree, state);
|
|
29091
29314
|
const notes = [cleanupNote, checkboxWarning, cleanupResidualWarning].filter(Boolean);
|
|
29092
|
-
|
|
29093
|
-
|
|
29094
|
-
|
|
29095
|
-
return notes.length > 0 ? `${doneMessage}
|
|
29315
|
+
const summary = renderReviewIssueSummary(item);
|
|
29316
|
+
const doneMessage = isReviewNone ? `独立审查会话已完成(只审模式,未合并,worktree 与分支已销毁)。` : `独立审查会话已完成并合并到 "${mergeTarget}"。`;
|
|
29317
|
+
return notes.length > 0 ? `${doneMessage}
|
|
29096
29318
|
${notes.join(`
|
|
29097
29319
|
`)}
|
|
29098
29320
|
${summary}` : `${doneMessage}
|
|
29099
29321
|
${summary}`;
|
|
29100
|
-
}
|
|
29101
|
-
const doneMessage = `任务组已完成并合并到 "${mergeTarget}"。`;
|
|
29102
|
-
return notes.length > 0 ? `${doneMessage}
|
|
29103
|
-
${notes.join(`
|
|
29104
|
-
`)}` : doneMessage;
|
|
29105
29322
|
}
|
|
29106
29323
|
async function setUnattendedExecute(params, ctx) {
|
|
29107
29324
|
assertOrchestrator(ctx, "opx_orch_set_unattended");
|
|
@@ -29914,13 +30131,13 @@ var orchInitSchema = {
|
|
|
29914
30131
|
},
|
|
29915
30132
|
base_branch: {
|
|
29916
30133
|
type: "string",
|
|
29917
|
-
description: "基准分支名(如 main、develop
|
|
30134
|
+
description: "基准分支名(如 main、develop),作为 change 分支(change/{changeId})的 fork 源与收口合并目标。未传则自动从当前 git 分支推导。仅 change 会话入口有效。"
|
|
29918
30135
|
},
|
|
29919
30136
|
recovery: recoverySchema,
|
|
29920
30137
|
mode: {
|
|
29921
30138
|
type: "string",
|
|
29922
30139
|
enum: ["full", "simple"],
|
|
29923
|
-
description: "流程模式选择(仅 change 会话入口有效,独立审查会话不使用):full=完整流程(analyze→implement→三重审查+收尾验证);simple=精简流程(implement→quality_review→done
|
|
30140
|
+
description: "流程模式选择(仅 change 会话入口有效,独立审查会话不使用):full=完整流程(analyze→implement→三重审查+收尾验证);simple=精简流程(implement→quality_review→done,verify_cleanup 仅作收口漂移/冲突回退落点,正常流转不经过,缺省)。首次新建编排状态时固化;已开始的变更仅在允许窗口内可更新:切换任务组(其他任务组均已完成或从未激活)或 recovery.phase=task_analysis 重制当前组(其他任务组同样须已完成或从未激活);其余场景传不同 mode 将报错。"
|
|
29924
30141
|
},
|
|
29925
30142
|
review_scope: {
|
|
29926
30143
|
type: "object",
|
|
@@ -29962,8 +30179,8 @@ var setWorktreeSchema = {
|
|
|
29962
30179
|
type: "object",
|
|
29963
30180
|
properties: {
|
|
29964
30181
|
change_id: { type: "string", minLength: 1, description: "change ID" },
|
|
29965
|
-
worktree_path: { type: "string", description: "git worktree
|
|
29966
|
-
branch_name: { type: "string", description: "worktree
|
|
30182
|
+
worktree_path: { type: "string", description: "git worktree 的绝对路径(可选,不传则按规范自动生成 .worktree/{changeId}/ws)" },
|
|
30183
|
+
branch_name: { type: "string", description: "worktree 对应的分支名(可选,不传则按 change 模型确定性派生:change/{changeId},整个 change 串行复用同一分支与常驻 worktree)" }
|
|
29967
30184
|
},
|
|
29968
30185
|
required: ["change_id"],
|
|
29969
30186
|
additionalProperties: false
|
|
@@ -30152,7 +30369,7 @@ var TOOL_SPECS = {
|
|
|
30152
30369
|
execute: (args, ctx) => initExecute(args, ctx)
|
|
30153
30370
|
},
|
|
30154
30371
|
opx_orch_set_worktree: {
|
|
30155
|
-
description: "确保目标组的 git worktree
|
|
30372
|
+
description: "确保目标组的 git worktree 就绪。change 会话按 change 模型 create-or-reuse:分支 change/{changeId} 不存在时从基准分支 tip 创建,worktree 常驻于 .worktree/{changeId}/ws,全部任务组串行复用(复用校验:目录缺失自愈重建、openspec 文档脏自动提交、代码文件脏拒绝)。只补齐资源,不改变阶段。",
|
|
30156
30373
|
schema: setWorktreeSchema,
|
|
30157
30374
|
execute: (args, ctx) => setWorktreeExecute(args, ctx)
|
|
30158
30375
|
},
|
|
@@ -30162,7 +30379,7 @@ var TOOL_SPECS = {
|
|
|
30162
30379
|
execute: (args, ctx) => statusExecute({ change_id: args.change_id }, ctx)
|
|
30163
30380
|
},
|
|
30164
30381
|
opx_orch_complete_task_group: {
|
|
30165
|
-
description: "
|
|
30382
|
+
description: "完成任务组收尾。非最后任务组仅做门禁与范围标记(不合并、不销毁);最后一个任务组收口时把 change 分支(change/{changeId})一次性合并回 baseBranch(漂移含任一非文档文件或文本冲突时回退到收尾验证 verify_cleanup 并返回 blocked,纯文档漂移直接合并收口),成功后销毁 worktree 并删分支。须在收尾验证(verify_cleanup)通过后调用。主仓库本地改动文件与合并写入文件重合、或存在部分暂存文件时中止并返回 blocked(保留 worktree/分支);主仓库无关脏文件不阻塞合并。",
|
|
30166
30383
|
schema: completeTaskGroupSchema,
|
|
30167
30384
|
execute: (args, ctx) => completeTaskGroupExecute(args, ctx)
|
|
30168
30385
|
},
|
|
@@ -30207,7 +30424,7 @@ async function ensureDefaultUnattended(args, ctx) {
|
|
|
30207
30424
|
}
|
|
30208
30425
|
} catch {}
|
|
30209
30426
|
}
|
|
30210
|
-
var PKG_VERSION = "0.
|
|
30427
|
+
var PKG_VERSION = "0.138.0";
|
|
30211
30428
|
function buildMcpServer(worktree, opts = {}) {
|
|
30212
30429
|
const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION });
|
|
30213
30430
|
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 分支。合并前若基准分支已推进,先做基线漂移检查并比对基准侧净变化内容:纯文档变化(md / mdx / markdown / rst / adoc 五类后缀)不影响代码语义,直接合并收口;含任一非文档文件或文本冲突才把该任务组回退到收尾验证环节(合并基准分支最新代码并回归复验)后再收口——验收仅在实质漂移时动态发生,正常路径零额外环节。
|
|
96
|
+
5. **部署注意事项**:change 的全部任务组完成后,主智能体按状态视图指引汇总部署注意事项(规划文档提取 + 开发者实施补充点,含依赖清单/构建配置变更等合并后需在主仓库重装依赖、重新构建的事项)向用户汇报。全程留痕可审计。
|
|
96
97
|
|
|
97
98
|
## 独立审查会话
|
|
98
99
|
|