@ifrankwang/openspec-agents 0.135.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.
@@ -4056,13 +4056,14 @@ var require_fast_uri = __commonJS(function(exports, module) {
4056
4056
  if (!malformedIPLiteral) {
4057
4057
  malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
4058
4058
  }
4059
- if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4060
- if (uri.indexOf("%") !== -1) {
4061
- if (parsed.host !== undefined && !malformedIPLiteral) {
4062
- const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
4063
- parsed.host = reescapeHostDelimiters(host, isIP);
4064
- }
4059
+ if (uri.indexOf("%") !== -1 && parsed.host !== undefined && !malformedIPLiteral) {
4060
+ let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
4061
+ if (!isIP) {
4062
+ host = normalizePercentEncoding(host.toLowerCase());
4065
4063
  }
4064
+ parsed.host = reescapeHostDelimiters(host, isIP);
4065
+ }
4066
+ if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4066
4067
  if (parsed.path) {
4067
4068
  parsed.path = normalizePathEncoding(parsed.path);
4068
4069
  }
@@ -21662,6 +21663,14 @@ async function getCurrentBranch(worktree) {
21662
21663
  async function getMergeBase(worktree, baseBranch) {
21663
21664
  return runGit(worktree, ["merge-base", "HEAD", baseBranch]);
21664
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
+ }
21665
21674
  async function isLocalBranch(worktree, branch) {
21666
21675
  const r = await runGitChecked(worktree, ["rev-parse", "--verify", `refs/heads/${branch}`]);
21667
21676
  return r.success;
@@ -21798,7 +21807,7 @@ async function mergeBranchToTarget(repoDir, sourceBranch, targetBranch) {
21798
21807
  success: false,
21799
21808
  conflict: false,
21800
21809
  blockedMessage: [
21801
- `- **原因**: 目标分支 \`${targetBranch}\` 正被主仓库检出,且以下本地改动文件与任务组合并将写入的文件重合;为避免覆盖本地改动,本次未执行任何合并动作(分支引用未动、无半成品):`,
21810
+ `- **原因**: 目标分支 \`${targetBranch}\` 正被主仓库检出,且以下本地改动文件与变更分支合并将写入的文件重合;为避免覆盖本地改动,本次未执行任何合并动作(分支引用未动、无半成品):`,
21802
21811
  ...overlap.map((f) => ` - \`${f}\``),
21803
21812
  `- **处理**: 请先 commit/stash/移除上述文件的本地改动后重试;或自行执行 \`git merge ${sourceBranch}\`(真实合并自带脏工作区保护)。`
21804
21813
  ].join(`
@@ -21882,6 +21891,7 @@ async function mergeBranchToTarget(repoDir, sourceBranch, targetBranch) {
21882
21891
  }
21883
21892
  return { success: true, conflict: false };
21884
21893
  }
21894
+ var RESTORABLE_WORKTREE_BRANCH_PREFIXES = ["change/", "review/", "task-group/"];
21885
21895
  async function discoverDiskWorktrees(worktree) {
21886
21896
  const result = [];
21887
21897
  const wtList = await runGit(worktree, ["worktree", "list"]);
@@ -21890,7 +21900,7 @@ async function discoverDiskWorktrees(worktree) {
21890
21900
  const m = line.match(/^(\S+)\s+[0-9a-f]+\s+\[(.+?)\]/);
21891
21901
  if (m) {
21892
21902
  const branch = m[2].trim();
21893
- if (branch.startsWith("task-group/") || branch.startsWith("review/")) {
21903
+ if (RESTORABLE_WORKTREE_BRANCH_PREFIXES.some((p) => branch.startsWith(p))) {
21894
21904
  result.push({ branch, path: m[1].trim() });
21895
21905
  }
21896
21906
  }
@@ -22194,6 +22204,9 @@ function isTaskGroupSettled(item) {
22194
22204
  const childrenSettled = item.children.filter((c) => c.type === "task").every((c) => c.phase === "todo" || isTerminalPhase(c.phase));
22195
22205
  return noTags && childrenSettled;
22196
22206
  }
22207
+ function isFinalTaskGroup(state, item) {
22208
+ return state.workItems.filter((w) => w.id.startsWith("task:") && w.id !== item.id).every(isTaskGroupSettled);
22209
+ }
22197
22210
  function isBlockingSeverity(severity) {
22198
22211
  return severity !== undefined && BLOCKING_SEVERITIES.includes(severity);
22199
22212
  }
@@ -27031,7 +27044,7 @@ function renderWorktreeSection(state, tg, opts) {
27031
27044
  lines.push("- **审查范围**: 全量代码库审查(无区间界定,工具检查与审查覆盖整个代码库)");
27032
27045
  }
27033
27046
  } else if (tg.baseRef) {
27034
- lines.push(`- **变更范围**: 用 \`git -C ${tg.worktreePath} diff --name-only ${tg.baseRef}..HEAD\` 查询本 change 全部已提交变更文件`);
27047
+ lines.push(`- **变更范围**: 用 \`git -C ${tg.worktreePath} diff --name-only ${tg.baseRef}..HEAD\` 查询本任务组的已提交变更文件`);
27035
27048
  }
27036
27049
  lines.push("- **⚠️ 约束**: 所有读写和 git 操作均在此目录下进行;严禁直接修改主仓库/主分支路径下的文件(如 `<repo>/openspec/...`)");
27037
27050
  lines.push("- **路径解析**: 推荐阅读文档均为相对 worktree 路径的引用,一律以 worktree 路径为基准解析,禁止从主仓库根目录解析");
@@ -27347,13 +27360,14 @@ function renderTerminalPhase(item, state, caller) {
27347
27360
  return lines.join(`
27348
27361
  `);
27349
27362
  }
27363
+ const finalGroup = state ? isFinalTaskGroup(state, item) : true;
27350
27364
  if (item.metadata["completed_at"] !== undefined) {
27351
27365
  return [
27352
27366
  "# ✅ 任务组已完成",
27353
27367
  "",
27354
27368
  `- **完成时间**: ${item.metadata["completed_at"]}`,
27355
27369
  "",
27356
- "编排已完成并收尾。",
27370
+ finalGroup ? "本任务组为最后一个收口任务组,change 收口已完成:变更分支已合并回基准分支,编排已完成并收尾。" : "本任务组已完成,变更保留在 change 分支上,待全部任务组完成后统一收口合并。",
27357
27371
  "",
27358
27372
  ...renderDeploymentNotesSection(state, caller)
27359
27373
  ].join(`
@@ -27362,7 +27376,8 @@ function renderTerminalPhase(item, state, caller) {
27362
27376
  return [
27363
27377
  "# \uD83C\uDFC1 任务组已完成,待收尾",
27364
27378
  "",
27365
- "全部审核层已通过。调用 `opx_orch_complete_task_group` 合并分支并完成收尾。",
27379
+ "全部审核层已通过。",
27380
+ finalGroup ? "调用 `opx_orch_complete_task_group` 完成 change 收口(把变更分支合并回基准分支并清理)。" : "调用 `opx_orch_complete_task_group` 完成任务组收尾(变更保留在 change 分支,统一收口时合并)。",
27366
27381
  "",
27367
27382
  ...renderDeploymentNotesSection(state, caller)
27368
27383
  ].join(`
@@ -27379,7 +27394,7 @@ function renderDeploymentNotesSection(state, caller) {
27379
27394
  "",
27380
27395
  "向用户汇报本次变更的部署注意事项:",
27381
27396
  "",
27382
- `- 阅读主仓库 \`openspec/changes/${state.changeId}/\` 下规划文档(design.md、tasks.md、proposal.md、specs/)提取部署要点:数据/结构变更脚本执行、消息队列/缓存等中间件配置调整、新增配置项或功能开关、外部接口契约变化`,
27397
+ `- 阅读主仓库 \`openspec/changes/${state.changeId}/\` 下规划文档(design.md、tasks.md、proposal.md、specs/)提取部署要点:数据/结构变更脚本执行、消息队列/缓存等中间件配置调整、新增配置项或功能开关、外部接口契约变化、依赖清单/构建配置变更(合并后需在主仓库重装依赖、重新构建)`,
27383
27398
  "- 无规划文档时跳过文档提取,仅转述下方实施补充点",
27384
27399
  ""
27385
27400
  ];
@@ -27763,7 +27778,7 @@ function renderAgentWorking(item, rec, step, common, ctxAgent, state, tg, toolCh
27763
27778
  lines.push(...eff.lines);
27764
27779
  n = eff.nextNum;
27765
27780
  if (tg.worktreePath && tg.baseRef) {
27766
- lines.push(`${n++}. 用上方「变更范围」命令获取本 change 全部已提交变更文件清单,作为本次实施/修改范围;审查/报告范围以当前 step 指令为准`);
27781
+ lines.push(`${n++}. 用上方「变更范围」命令获取本任务组的已提交变更文件清单,作为本次实施/修改范围;审查/报告范围以当前 step 指令为准`);
27767
27782
  }
27768
27783
  lines.push(`${n++}. 执行当前 step(\`${rec.stepId}\`)职责范围内的全部工作——遵循所有已加载 skill 的全部规范与约束`);
27769
27784
  lines.push(`${n++}. 逐项检视所有已加载 skill 的 MUST 规范,确认全部满足(不满足则补做,不得跳过)`);
@@ -27976,7 +27991,7 @@ function renderToolChangesEvidence(item, tg, toolChanges, kind) {
27976
27991
  `)];
27977
27992
  }
27978
27993
  if (checkpoint) {
27979
- lines.push(`- **口径**: 本次为「${label.title}(${checkpoint}..HEAD)」的增量区间(含已提交与未提交的非 openspec 变更);` + `与上方「变更范围」(${baseRef ?? "(无)"}..HEAD 整个 change 累计口径)不同`, "");
27994
+ lines.push(`- **口径**: 本次为「${label.title}(${checkpoint}..HEAD)」的增量区间(含已提交与未提交的非 openspec 变更);` + `与上方「变更范围」(${baseRef ?? "(无)"}..HEAD 本任务组累计口径)不同`, "");
27980
27995
  } else if (baseRef) {
27981
27996
  lines.push(`- **口径**: 本次区间以基线(${baseRef}..HEAD)兜底,与上方「变更范围」一致(首次进入,无${label.lastCheck}记录;` + "含已提交与未提交的非 openspec 变更)", "");
27982
27997
  } else {
@@ -28312,7 +28327,16 @@ function applyRecoveryState(item, recovery, parsedTasks, state) {
28312
28327
  if ((recovery?.reset_steps ?? []).includes("quality_review")) {
28313
28328
  clearStepTags(item, "quality_review");
28314
28329
  }
28315
- item.currentStep = "quality_review";
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
+ }
28316
28340
  syncTaskChildren(item, parsedTasks, { defaultStatus: "done" });
28317
28341
  return;
28318
28342
  }
@@ -28797,17 +28821,75 @@ async function bindReviewWorktreeRefs(item, worktreePath, branch, state) {
28797
28821
  }
28798
28822
  delete item.metadata["base_ref"];
28799
28823
  }
28800
- async function bindWorktreeRefs(item, worktreePath, branch, baseBranch, opts = {}) {
28824
+ async function bindWorktreeRefs(item, worktreePath, branch) {
28801
28825
  item.metadata["worktree_path"] = worktreePath;
28802
28826
  item.metadata["branch_name"] = branch;
28803
- const baseRef = await getMergeBase(worktreePath, baseBranch);
28804
- if (!baseRef) {
28805
- if (opts.requireBaseRef) {
28806
- throw new Error(`worktree 创建成功但无法获取与 ${baseBranch} 的 merge-base:${worktreePath}`);
28807
- }
28808
- return;
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;
28833
+ }
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}`);
28809
28890
  }
28810
- item.metadata["base_ref"] = baseRef;
28891
+ await bindWorktreeRefs(item, targetPath, branch);
28892
+ return { path: targetPath, reused: true };
28811
28893
  }
28812
28894
  async function setWorktreeExecute(params, ctx) {
28813
28895
  assertOrchestrator(ctx, "opx_orch_set_worktree");
@@ -28818,6 +28900,7 @@ async function setWorktreeExecute(params, ctx) {
28818
28900
  if (!item)
28819
28901
  throw new Error(`工作项 "task:${state.taskGroupId}" 缺失,请重新调用 opx_orch_init。`);
28820
28902
  const repoRoot = ctx.worktree;
28903
+ const isReview = state.kind === "review";
28821
28904
  const rawBranch = params.branch_name ?? "";
28822
28905
  if (rawBranch !== "") {
28823
28906
  const check = await runGitChecked(repoRoot, ["check-ref-format", "--branch", rawBranch]);
@@ -28825,13 +28908,18 @@ async function setWorktreeExecute(params, ctx) {
28825
28908
  throw new Error(`分支名 "${rawBranch}" 不合法,请修正后重试。`);
28826
28909
  }
28827
28910
  }
28828
- const isReview = state.kind === "review";
28829
- const branch = rawBranch || (isReview ? `review/${state.changeId}` : `task-group/${state.changeId}/${state.taskGroupId}`);
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
+ }
28830
28918
  let wtPath;
28831
28919
  if (params.worktree_path) {
28832
28920
  wtPath = assertPathWithin(repoRoot, params.worktree_path, "worktree_path");
28833
28921
  } else {
28834
- wtPath = isReview ? path6.join(repoRoot, ".worktree", state.changeId, "review") : path6.join(repoRoot, ".worktree", state.changeId, `task-group-${state.taskGroupId}`);
28922
+ wtPath = isReview ? path6.join(repoRoot, ".worktree", state.changeId, "review") : path6.join(repoRoot, ".worktree", state.changeId, "ws");
28835
28923
  }
28836
28924
  const changeStatus = await runGit(repoRoot, ["status", "--porcelain", `openspec/changes/${state.changeId}/`]);
28837
28925
  if (changeStatus.trim().length > 0) {
@@ -28842,61 +28930,57 @@ async function setWorktreeExecute(params, ctx) {
28842
28930
  if (!commitResult.success)
28843
28931
  throw new Error(`change 目录 git commit 失败:${commitResult.stderr}`);
28844
28932
  }
28845
- const wtList = await runGit(repoRoot, ["worktree", "list"]);
28846
- const existingLine = wtList.split(`
28847
- `).find((l) => {
28848
- const m = l.match(/^(\S+)\s+[0-9a-f]+\s+\[(.+?)\]/);
28849
- return m && m[2].trim() === branch;
28850
- });
28851
- const existingPath = existingLine ? existingLine.match(/^(\S+)/)?.[1] : undefined;
28852
28933
  let reused = false;
28853
- if (existingPath) {
28854
- const baseHead = await runGit(repoRoot, ["rev-parse", state.baseBranch]);
28855
- const mergeResult = await runGitChecked(existingPath, ["merge", "--ff-only", baseHead]);
28856
- if (mergeResult.success) {
28857
- if (isReview)
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) {
28858
28946
  await bindReviewWorktreeRefs(item, existingPath, branch, state);
28859
- else
28860
- await bindWorktreeRefs(item, existingPath, branch, state.baseBranch);
28861
- reused = true;
28862
- } else {
28863
- const clean = await isWorktreeClean(existingPath);
28864
- if (!clean) {
28865
- throw new Error(`已有 worktree "${existingPath}" 与 ${state.baseBranch} 分叉且有未提交变更,无法自动 fast-forward。
28866
- ` + `请手动处理后重试。`);
28867
- }
28868
- const localCommitCount = parseInt(await runGit(existingPath, ["rev-list", "--count", `${state.baseBranch}..HEAD`]), 10);
28869
- if (localCommitCount > 0 || Number.isNaN(localCommitCount)) {
28870
- if (isReview)
28871
- await bindReviewWorktreeRefs(item, existingPath, branch, state);
28872
- else
28873
- await bindWorktreeRefs(item, existingPath, branch, state.baseBranch);
28874
28947
  reused = true;
28875
28948
  } else {
28876
- const cleanup = await removeTaskGroupWorktree(repoRoot, existingPath, { branchName: branch });
28877
- if (!cleanup.dirResolved || cleanup.branchDeleted === false) {
28878
- const problems = [];
28879
- if (!cleanup.dirResolved)
28880
- problems.push(`无法清理已有 worktree "${existingPath}"`);
28881
- if (cleanup.branchDeleted === false)
28882
- problems.push(`无法清理已有分支 "${branch}"`);
28883
- throw new Error(`${problems.join(";")}:${cleanup.errors.join(";") || "原因未知"}
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(";") || "原因未知"}
28884
28967
  请手动处理后重试。`);
28968
+ }
28885
28969
  }
28886
28970
  }
28887
28971
  }
28888
- }
28889
- if (!reused) {
28890
- const forkBranch = state.baseBranch;
28891
- await runGit(repoRoot, ["worktree", "add", "-b", branch, wtPath, forkBranch]);
28892
- if (isReview) {
28972
+ if (!reused) {
28973
+ const forkBranch = state.baseBranch;
28974
+ await runGit(repoRoot, ["worktree", "add", "-b", branch, wtPath, forkBranch]);
28893
28975
  await bindReviewWorktreeRefs(item, wtPath, branch, state);
28894
28976
  if (state.reviewScope?.scopeType === "pr" && !item.metadata["base_ref"]) {
28895
28977
  throw new Error(`worktree 创建成功但无法获取 ${state.reviewScope.baseRef} 与 ${state.baseBranch} 的 merge-base:${wtPath}`);
28896
28978
  }
28897
- } else {
28898
- await bindWorktreeRefs(item, wtPath, branch, forkBranch, { requireBaseRef: true });
28899
28979
  }
28980
+ } else {
28981
+ const ensured = await ensureChangeWorktree(repoRoot, state, item, branch, wtPath);
28982
+ wtPath = ensured.path;
28983
+ reused = ensured.reused;
28900
28984
  }
28901
28985
  await writeState(ctx.worktree, state);
28902
28986
  const storedPath = typeof item.metadata["worktree_path"] === "string" ? item.metadata["worktree_path"] : null;
@@ -28997,8 +29081,43 @@ function renderReviewIssueSummary(item) {
28997
29081
  return lines.join(`
28998
29082
  `);
28999
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
+ }
29000
29110
  async function completeTaskGroupExecute(params, ctx) {
29001
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) {
29002
29121
  const state = await readStateByWorktree(ctx.worktree, params.change_id);
29003
29122
  if (!state)
29004
29123
  throw new Error("编排会话未初始化。请先调用 opx_orch_init。");
@@ -29017,7 +29136,8 @@ async function completeTaskGroupExecute(params, ctx) {
29017
29136
  if (!clean)
29018
29137
  throw new Error(`worktree "${worktreePath}" 存在未 commit 内容,请先 commit 再完成任务组。`);
29019
29138
  }
29020
- const isReviewNone = state.kind === "review" && state.reviewScope?.fix === "none";
29139
+ const isReview = state.kind === "review";
29140
+ const isReviewNone = isReview && state.reviewScope?.fix === "none";
29021
29141
  const openIssues = item.children.filter((c) => isBlockingSeverity(c.severity) && !isTerminalPhase(c.phase));
29022
29142
  if (!isReviewNone && openIssues.length > 0) {
29023
29143
  throw new Error(`存在 ${openIssues.length} 个 Low 及以上的未解决 issue 未处理,请先修复或申请豁免。`);
@@ -29039,6 +29159,91 @@ async function completeTaskGroupExecute(params, ctx) {
29039
29159
  checkboxWarning = `- **tasks.md 复选框勾选失败**: ${e instanceof Error ? e.message : String(e)}(不影响收尾)`;
29040
29160
  }
29041
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;
29042
29247
  const mergeTarget = state.baseBranch;
29043
29248
  if (branchName && !isReviewNone) {
29044
29249
  const mergeResult = await mergeBranchToTarget(ctx.worktree, branchName, mergeTarget);
@@ -29088,19 +29293,13 @@ async function completeTaskGroupExecute(params, ctx) {
29088
29293
  item.metadata["completed_at"] = new Date().toISOString();
29089
29294
  await writeState(ctx.worktree, state);
29090
29295
  const notes = [cleanupNote, checkboxWarning, cleanupResidualWarning].filter(Boolean);
29091
- if (state.kind === "review") {
29092
- const summary = renderReviewIssueSummary(item);
29093
- const doneMessage = isReviewNone ? `独立审查会话已完成(只审模式,未合并,worktree 与分支已销毁)。` : `独立审查会话已完成并合并到 "${mergeTarget}"。`;
29094
- return notes.length > 0 ? `${doneMessage}
29296
+ const summary = renderReviewIssueSummary(item);
29297
+ const doneMessage = isReviewNone ? `独立审查会话已完成(只审模式,未合并,worktree 与分支已销毁)。` : `独立审查会话已完成并合并到 "${mergeTarget}"。`;
29298
+ return notes.length > 0 ? `${doneMessage}
29095
29299
  ${notes.join(`
29096
29300
  `)}
29097
29301
  ${summary}` : `${doneMessage}
29098
29302
  ${summary}`;
29099
- }
29100
- const doneMessage = `任务组已完成并合并到 "${mergeTarget}"。`;
29101
- return notes.length > 0 ? `${doneMessage}
29102
- ${notes.join(`
29103
- `)}` : doneMessage;
29104
29303
  }
29105
29304
  async function setUnattendedExecute(params, ctx) {
29106
29305
  assertOrchestrator(ctx, "opx_orch_set_unattended");
@@ -29913,13 +30112,13 @@ var orchInitSchema = {
29913
30112
  },
29914
30113
  base_branch: {
29915
30114
  type: "string",
29916
- description: "基准分支名(如 main、develop),用于计算 merge-base 和 worktree fork 源。未传则自动从当前 git 分支推导。仅 change 会话入口有效。"
30115
+ description: "基准分支名(如 main、develop),作为 change 分支(change/{changeId})的 fork 源与收口合并目标。未传则自动从当前 git 分支推导。仅 change 会话入口有效。"
29917
30116
  },
29918
30117
  recovery: recoverySchema,
29919
30118
  mode: {
29920
30119
  type: "string",
29921
30120
  enum: ["full", "simple"],
29922
- description: "流程模式选择(仅 change 会话入口有效,独立审查会话不使用):full=完整流程(analyze→implement→三重审查+收尾验证);simple=精简流程(implement→quality_review→done,缺省)。首次新建编排状态时固化;已开始的变更仅在允许窗口内可更新:切换任务组(其他任务组均已完成或从未激活)或 recovery.phase=task_analysis 重制当前组(其他任务组同样须已完成或从未激活);其余场景传不同 mode 将报错。"
30121
+ description: "流程模式选择(仅 change 会话入口有效,独立审查会话不使用):full=完整流程(analyze→implement→三重审查+收尾验证);simple=精简流程(implement→quality_review→done,verify_cleanup 仅作收口漂移/冲突回退落点,正常流转不经过,缺省)。首次新建编排状态时固化;已开始的变更仅在允许窗口内可更新:切换任务组(其他任务组均已完成或从未激活)或 recovery.phase=task_analysis 重制当前组(其他任务组同样须已完成或从未激活);其余场景传不同 mode 将报错。"
29923
30122
  },
29924
30123
  review_scope: {
29925
30124
  type: "object",
@@ -29961,8 +30160,8 @@ var setWorktreeSchema = {
29961
30160
  type: "object",
29962
30161
  properties: {
29963
30162
  change_id: { type: "string", minLength: 1, description: "change ID" },
29964
- worktree_path: { type: "string", description: "git worktree 的绝对路径(可选,不传则按规范自动生成)" },
29965
- branch_name: { type: "string", description: "worktree 对应的分支名(可选,不传则按规范 task-group/{changeId}/{taskGroupId})" }
30163
+ worktree_path: { type: "string", description: "git worktree 的绝对路径(可选,不传则按规范自动生成 .worktree/{changeId}/ws)" },
30164
+ branch_name: { type: "string", description: "worktree 对应的分支名(可选,不传则按 change 模型确定性派生:change/{changeId},整个 change 串行复用同一分支与常驻 worktree)" }
29966
30165
  },
29967
30166
  required: ["change_id"],
29968
30167
  additionalProperties: false
@@ -30151,7 +30350,7 @@ var TOOL_SPECS = {
30151
30350
  execute: (args, ctx) => initExecute(args, ctx)
30152
30351
  },
30153
30352
  opx_orch_set_worktree: {
30154
- description: "确保目标组的 git worktree 就绪。若已存在则复用,否则按规范自动创建(分支 task-group/{changeId}/{taskGroupId},路径 .worktree/{changeId}/task-group-{taskGroupId})。只补齐资源,不改变阶段。",
30353
+ description: "确保目标组的 git worktree 就绪。change 会话按 change 模型 create-or-reuse:分支 change/{changeId} 不存在时从基准分支 tip 创建,worktree 常驻于 .worktree/{changeId}/ws,全部任务组串行复用(复用校验:目录缺失自愈重建、openspec 文档脏自动提交、代码文件脏拒绝)。只补齐资源,不改变阶段。",
30155
30354
  schema: setWorktreeSchema,
30156
30355
  execute: (args, ctx) => setWorktreeExecute(args, ctx)
30157
30356
  },
@@ -30161,7 +30360,7 @@ var TOOL_SPECS = {
30161
30360
  execute: (args, ctx) => statusExecute({ change_id: args.change_id }, ctx)
30162
30361
  },
30163
30362
  opx_orch_complete_task_group: {
30164
- description: "完成任务组收尾:合并 task-group 分支到 baseBranch → 清理 worktree 与分支。须在收尾验证(verify_cleanup)通过后调用。合并冲突、主仓库本地改动文件与合并写入文件重合、或存在部分暂存文件时中止并返回 blocked(保留 worktree/分支);主仓库无关脏文件不阻塞合并。",
30363
+ description: "完成任务组收尾。非最后任务组仅做门禁与范围标记(不合并、不销毁);最后一个任务组收口时把 change 分支(change/{changeId})一次性合并回 baseBranch(基准分支漂移或文本冲突时回退到收尾验证 verify_cleanup 并返回 blocked),成功后销毁 worktree 并删分支。须在收尾验证(verify_cleanup)通过后调用。主仓库本地改动文件与合并写入文件重合、或存在部分暂存文件时中止并返回 blocked(保留 worktree/分支);主仓库无关脏文件不阻塞合并。",
30165
30364
  schema: completeTaskGroupSchema,
30166
30365
  execute: (args, ctx) => completeTaskGroupExecute(args, ctx)
30167
30366
  },
@@ -30206,7 +30405,7 @@ async function ensureDefaultUnattended(args, ctx) {
30206
30405
  }
30207
30406
  } catch {}
30208
30407
  }
30209
- var PKG_VERSION = "0.135.0";
30408
+ var PKG_VERSION = "0.137.0";
30210
30409
  function buildMcpServer(worktree, opts = {}) {
30211
30410
  const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION });
30212
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. **收尾**:变更完成后自动合并分支并清理执行环境;change 的全部任务组完成后,主智能体按状态视图指引汇总部署注意事项(规划文档提取 + 开发者实施补充点)向用户汇报。全程留痕可审计。
95
+ 4. **收尾合并**:任务组完成只做门禁校验并标记变更范围,不合并、不销毁 worktree;最后一个任务组收口时把 change 分支一次性合并回基准分支,随后销毁 worktree 并删除 change 分支。合并前若基准分支已推进,先做基线漂移检查,发现漂移或冲突则把该任务组回退到收尾验证环节(合并基准分支最新代码并回归复验)后再收口——验收仅在漂移时动态发生,正常路径零额外环节。
96
+ 5. **部署注意事项**:change 的全部任务组完成后,主智能体按状态视图指引汇总部署注意事项(规划文档提取 + 开发者实施补充点,含依赖清单/构建配置变更等合并后需在主仓库重装依赖、重新构建的事项)向用户汇报。全程留痕可审计。
96
97
 
97
98
  ## 独立审查会话
98
99
 
@@ -15,7 +15,7 @@ rules:
15
15
  - "关联场景清点(强制):proposal 动笔前必须完成「关联场景清点」并以代码检索为证据记入 clarify.md 的「关联场景清点(Related-Surface Survey)」小节:枚举本变更触及能力的全部消费方与关联面——功能入口(API 端点、导入/导出入口、定时任务、消息消费、前端调用等)、共享处理逻辑(被多个入口复用的处理链路)、数据模型与数据流下游(表/字段/事件/导出产物的读写双方),每条附代码位置证据(文件/类/方法)。凡共享处理逻辑发生变更,其全部消费入口必须在同一 change 内统一适配,禁止按入口分别提 change 导致共享逻辑两边割裂维护;确需拆分时必须在 proposal.md 单列「拆分边界」小节论证(每个 change 覆盖哪些入口、共享逻辑变更归属哪个 change、入口间契约),并经用户确认。proposal.md 必须单列「关联场景(Related Surface)」小节逐条列出清点结论与划界结论;泛写「无关联影响」而不附检索证据视为 proposal 未完成。"
16
16
  - "契约变更详列(强制):凡变更涉及 DDL(建表/加列/索引/约束)或接口契约(新增/修改端点、请求响应结构、错误码),proposal.md 必须单列「契约变更点」小节逐条列出——DDL 逐表逐列给全表名、列名、类型、可空性、默认值与用途;API 逐端点给全方法+路径、请求/响应结构变化、新增/修改标记与兼容性影响。泛写「有契约变更」不逐条展开视为 proposal 未完成。"
17
17
  - "动笔前访谈门(强制):proposal/specs/design/tasks 任一文档动笔前,必须已按 spec-driven-standard schema clarify 指令的决策树访谈方法论完成对用户的访谈——把方案建模为决策树逐轮穷尽疑点:每轮将前提已落定、当前可回答的问题一次性编号抛出并逐题附推荐答案,用户答复后重算下一轮前沿,直到决策树所有分支走完、无未决疑点、无静默假设;访谈问答全程记入 clarify.md(Open Questions / User Answers / 关联场景清点)。事实类问题必须自行查代码与 spec 求证,不抛给用户;疑点未穷尽时禁止写任何文档,不得以「需求简单」等理由跳过。"
18
- - "动笔前用户确认门(强制):proposal/specs/design/tasks 任一文档动笔前,clarify.md 的 Architecture Direction 必须已记录用户对方案摘要的明确确认;未记录时禁止写任何文档——主代理亲自写时先停下向用户补确认,生成子代理则立即终止并向编排方报告,不得以「需求简单」等理由跳过或代用户确认。"
18
+ - "动笔前用户确认门(强制):proposal/specs/design/tasks 任一文档动笔前,clarify.md 的 Architecture Direction 必须已记录用户对方案摘要的明确确认;确认必须以普通消息直接列出方案摘要要素点(要素清单见 spec-driven-standard schema clarify 指令的 Plan confirmation gate)并停下等待用户直接文字回复,禁止通过提问工具发起确认,提问工具的选项点击不视为确认;未记录时禁止写任何文档——主代理亲自写时先停下向用户补确认,生成子代理则立即终止并向编排方报告,不得以「需求简单」等理由跳过或代用户确认。"
19
19
  specs:
20
20
  - "数据行为需求(如\"系统 SHALL 持久化 X 含审计字段\")放 spec;物理 DDL 放 design.md。"
21
21
  - "MODIFIED 的需求描述直接给出变更后的终态契约表述(\"系统 SHALL 使用 Y\"),供归档合并时原样落入主 spec;禁止以\"原 X 改为 Y\"的历史对比充当主 spec 内容。"
@@ -6,8 +6,9 @@
6
6
  (枚举变更触及能力的全部消费方——功能入口 / 共享处理逻辑 / 数据流下游,
7
7
  附代码证据;共享处理逻辑变更须与其全部消费入口同一 change 统一适配);
8
8
  访谈结束后、写任何文档之前,主代理必须先向用户简单直接汇报方案(一句话
9
- 讲清做什么 + 是否动契约 + 取舍 + 影响面)并取得用户明确确认(HARD-GATE,
10
- 访谈中的问答不视为确认),确认记录落在 clarify.md,后续文档不得偏离。
9
+ 讲清做什么 + 是否动契约 + 取舍 + 影响面)并取得用户明确确认(HARD-GATE:
10
+ 汇报须以普通消息直接列出要素点,禁止用提问工具发起确认,访谈中的问答
11
+ 不视为确认),确认记录落在 clarify.md,后续文档不得偏离。
11
12
  2. **主代理访谈直录 + 分级撰写 + 独立复核循环**:主代理亲自访谈,问答
12
13
  直接记入 clarify.md(访谈阶段唯一落笔的文档,不设独立访谈文件);
13
14
  架构方向经用户确认后,按复杂度分级产出其余文档(proposal → specs →