@ifrankwang/openspec-agents 0.127.0 → 0.130.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/src/core/git.ts CHANGED
@@ -371,3 +371,64 @@ export async function reconcileMainPollution(
371
371
  }
372
372
  }
373
373
  }
374
+
375
+ /** worktree 自动提交(reviewer 家族文档直改兜底)结果。 */
376
+ export interface AutoCommitResult {
377
+ /** skipped:过滤 openspec/states/ 后无已跟踪文件变更(未触发提交);committed:add -u + commit 完成;
378
+ * failed:git 调用失败(降级返回,不抛异常)。 */
379
+ status: "skipped" | "committed" | "failed"
380
+ /** 纳入自动提交的已跟踪文件变更路径(skipped 时为空)。 */
381
+ files: string[]
382
+ /** status=failed 时的失败 stderr。 */
383
+ stderr?: string
384
+ /** 未跟踪新建文件(?? 条目)路径清单:add -u 不纳入提交,提示编排者另行处理。 */
385
+ untrackedFiles: string[]
386
+ }
387
+
388
+ /**
389
+ * 自动提交 worktree 内已跟踪文件的未提交修改/删除(reviewer 家族文档直改兜底)。
390
+ *
391
+ * 触发场景:审查者/架构师按文档直改义务修正文档/注释后,由 opx_agent_submit 在 verdict
392
+ * 校验通过后调用——未提交修正会被下一轮工具检查点增量检测误判为新变更,也会在
393
+ * reconcileMainPollution 的干净树预检处死锁,提交时自动 commit 兜底。
394
+ *
395
+ * 语义:
396
+ * 1. `status --porcelain` 解析路径清单,过滤 `openspec/states/` 编排状态目录;过滤后无已跟踪
397
+ * 文件变更 → skipped(未触发)。
398
+ * 2. 有变更 → `add -u`(仅已跟踪文件的修改/删除,未跟踪新建文件不纳入,排除 openspec/states/)
399
+ * → `commit`(消息风格对齐既有 `docs(tasks):` / `docs(openspec):` 惯例)。
400
+ * 3. 任一 git 调用失败 → failed(含 stderr),不抛异常——调用方据此在返回体提示,不阻塞 verdict 写入。
401
+ */
402
+ export async function autoCommitWorktreeChanges(
403
+ wtPath: string,
404
+ agent: string,
405
+ stepId: string,
406
+ ): Promise<AutoCommitResult> {
407
+ const statusRes = await runGitChecked(wtPath, ["status", "--porcelain"])
408
+ if (!statusRes.success) {
409
+ return { status: "failed", files: [], untrackedFiles: [], stderr: statusRes.stderr }
410
+ }
411
+ const files: string[] = []
412
+ const untrackedFiles: string[] = []
413
+ for (const line of statusRes.stdout.split("\n").map((l) => l.trim()).filter(Boolean)) {
414
+ const path = parsePorcelainPaths(line)[0]
415
+ if (!path || path.startsWith("openspec/states/")) continue
416
+ if (line.startsWith("??")) untrackedFiles.push(path)
417
+ else files.push(path)
418
+ }
419
+ if (files.length === 0) return { status: "skipped", files: [], untrackedFiles }
420
+
421
+ const addResult = await runGitChecked(wtPath, ["add", "-u", "--", ".", ":(exclude)openspec/states"])
422
+ if (!addResult.success) {
423
+ return { status: "failed", files, untrackedFiles, stderr: addResult.stderr }
424
+ }
425
+ const commitResult = await runGitChecked(wtPath, [
426
+ "commit",
427
+ "-m",
428
+ `docs(opx): direct fixes by ${agent} (${stepId})`,
429
+ ])
430
+ if (!commitResult.success) {
431
+ return { status: "failed", files, untrackedFiles, stderr: commitResult.stderr }
432
+ }
433
+ return { status: "committed", files, untrackedFiles }
434
+ }
@@ -11,6 +11,11 @@
11
11
  * {"item":"<对应必做项>","category":"<降级类别>","adjudication":"user_response|unattended_auto|env_unavailable","note":"<说明>"}
12
12
  * adjudication 裁定方式枚举:user_response=用户答复、unattended_auto=无人值守自动降级、
13
13
  * env_unavailable=环境不可用(note 须记录尝试)。
14
+ *
15
+ * 必做覆盖口径:核验申报(completed=true + 描述注明核验方式与抽验样本,step 名首段 token 命中必做项)
16
+ * 是高成本必做项的合法覆盖形态——仅限 workflow 指令白名单限定的高成本必做项(deep_scan),低成本
17
+ * 必做项必须实跑后申报。覆盖判定只认 step 名 token 命中,不区分实跑与核验形态(机制层不拦截低成本项
18
+ * 误用核验申报形态,白名单约束由 workflow 指令与门禁错误提示承载)。
14
19
  */
15
20
  import {
16
21
  scanSkillTags,
@@ -641,16 +641,17 @@ export async function statusExecute(params: StatusParams, ctx: ToolContext): Pro
641
641
  const tg = findTaskGroup(state, state.taskGroupId)
642
642
  // 主仓库 openspec 污染诊断(56ddfe9 意图):编排者分派视图展示主仓库污染,供编排者人工核对
643
643
  const mainPollution = ctx.orchestrator ? await detectMainRepoPollution(ctx.worktree) : null
644
- // tool review 检查点增量检测(A4):verify_tool 的 reviewer-tool 工作视图按「检查点 → 当前 HEAD」区间
645
- // 变更分流(直提 / 仅处理待复核项 / 全量)。渲染层为同步函数,此处预计算后经 WorkflowStatusViewOptions 传入。
646
- // 仅在推荐分派该 agent 时计算,其余角色/step 不产生额外 git 调用。
644
+ // tool review 检查点增量检测(A4):full 的 verify_tool(reviewer-tool)与 simple 的 quality_review
645
+ // (合并审查者 openspec-reviewer)工作视图均按「检查点 → 当前 HEAD」区间变更分流。渲染层为同步函数,
646
+ // 此处预计算后经 WorkflowStatusViewOptions 传入。仅推荐分派对应 agent 时计算(verify_tool → reviewer-tool、
647
+ // quality_review → openspec-reviewer),其余角色/step 不产生额外 git 调用。
647
648
  let toolChanges: DetectChangesResult | undefined
648
- if (
649
- agentToReviewLayer(agent) === "tool" &&
649
+ const changesEvidenceStep =
650
650
  rec.status === "recommend" &&
651
- rec.stepId === "verify_tool" &&
652
- rec.agents.includes(agent)
653
- ) {
651
+ rec.agents.includes(agent) &&
652
+ ((agentToReviewLayer(agent) === "tool" && rec.stepId === "verify_tool") ||
653
+ (agent === "openspec-reviewer" && rec.stepId === "quality_review"))
654
+ if (changesEvidenceStep) {
654
655
  const wtPath = typeof item.metadata["worktree_path"] === "string" ? item.metadata["worktree_path"] : undefined
655
656
  if (wtPath) {
656
657
  const checkpoint =
@@ -333,7 +333,11 @@ export const agentSubmitSchema: JSONSchema = {
333
333
  description: "implement step:已完成的 task id(覆盖门禁:全部 open/rejected task 必须被覆盖)",
334
334
  items: { type: "string" },
335
335
  },
336
- test_results: { type: "string", description: "verify_tool step:UT 运行结果摘要" },
336
+ test_results: {
337
+ type: "string",
338
+ description:
339
+ "implement / verify_tool step:测试运行结果摘要(implement 为接口测试执行结果——附执行顺序与覆盖接口清单;verify_tool 为 UT 运行结果摘要);quality_review(simple 合并审查)下由开发者在 implement 申报,审查者提交时不生效",
340
+ },
337
341
  validation_steps: {
338
342
  type: "array",
339
343
  description: "review step:验证步骤执行摘要",
@@ -11,11 +11,11 @@ import {
11
11
  getStepVerdict, clearStepTags, isBlockingSeverity, isInfoSeverity, isTerminalPhase, applyTransition,
12
12
  } from "../workflow/engine.ts"
13
13
  import { resetReviewTagsOnFix, dedupeNewChildren, resolveChildIssueFields } from "../workflow/reset.ts"
14
- import { agentToReviewDimension, agentToReviewLayer, readIssueSource } from "../constants.ts"
14
+ import { agentToReviewDimension, agentToReviewLayer, isReviewerFamily, readIssueSource } from "../constants.ts"
15
15
  import { REVIEW_DIMENSIONS } from "../types.ts"
16
16
  import type { Dimension } from "../types.ts"
17
17
  import { taskChildrenOf, taskChildById, normalizeTaskChildIds, taskListOf, issueChildrenOf } from "../task-children.ts"
18
- import { reconcileMainPollution, getCurrentHead, isWorktreeClean } from "../git.ts"
18
+ import { autoCommitWorktreeChanges, reconcileMainPollution, getCurrentHead, isWorktreeClean } from "../git.ts"
19
19
  import { assertIssueFilesWithin } from "../paths.ts"
20
20
  import {
21
21
  readStateByWorktree, writeState, getLockPath, acquireLock, releaseLock,
@@ -264,7 +264,8 @@ function handleAnalyzeParams(item: WorkItem, params: AgentSubmitParams): void {
264
264
  }
265
265
  }
266
266
 
267
- /** implement step 参数处理:blocker(on_fail)、completed_task_ids + 覆盖门禁、self_check_results。 */
267
+ /** implement step 参数处理:blocker(on_fail)、completed_task_ids + 覆盖门禁、self_check_results /
268
+ * test_results 申报存档。 */
268
269
  function handleImplementParams(item: WorkItem, params: AgentSubmitParams): void {
269
270
  if (params.blocker) {
270
271
  if (params.verdict !== "failed") {
@@ -311,6 +312,11 @@ function handleImplementParams(item: WorkItem, params: AgentSubmitParams): void
311
312
  if (params.self_check_results) {
312
313
  item.metadata["self_check_results"] = params.self_check_results
313
314
  }
315
+ // test_results 为 dev 申报的接口测试执行结果存档(simple 模式 quality_review 视图「开发者自检申报」
316
+ // 区块渲染源),与 self_check_results 同为跨角色可见的正式提交参数存档。
317
+ if (params.test_results) {
318
+ item.metadata["test_results"] = params.test_results
319
+ }
314
320
  }
315
321
 
316
322
  /** 把 boundary_expansion 的目录/包合并进执行边界(metadata 为事实源,缺失则忽略)。 */
@@ -395,7 +401,7 @@ function assertFailedHasReason(
395
401
  }
396
402
  }
397
403
 
398
- /** review step(verify_tool/verify_task/verify_quality)参数处理。 */
404
+ /** review step(verify_tool/verify_task/verify_quality/quality_review)参数处理。 */
399
405
  function handleReviewParams(
400
406
  item: WorkItem,
401
407
  params: AgentSubmitParams,
@@ -417,7 +423,10 @@ function handleReviewParams(
417
423
  mergeBoundaryInto(item, params.boundary_expansion)
418
424
  }
419
425
 
420
- if (params.test_results) item.metadata["test_results"] = params.test_results
426
+ // test_results 透传:full 模式 verify_tool 的 UT 摘要写入不受影响;simple 模式 quality_review 下
427
+ // test_results 是 dev 申报的接口测试结果存档(quality_review 视图「开发者自检申报」区块渲染源),
428
+ // 合并审查者提交不得覆盖 dev 申报(spec 契约:申报区块语义为 dev 产出)。
429
+ if (params.test_results && stepId !== "quality_review") item.metadata["test_results"] = params.test_results
421
430
 
422
431
  if (params.validation_steps) {
423
432
  // 结构化 skip_reason 校验(升级既有「非空即可」):未完成项必须带合法结构化降级声明
@@ -428,6 +437,9 @@ function handleReviewParams(
428
437
  // 必做清单覆盖度门禁:verdict=passed 且非补交豁免(仅裁定参数)时,validation_steps 须逐项覆盖
429
438
  // 当前 agent 能力集解析出的质量门 skill 必做清单(must_do)。遗漏必做项且无有效降级证据 → 拒绝提交。
430
439
  // 未声明 must_do 的 skill / 解析不到质量门 skill 的 step 自动跳过(不误伤 verify_task/verify_quality)。
440
+ // 覆盖口径:核验申报(completed=true + 描述注明核验方式与抽验样本,step 名首段 token 命中必做项)是
441
+ // 高成本必做项的合法覆盖形态,仅限 workflow 指令白名单限定的高成本必做项(deep_scan);低成本必做项
442
+ // 必须实跑后申报(机制层只认 token 命中不区分形态,白名单口径在错误提示中明示)。
431
443
  if (params.verdict === "passed" && !opts.skipMustDoGate) {
432
444
  const uncovered = uncoveredMustDo(opts.capabilityTags, params.validation_steps)
433
445
  if (uncovered.length > 0) {
@@ -435,7 +447,8 @@ function handleReviewParams(
435
447
  `提交的验证步骤未覆盖质量门 skill 必做清单,缺少以下必做项:${uncovered.join("、")}。\n` +
436
448
  `请按已加载质量门类 skill 的必做清单逐项执行并逐项申报结果(completed=true);` +
437
449
  `确实无法执行的必做项须以结构化 skip_reason 申报降级理由` +
438
- `(格式:${SKIP_REASON_FORMAT})。`
450
+ `(格式:${SKIP_REASON_FORMAT})。` +
451
+ `低成本必做项必须实跑后申报;核验申报仅限 workflow 指令白名单限定的高成本必做项。`
439
452
  )
440
453
  }
441
454
  }
@@ -734,6 +747,33 @@ export async function agentSubmitExecute(params: AgentSubmitParams, ctx: ToolCon
734
747
  newChildren: accepted,
735
748
  })
736
749
 
750
+ // reviewer 家族(openspec-architect + 全部审查者逻辑身份)worktree 直改修正自动提交兜底:
751
+ // 审查者/架构师按文档直改义务修正文档/注释后,提交时自动 commit(openspec/states/ 编排状态
752
+ // 除外,仅纳入已跟踪文件的修改/删除)。
753
+ // 时序硬约束:必须在 _tool_review_checkpoint 捕获之前(否则修正会被下一轮增量检测误判为新
754
+ // 变更)、必须在 reconcileMainPollution 之前(其干净树预检要求 worktree 无未提交内容——架构师
755
+ // 身份的 worktree md 修正无人提交会在该预检处死锁)。
756
+ // 硬边界(防事故):自动提交目标恒为 wtPath(任务组 worktree),严禁对 ctx.worktree(主仓库)
757
+ // 执行 add/commit。失败不阻塞 verdict 写入,仅在返回体提示编排者安排补提交。
758
+ const autoCommitLines: string[] = []
759
+ if (isReviewerFamily(ctx.agent)) {
760
+ const ac = await autoCommitWorktreeChanges(wtPath, ctx.agent, params.step_id)
761
+ if (ac.status === "committed") {
762
+ autoCommitLines.push(
763
+ `- ✅ worktree 直改修正已自动提交(${ac.files.length} 个文件,commit message: \`docs(opx): direct fixes by ${ctx.agent} (${params.step_id})\`)`,
764
+ )
765
+ } else if (ac.status === "failed") {
766
+ autoCommitLines.push(
767
+ `- ⚠️ worktree 直改修正自动提交失败:${ac.stderr || "未知错误"}。请编排者安排补提交(worktree 内 git add -u 后 commit)`,
768
+ )
769
+ }
770
+ if (ac.untrackedFiles.length > 0) {
771
+ autoCommitLines.push(
772
+ `- ⚠️ 存在未跟踪新建文件未纳入自动提交(须纳入版本控制时由编排者/开发者处理):${ac.untrackedFiles.join("、")}`,
773
+ )
774
+ }
775
+ }
776
+
737
777
  // arch_submit 主仓库污染文档自动合并兜底(56ddfe9 意图):analyze step(架构师)以 passed 提交后,
738
778
  // 若主仓库本 change 目录下存在未提交 openspec 文档污染,并入 worktree 分支并清理主仓库工作树。
739
779
  if (
@@ -750,10 +790,15 @@ export async function agentSubmitExecute(params: AgentSubmitParams, ctx: ToolCon
750
790
  })
751
791
  }
752
792
 
753
- // tool review 检查点增量检测(A3):verify_tool 的 reviewer-tool 提交成功后记录当前 HEAD,
754
- // 供下次重进 verify_tool 时仅对比「检查点 → 当前 HEAD」区间的变更(无代码/配置变更时直提 passed,
755
- // 省去全量工具扫描)。写入位于 submitForStep 成功与 writeState 之间,任何异常路径均不落盘(零状态变更)。
756
- if (params.step_id === "verify_tool" && agentToReviewLayer(ctx.agent) === "tool") {
793
+ // 审查检查点增量检测(A3):full 的 verify_tool(reviewer-tool)与 simple 的 quality_review
794
+ // (合并审查者 openspec-reviewer)提交成功后记录当前 HEAD,供下次重进该 step 时仅对比「检查点 →
795
+ // 当前 HEAD」区间的变更(无代码/配置变更时直提 passed,省去全量工具扫描)。两者写同一 metadata key
796
+ // `_tool_review_checkpoint`,但 simple / full 两 workflow 无对方 step,读写互不交叉。写入位于
797
+ // submitForStep 成功与 writeState 之间,任何异常路径均不落盘(零状态变更)。
798
+ if (
799
+ (params.step_id === "verify_tool" && agentToReviewLayer(ctx.agent) === "tool") ||
800
+ (params.step_id === "quality_review" && ctx.agent === "openspec-reviewer")
801
+ ) {
757
802
  const head = await getCurrentHead(wtPath)
758
803
  if (head) item.metadata["_tool_review_checkpoint"] = head
759
804
  }
@@ -766,7 +811,15 @@ export async function agentSubmitExecute(params: AgentSubmitParams, ctx: ToolCon
766
811
  }
767
812
 
768
813
  await writeState(ctx.worktree, state)
769
- return SUBMIT_OK_MESSAGE
814
+ if (autoCommitLines.length === 0) return SUBMIT_OK_MESSAGE
815
+ return [
816
+ `## ✅ ${SUBMIT_OK_MESSAGE}`,
817
+ "",
818
+ "### worktree 自动提交",
819
+ "",
820
+ ...autoCommitLines,
821
+ "",
822
+ ].join("\n")
770
823
  } finally {
771
824
  releaseLock(lockPath)
772
825
  }
package/src/core/views.ts CHANGED
@@ -165,6 +165,27 @@ export function renderAgentSummaries(agentSummaries: Record<string, string> | un
165
165
  return lines
166
166
  }
167
167
 
168
+ /** 开发者自检申报渲染。数据源为 workItem metadata 的 self_check_results / test_results(提交参数的存档),
169
+ * 分「自检申报(self_check_results)」与「接口测试结果(test_results)」两段渲染,两者皆缺失/为空时
170
+ * 返回空数组(不渲染空区块)。
171
+ * 与 renderAgentSummaries 的语义区分:会话摘要按角色隔离(辅助记忆,不跨 agent 传递);自检申报是
172
+ * 正式提交参数的存档,跨角色可见是设计意图——reviewer 分级复验以 dev 申报为事实输入(低成本实跑对照、
173
+ * 高成本项核验申报+抽样重放),不违反「子代理上下文不得转述」:该条约束的是编排者在分派 prompt 中
174
+ * 转述动态上下文,视图渲染动态上下文本就是 opx_status 工具本职。 */
175
+ export function renderDevSelfCheckDeclaration(metadata: Record<string, unknown>): string[] {
176
+ const selfCheck = typeof metadata["self_check_results"] === "string" ? metadata["self_check_results"].trim() : ""
177
+ const testResults = typeof metadata["test_results"] === "string" ? metadata["test_results"].trim() : ""
178
+ if (selfCheck === "" && testResults === "") return []
179
+ const lines: string[] = ["## 开发者自检申报", ""]
180
+ if (selfCheck !== "") {
181
+ lines.push("**自检申报(self_check_results)**:", "", selfCheck, "")
182
+ }
183
+ if (testResults !== "") {
184
+ lines.push("**接口测试结果(test_results)**:", "", testResults, "")
185
+ }
186
+ return lines
187
+ }
188
+
168
189
  // ─── 占位符插值层 ───
169
190
 
170
191
  /** 插值白名单:仅这些 key 可被 {{key}} 占位符引用,其余一律保留原文(防配置注入任意动态值)。 */
@@ -12,7 +12,7 @@ import { resolveChildIssueFields } from "./reset.ts"
12
12
  import { taskListOf, issueChildrenOf } from "../task-children.ts"
13
13
  import {
14
14
  renderSkillSuggestions, renderEfficiencySteps, renderWorktreeSection,
15
- renderAgentSummaries, renderTaskItem, formatFilePath, formatSeverity,
15
+ renderAgentSummaries, renderDevSelfCheckDeclaration, renderTaskItem, formatFilePath, formatSeverity,
16
16
  isWorktreeReady, renderWorktreeNotReady, interpolateText, renderStateMismatchDiagnostic,
17
17
  } from "../views.ts"
18
18
  import { resolveMustDoForCaps, SKIP_REASON_FORMAT } from "../tools/gate.ts"
@@ -547,13 +547,25 @@ function renderAgentWorking(
547
547
  }
548
548
  lines.push(...renderStepSemantics(step, common, stepCtx))
549
549
  lines.push(...renderWorktreeSection(state, tg, { showNamespace: true, showPort: true }))
550
- // verify_tool 分支③(有变更 → 全量路径)证据注入:展示检查点增量口径的本次变更证据(文件清单 +
551
- // 区间 diff 命令),与 Worktree 区块「变更范围」(baseRef..HEAD 整个 change 累计口径)区分,
552
- // 供 reviewer 审查变更内容后按操作指引裁量是否可免全量工具检查(裁量语义由 workflow 配置单源承载,
553
- // 此处仅渲染事实证据)。仅 verify_tool + reviewer-tool + 已预计算变更检测结果(hasNonDocChange=true)
554
- // 时渲染;分支①②提前返回、toolChanges 缺省路径不经过此处,不误伤其他 agent/step。
550
+ // 证据区块注入:展示检查点增量口径的本次变更证据(文件清单 + 区间 diff 命令),与 Worktree 区块
551
+ // 「变更范围」(baseRef..HEAD 整个 change 累计口径)区分,供审查者审查变更内容后按操作指引裁量是否可免
552
+ // 全量工具检查(裁量语义由 workflow 配置单源承载,此处仅渲染事实证据)。
553
+ // - verify_tool(full 分支③有变更 → 全量路径):仅 reviewer-tool + 已预计算变更检测结果
554
+ // (hasNonDocChange=true)时渲染;分支①②提前返回、toolChanges 缺省路径不经过此处,不误伤其他 agent/step。
555
+ // - quality_review(simple 合并审查):toolChanges 已预计算即渲染,不判 hasNonDocChange——simple 无
556
+ // 三分支替换式视图,无变更时也渲染区块(文件清单处显式「未检出」),给「无变更直提」分流显式视觉信号。
555
557
  if (step?.id === "verify_tool" && agentToReviewLayer(ctxAgent) === "tool" && toolChanges?.hasNonDocChange) {
556
- lines.push(...renderToolChangesEvidence(item, tg, toolChanges))
558
+ lines.push(...renderToolChangesEvidence(item, tg, toolChanges, "tool_review"))
559
+ }
560
+ if (step?.id === "quality_review" && toolChanges) {
561
+ lines.push(...renderToolChangesEvidence(item, tg, toolChanges, "merged_review"))
562
+ }
563
+ // simple 合并审查(quality_review)证据注入:渲染提交参数存档的自检申报(self_check_results /
564
+ // test_results),作为分级复验的事实输入——低成本实跑对照、高成本项核验申报+抽样重放均以该申报为
565
+ // 证据源。仅 quality_review step 渲染(消费方是 reviewer);implement 视图与其余 step 视图不受影响
566
+ //(dev 自己刚产出该申报,无需回显)。
567
+ if (step?.id === "quality_review") {
568
+ lines.push(...renderDevSelfCheckDeclaration(item.metadata))
557
569
  }
558
570
  lines.push(...renderAgentSummaries(readAgentSummaries(item), ctxAgent))
559
571
  lines.push(...renderStepContext(item, step, ctxAgent, state, exemptionCtx))
@@ -799,26 +811,42 @@ function renderToolChildren(item: WorkItem, ctxAgent: string, exemptionCtx?: Exe
799
811
  return lines
800
812
  }
801
813
 
802
- /** verify_tool 分支③(全量路径)证据区块:检查点增量口径的本次变更证据(文件清单 + 区间 diff 命令)。
803
- * 有检查点时标注「本次为自上次工具检查(checkpoint..HEAD)增量区间」,与 Worktree 区块「变更范围」
814
+ /** 变更证据区块的检查点口径措辞:tool_review=verify_tool(full,「工具检查」)、
815
+ * merged_review=quality_review(simple 合并审查,「合并审查」)。verify_tool 文案保持既有原文。 */
816
+ type ChangesEvidenceKind = "tool_review" | "merged_review"
817
+
818
+ const EVIDENCE_KIND_LABELS: Record<ChangesEvidenceKind, { title: string; lastCheck: string }> = {
819
+ tool_review: { title: "自上次工具检查", lastCheck: "上次工具检查" },
820
+ merged_review: { title: "自上次合并审查", lastCheck: "上次合并审查" },
821
+ }
822
+
823
+ /** 证据区块:检查点增量口径的本次变更证据(文件清单 + 区间 diff 命令)。verify_tool 分支③(全量路径)
824
+ * 与 quality_review(simple 合并审查)共用,经 kind 区分口径措辞(「自上次工具检查」/「自上次合并审查」)。
825
+ * 有检查点时标注「本次为自上次审查(checkpoint..HEAD)增量区间」,与 Worktree 区块「变更范围」
804
826
  * (baseRef..HEAD 整个 change 累计口径)区分;无检查点(首次进入)时标注基线兜底口径,与变更范围一致;
805
827
  * 无检查点亦无基线基准时不渲染 diff 命令(无法界定区间)。仅渲染事实证据,不做变更性质判定——
806
828
  * 裁量语义由 workflow 配置操作指引单源承载。 */
807
- function renderToolChangesEvidence(item: WorkItem, tg: TaskGroupState, toolChanges: DetectChangesResult): string[] {
829
+ function renderToolChangesEvidence(
830
+ item: WorkItem,
831
+ tg: TaskGroupState,
832
+ toolChanges: DetectChangesResult,
833
+ kind: ChangesEvidenceKind,
834
+ ): string[] {
835
+ const label = EVIDENCE_KIND_LABELS[kind]
808
836
  const checkpoint =
809
837
  typeof item.metadata["_tool_review_checkpoint"] === "string" ? item.metadata["_tool_review_checkpoint"] : undefined
810
838
  const baseRef = tg.baseRef ?? undefined
811
839
  const rangeRef = checkpoint ?? baseRef
812
- const lines = ["## 本次变更证据(自上次工具检查)", ""]
840
+ const lines = [`## 本次变更证据(${label.title})`, ""]
813
841
  if (checkpoint) {
814
842
  lines.push(
815
- `- **口径**: 本次为「自上次工具检查(${checkpoint}..HEAD)」的增量区间(含已提交与未提交的非 openspec 变更);` +
843
+ `- **口径**: 本次为「${label.title}(${checkpoint}..HEAD)」的增量区间(含已提交与未提交的非 openspec 变更);` +
816
844
  `与上方「变更范围」(${baseRef ?? "(无)"}..HEAD 整个 change 累计口径)不同`,
817
845
  "",
818
846
  )
819
847
  } else if (baseRef) {
820
848
  lines.push(
821
- `- **口径**: 本次区间以基线(${baseRef}..HEAD)兜底,与上方「变更范围」一致(首次进入,无上次工具检查记录;` +
849
+ `- **口径**: 本次区间以基线(${baseRef}..HEAD)兜底,与上方「变更范围」一致(首次进入,无${label.lastCheck}记录;` +
822
850
  "含已提交与未提交的非 openspec 变更)",
823
851
  "",
824
852
  )