@ifrankwang/openspec-agents 0.132.1 → 0.133.1

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/README.md CHANGED
@@ -79,6 +79,16 @@ npm install -D @ifrankwang/openspec-agents
79
79
  3. **门禁**:每一步的结论通过检查后才进入下一步;不通过的结论自动回到实现环节修复。
80
80
  4. **收尾**:变更完成后自动合并分支并清理执行环境,全程留痕可审计。
81
81
 
82
+ ## 独立审查会话
83
+
84
+ 除绑定 OpenSpec 变更的编排流程外,插件支持不绑定任何变更的独立代码审查——对某个 PR 的改动区间或整个代码库做一次完整审查。
85
+
86
+ - **入口**:初始化时传 `review_scope`(与 `change_id`/`task_group_id` 二选一,互斥),说明三件事——审查范围(`pr`=两个本地分支之间的改动区间,或 `full`=全量代码库)、颗粒度(`thorough`=工具检查 → 回归验证 → 五个维度的质量审查三层;`simple`=单个审查者合并完成全部检查)、修复策略(`fix`=发现问题后自动修复再重审;`none`=只审不修,问题报告本身就是交付物)。工具按审查范围推导出会话 ID 并在返回结果里告知,后续步骤都用这个会话 ID 进行。
87
+ - **流程**:审查在独立 worktree 中进行(PR 形态从目标分支建工作分支,全量形态从当前分支建);`fix` 模式下审查不通过会回退给开发者按问题清单修复,修完再回到第一层审查重跑;`none` 模式下审查失败直接收口,不会进入修复环节。
88
+ - **回归验证**:thorough 颗粒度的第二层是回归验证——构建必须通过、既有测试不得出现新失败(首轮跑出的存量失败会作为问题上报进入处置通道),可启动服务时还会做冒烟与既有 API 测试。
89
+ - **收尾**:`fix` 模式审查全部通过后,把工作分支合并回目标分支并清理环境;`none` 模式不合并,直接销毁工作环境,收尾返回按严重级别、维度、文件位置归并的审查结果摘要。
90
+ - **恢复**:与变更编排一致,中断后可用恢复参数回到指定审查层继续(独立审查会话的恢复步骤值域按颗粒度校验)。
91
+
82
92
  ## 适用场景与代价
83
93
 
84
94
  | | |
@@ -15,7 +15,7 @@ permission:
15
15
 
16
16
  - **架构师**(analyze,方案审查):文档一致性复核与方案评估——以 design.md 为输入审查架构模式合规性与方案合理性,仅可编辑 md 文档直接修复文档问题,信息齐备时输出执行边界、存在缺口时提交 blockers
17
17
  - **工具维度审查者**(tool review,full 模式三层门禁第一层):执行确定性工具检查,将工具输出映射为统一 issue 结构;简单(simple)模式亦承担该职责
18
- - **任务维度审查者**(task review,full 模式三层门禁第二层):验证 task 产出完整性、启动服务并检查健康、独立执行 API 测试并审查质量、审查测试代码质量;验证完成后清理本变更启动的环境(停止残留服务、清理隔离环境);简单(simple)模式亦承担该职责
18
+ - **任务维度审查者**(task review,full 模式三层门禁第二层):验证 task 产出完整性、启动服务并检查健康、独立执行 API 测试并审查质量、审查测试代码质量;验证完成后清理本变更启动的环境(停止残留服务、清理隔离环境);简单(simple)模式亦承担该职责。独立审查会话(不绑定 change 的 thorough 颗粒度)中该身份为回归验证层——无 spec 验收标准,锚点为既有测试与行为基线(构建通过、既有测试不引入新失败、可启动服务时冒烟与既有 API 测试脚本执行);判例与严重级别体系沿用
19
19
  - **质量维度审查者**(quality review,full 模式三层门禁第三层):按分派维度(style / architecture / performance / security / maintainability)审查
20
20
  - **simple 合并审查者**:以分级复验与对抗审查承担 simple 模式合并审查——低成本确定性工具实跑,高成本项核验 dev 自检申报并以核心链路抽样重放验证,对抗性判断(测试代码质量、业务行为真实性、关键数据产出核验)保持独立视角不重走 dev 路径;验证完成后清理本变更启动的环境(停止残留服务、清理隔离环境)
21
21
 
@@ -33,6 +33,15 @@ capabilities: ["orchestrator"]
33
33
  - DSH 形态:`openspec_developer` / `openspec_reviewer` 两个物理子代理(`openspec_<role>`,物理 agent 已收敛,9 种逻辑身份经 `_agent` 参数承载),分派时直接调用这些专用工具,不要使用通用 `subagent` 代替;
34
34
  - 其他 harness 按各自原生子代理机制分派(Claude Code / Codex / ZCode 插件已注入对应子代理)。
35
35
 
36
+ ## 审查请求入口(独立审查会话)
37
+
38
+ 用户表达审查意图且不绑定 OpenSpec change(如「审查这个 PR」「对代码库做一次审查」)时,走独立审查入口:初始化工具传 `review_scope`(不传 `change_id`/`task_group_id`,两者互斥)。`review_scope` 含四个要素:`scope_type`(pr=按 base..head 本地分支区间 / full=全量代码库)、`base_ref`(pr 基准分支,缺省自动推导 main→master)、`head_ref`(pr 审查目标分支,须为本地分支——`origin/*` 远端引用不受支持)、`granularity`(simple=单层合并审查 / thorough=三层审查)、`fix`(none=只审不修 / fix=审并修)。
39
+
40
+ - 有人值守且 `fix` 或 `granularity` 未明确:用 question 工具逐项向用户确认后显式传入,不擅自替用户拍板
41
+ - 无人值守且未明确:缺省 `fix=none` + `granularity=simple`(只读零副作用、成本最低);遇高敏信号(如用户明确要求安全审计)可自行升档 `thorough`
42
+ - 初始化返回体回传会话 ID——后续所有编排工具以 `change_id="<会话 ID>"` 传入(worktree 创建、状态查询、提交、收尾同一会话 ID)
43
+ - 独立审查会话无「任务组」概念,分派模板措辞按「审查会话 <会话 ID>」表述;其余分派范式(不转述动态上下文、`_agent` 身份指令)不变
44
+
36
45
  ## 行为准则
37
46
 
38
47
  - 每次子代理返回后,调用状态查询工具获取权威「下一步」指令并严格遵循;推进被拦时按工具给出的阻塞原因与修复建议处理,不自行推断其他动作
@@ -0,0 +1,86 @@
1
+ # review-simple workflow:独立审查会话(simple 颗粒度,不绑定 OpenSpec change)
2
+ # 流转:单层合并审查(review 阶段 quality_review)+ in_progress 阶段 implement 修复闭环。
3
+ # fix=none(只审)会话的 fail 方向转移由引擎按 review_fix_policy 短路映射 done(implement 为拓扑统一声明的死 step)。
4
+ # 语义归属:step 级 instructions/constraints 定义各 step 专属语义;
5
+ # 跨所有 step 共享内容收敛到顶层 common(step 自动继承);非 step 归属的引擎状态文案不进配置。
6
+ # 占位符 {{key}} 经 interpolateText 注入动态值,缺值保留原文。
7
+ # 无 change 语义:不引用 tasks.md/design.md/spec/openspec/changes;执行边界缺省整个 worktree。
8
+ # implement/quality_review 的压制与隔离/清理约束与 review.yaml 同步维护
9
+ id: review-simple
10
+ name: Standalone Review Workflow (simple)
11
+ max_retries: 10
12
+
13
+ # 跨所有 step 共享的通用指引:渲染时 common 在前、step 在后合并进对应视图区块
14
+ common:
15
+ instructions:
16
+ - "非 OpenCode 直载形态(经 MCP 接入的 agent 客户端)调用 opx_* 工具时,须携带 `_agent` 参数并传自身角色名(如 openspec-developer);缺省会被视为编排主代理视角,将拿不到自身角色的执行视图与操作指引"
17
+ - "若当前工具列表中的编排工具带有 `mcp__<server>__` 前缀(MCP 接入形态,如 DeepSeek Harness / Claude Code / Codex / ZCode),调用时使用工具列表中的完整名称,例如 `mcp__opx__status`、`mcp__opx__agent_submit`;若工具列表中没有这些工具且存在 `dev_tool_search`,先用 `dev_tool_search` 搜索 `opx` 并一次性解锁全部需要的工具(含 `todo_write`);禁止用 shell 查找 opx/openspec/MCP 配置"
18
+ - "首次调用 opx_status 前不要探查仓库结构或状态文件;工具未解锁时只用 dev_tool_search 解锁,解锁后立即调用 opx_status"
19
+ - "调用 opx_status 获取动态上下文({{worktree_path}}、审查范围、上轮会话摘要)"
20
+ - "按 Skill 加载清单加载匹配的 skill 并遵循其全部规范"
21
+ - "阅读 `{{worktree_path}}/AGENTS.md` 项目规范"
22
+ - "需要外部框架/工具的权威文档(规则语义、API 用法、配置说明)时,优先通过文档检索渠道获取,禁止直接翻源码"
23
+ - "完成后调用 opx_agent_submit 提交裁决或失败上报"
24
+ constraints:
25
+ - "所有 edit/write 操作限定在 {{worktree_path}} 内,严禁修改主仓库路径下的文件"
26
+ - "禁止为获取外部框架/工具的规则语义、API 用法等知识而解析其源码、jar 包或执行 AST dump;此类知识一律通过文档检索渠道获取;声明式规则定义来源(规则定义 XML 直拉、工具官方 Web API 返回、官方文档页)属于文档检索渠道范畴"
27
+ - "不跳步骤、不自行推断阶段流转,next-step 以 opx_status 为准"
28
+ - "禁止调用任何 opx_orch_* 工具(编排者专属)"
29
+ - "审查/报告范围以视图「审查范围」区块为准:PR 区间形态为 base..head 区间 + 修复轮检查点增量;全量形态为整个代码库"
30
+
31
+ phases:
32
+ - name: in_progress
33
+ steps:
34
+ - id: implement
35
+ agents:
36
+ - id: openspec-developer
37
+ capability_tags: [efficiency, api-testing, quality-gate, dev-practices, db-design, style, architecture, performance, security, maintainability, cleanup]
38
+ transitions:
39
+ on_pass: quality_review
40
+ on_fail: implement
41
+ instructions:
42
+ - "以视图 Issue 清单驱动修复:Low+ 必办(修复后经 fixed_issue_ids 上报进入待复核),Info 建议修复不阻塞提交;不可修 issue 经 exempt_issue_ids 申请豁免(申请后进入待裁定状态,由报源 reviewer 裁定,被驳回的 issue 回待修复);全部 blocking issue 均已上报修复或申请豁免后才可 passed 提交推进"
43
+ - "执行边界缺省整个 worktree(独立审查无分析环节,不生成边界白名单);issue 的 file 字段指向的文件即修复边界,修复这些文件(含回归引入问题)不算越界;可按 issue 的 suggestion 直接执行修复;reviewer 的 boundary_expansion 已并入边界"
44
+ - "对修复未过次数≥2 的 issue 须先完成 5-Why 根因分析再动手,不得跳过分析直接改代码"
45
+ - "修复完成后先 git commit 再提交;提交前按已加载的质量门类 skill 完成本地构建验证,并按维度规范类 skill(style/architecture/performance/security/maintainability 能力类别)对本次修复文件逐项自检,自检结论与工具检查结果一并汇总进 self_check_results"
46
+ - "自证/验证需启动服务或基础设施时,一律按已加载技术栈 skill 的隔离启动规范以隔离方式启动,隔离标识与建议端口以视图 Worktree 区块为准;验证完成后停止本次启动的实例,不留残留;禁止为腾出端口或资源而停止、复用或干扰非本会话启动的进程"
47
+ - "确定性质量工具检查出的问题不得以压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等)静默规避:确需压制时,必须在压制处代码注释说明清楚理由,并经 exempt_issue_ids 申请豁免由报源 reviewer 裁定;不得以 fixed_issue_ids 冒充修复"
48
+ - "提交 opx_agent_submit:必传 verdict;修复完成的 issue 经 fixed_issue_ids 上报、不可修 issue 经 exempt_issue_ids 申请豁免"
49
+ constraints:
50
+ - "本会话无任务清单门禁(无 tasks.md 子任务),修复范围仅由 issue 清单与检查结果界定"
51
+ - "生产路径禁止 stub/fake/空实现/硬编码成功"
52
+ - "提交前自检必须覆盖已加载的全部 skill(含维度规范类)检查点,不得以工具检查结果替代维度规范自检"
53
+ - "禁止以压制手段静默规避确定性质量工具问题:压制必须附明确理由并经豁免申请流程裁定,不得以工具不再报错为由跳过申报"
54
+ - name: review
55
+ steps:
56
+ - id: quality_review
57
+ agents:
58
+ - id: openspec-reviewer
59
+ capability_tags: [quality-gate, api-testing, dev-practices, efficiency, style, architecture, performance, security, maintainability, tool-improvement, cleanup]
60
+ transitions:
61
+ on_pass: done
62
+ on_fail: implement
63
+ instructions:
64
+ - "依据 opx_status 提供的变更检测结果分流:无代码/配置变更且本层无待复核/待裁定项 → 直接提交 passed,不运行全量工具检查,并以一条 step 名首段为 no_change 的 validation_steps 声明本轮整体豁免必做清单(completed=false + 结构化 skip_reason 注明判定依据);无变更但有本层待复核/待裁定项 → 仅复核/裁定待处理项后提交,不跑全量工具扫描;有变更 → 加载质量门类 skill 获取工具清单与映射规则,按已加载质量门 skill 必做清单逐项执行全部确定性工具检查,将工具输出映射为统一 issue 结构后提交"
65
+ - "审查锚点按视图「审查范围」:PR 区间形态为 base..head 区间(首轮)或检查点增量(修复轮);全量形态为整个代码库——全量形态下的工具违规无论是否本轮引入一律映射为 issue,不得以不在变更清单为由丢弃"
66
+ - "「有变更」分支先审查上方「本次变更证据」区块的变更文件清单与区间 diff 内容后自行判断:若确认本次变更仅为注释/文档性且无逻辑影响,可自行决定跳过全量工具检查直接提交 passed(validation_steps 以 step=no_change 申报整体豁免必做清单);无法确认或含逻辑/结构变更,必须顺序运行全部确定性工具检查;裁量仅免除全量工具扫描,不免除复核/裁定——本层 blocking issue 仍须处理至终态后提交"
67
+ - "回归验证(独立审查场景,无 spec 验收标准——以既有测试与行为基线为锚):①构建通过;②既有测试全量执行不引入新失败——首轮跑出的存量失败按 issue 上报进入处置通道(审并修模式进修复闭环或豁免,只审模式进报告),修复轮出现「上轮未登记的新失败」即回归、verdict=failed 并报 issue(说明失败用例与归因);③可启动服务时按已加载技术栈 skill 的隔离启动规范隔离启动做冒烟与既有 API 测试脚本执行(存在时),验证后按已加载的清理规范幂等清理(含 verdict=failed 轮次);无法启动服务或无脚本时 ③ 置 completed=false 并以结构化 skip_reason 申报"
68
+ - "加载匹配维度 skill 做五维度合并审查(style/architecture/performance/security/maintainability),结合领域知识做拓展审查(skill 仅为最低基准);顺带发现的其他缺陷按对应维度严重级别标准报 issue,禁止静默丢弃"
69
+ - "审查新 issue 前先查看视图「待复核」区块中的既有 issue(仅本审查者报源),避免语义重复"
70
+ - "报 issue 时必须显式声明归因维度 dimension(style/architecture/performance/security/maintainability):本 reviewer 不归属任一质量维度,dimension 即该 issue 归属的目标质量维度"
71
+ - "报 issue 前先判断该问题能否通过调整确定性质量扫描工具配置(规则收紧/新增规则/配置增强)统一收敛:可工具化 → 双报业务 issue(file=违规现场)与工具改进 issue(file=规则/配置文件,suggestion 含规则草案+验证命令,末尾标 [tool_eligible],且与业务 issue 同维度),两条 file 不同、方案不同,不构成语义重复,必须双报;不可工具化 → 仅报业务 issue。工具化判断细节与规则草案模板遵循已加载的工具改进类 skill,未加载时仅报业务 issue、跳过工具改进环节;提交时在 validation_steps 中记录工具化判断结论"
72
+ - "只报不改与文档直改:影响代码运行的逻辑类问题只上报、不直接修改;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue,修正情况在提交说明/validation_steps 留痕;工具改进建议仅上报(含规则草案),实施由开发者完成"
73
+ - "复核自己报的已修复待复核(review 态)issue:通过置 done,不通过驳回(附驳回原因,issue 回待修复并累计修复未过次数)"
74
+ - "对视图「待裁定是否可豁免」区块中的豁免申请经 exempt_adjudications 裁定(dismissed/rejected)"
75
+ - "审查区间 diff 与 git log,识别变更中新增的压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等):视为豁免申请,核对是否已在压制处代码注释或 commit message 说明理由并申报豁免;未申报或理由不成立 → 报 issue(file=压制位置、line=压制行、rule=被压制规则名,severity 至少 Low);理由成立 → 经 exempt_adjudications 裁定 dismissed"
76
+ - "报静态分析类 issue 时,rule 字段必填官方规则全名(命名格式遵循已加载的质量门类 skill 的规则导航索引),漏填将导致项目级豁免清单无法生效"
77
+ - "报静态分析类 issue 时,description 必须含:①复现命令(非静默形态,含必要开关);②实测数据(命中类清单/计数);③规则默认阈值与当前项目配置对比(引用可改的配置文件路径)"
78
+ - "按 opx_status 操作指引完成全部验证项,提交前将每项验证结果与证据填入 validation_steps 参数(未执行的项须填 skip_reason 注明原因),不得省略任何一项"
79
+ - "提交 opx_agent_submit,必带 verdict 与 validation_steps(验证步骤执行摘要)"
80
+ constraints:
81
+ - "工具调用边界:仅可调用 opx_status、opx_agent_submit、question(需要用户协助处理时);可 bash 运行确定性工具检查、启动服务并执行验证命令"
82
+ - "只报不改与文档直改:禁止 edit/write 影响代码运行的内容(逻辑类问题仅上报);文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
83
+ - "即使无 issue 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
84
+ - "Info 级 issue 的 description/suggestion 禁止阶段/时机表述(如'可后续处理')"
85
+ - "压制类 issue 严重级别至少 Low"
86
+ - "禁止为腾出端口或资源而停止、复用或干扰非本会话启动的进程(含用户或其他会话运行的服务实例);验证所需环境一律按隔离标识隔离启动"
@@ -0,0 +1,124 @@
1
+ # review workflow:独立审查会话(thorough 颗粒度,不绑定 OpenSpec change)
2
+ # 流转:三层审查(review 阶段 verify_tool → verify_task → verify_quality)+ in_progress 阶段 implement 修复闭环。
3
+ # fix=none(只审)会话的 fail 方向转移由引擎按 review_fix_policy 短路映射 done(implement 为拓扑统一声明的死 step)。
4
+ # 语义归属:step 级 instructions/constraints 定义各 step 专属语义;
5
+ # 跨所有 step 共享内容收敛到顶层 common(step 自动继承);非 step 归属的引擎状态文案不进配置。
6
+ # 占位符 {{key}} 经 interpolateText 注入动态值,缺值保留原文。
7
+ # 无 change 语义:不引用 tasks.md/design.md/spec/openspec/changes;执行边界缺省整个 worktree。
8
+ id: review
9
+ name: Standalone Review Workflow (thorough)
10
+ max_retries: 10
11
+
12
+ # 跨所有 step 共享的通用指引/约束:渲染时 common 在前、step 在后合并进对应视图区块
13
+ common:
14
+ instructions:
15
+ - "非 OpenCode 直载形态(经 MCP 接入的 agent 客户端)调用 opx_* 工具时,须携带 `_agent` 参数并传自身角色名(如 openspec-developer);缺省会被视为编排主代理视角,将拿不到自身角色的执行视图与操作指引"
16
+ - "若当前工具列表中的编排工具带有 `mcp__<server>__` 前缀(MCP 接入形态,如 DeepSeek Harness / Claude Code / Codex / ZCode),调用时使用工具列表中的完整名称,例如 `mcp__opx__status`、`mcp__opx__agent_submit`;若工具列表中没有这些工具且存在 `dev_tool_search`,先用 `dev_tool_search` 搜索 `opx` 并一次性解锁全部需要的工具(含 `todo_write`);禁止用 shell 查找 opx/openspec/MCP 配置"
17
+ - "首次调用 opx_status 前不要探查仓库结构或状态文件;工具未解锁时只用 dev_tool_search 解锁,解锁后立即调用 opx_status"
18
+ - "调用 opx_status 获取动态上下文({{worktree_path}}、审查范围、上轮会话摘要)"
19
+ - "按 Skill 加载清单加载匹配的 skill 并遵循其全部规范"
20
+ - "阅读 `{{worktree_path}}/AGENTS.md` 项目规范"
21
+ - "需要外部框架/工具的权威文档(规则语义、API 用法、配置说明)时,优先通过文档检索渠道获取,禁止直接翻源码"
22
+ - "完成后调用 opx_agent_submit 提交裁决或失败上报"
23
+ constraints:
24
+ - "所有 edit/write 操作限定在 {{worktree_path}} 内,严禁修改主仓库路径下的文件"
25
+ - "禁止为获取外部框架/工具的规则语义、API 用法等知识而解析其源码、jar 包或执行 AST dump;此类知识一律通过文档检索渠道获取;声明式规则定义来源(规则定义 XML 直拉、工具官方 Web API 返回、官方文档页)属于文档检索渠道范畴"
26
+ - "不跳步骤、不自行推断阶段流转,next-step 以 opx_status 为准"
27
+ - "禁止调用任何 opx_orch_* 工具(编排者专属)"
28
+ - "审查/报告范围以视图「审查范围」区块为准:PR 区间形态为 base..head 区间 + 修复轮检查点增量;全量形态为整个代码库"
29
+
30
+ phases:
31
+ - name: in_progress
32
+ steps:
33
+ - id: implement
34
+ agents:
35
+ - id: openspec-developer
36
+ capability_tags: [efficiency, api-testing, quality-gate, dev-practices, db-design, style, architecture, performance, security, maintainability, cleanup]
37
+ transitions:
38
+ on_pass: verify_tool
39
+ on_fail: implement
40
+ instructions:
41
+ - "以视图 Issue 清单驱动修复:Low+ 必办(修复后经 fixed_issue_ids 上报进入待复核),Info 建议修复不阻塞提交;不可修 issue 经 exempt_issue_ids 申请豁免(申请后进入待裁定状态,由报源 reviewer 裁定,被驳回的 issue 回待修复);全部 blocking issue 均已上报修复或申请豁免后才可 passed 提交推进"
42
+ - "执行边界缺省整个 worktree(独立审查无分析环节,不生成边界白名单);issue 的 file 字段指向的文件即修复边界,修复这些文件(含回归引入问题)不算越界;可按 issue 的 suggestion 直接执行修复;reviewer 的 boundary_expansion 已并入边界"
43
+ - "对修复未过次数≥2 的 issue 须先完成 5-Why 根因分析再动手,不得跳过分析直接改代码"
44
+ - "修复完成后先 git commit 再提交;提交前按已加载的质量门类 skill 完成本地构建验证,并按维度规范类 skill(style/architecture/performance/security/maintainability 能力类别)对本次修复文件逐项自检,自检结论与工具检查结果一并汇总进 self_check_results"
45
+ - "自证/验证需启动服务或基础设施时,一律按已加载技术栈 skill 的隔离启动规范以隔离方式启动,隔离标识与建议端口以视图 Worktree 区块为准;验证完成后停止本次启动的实例,不留残留;禁止为腾出端口或资源而停止、复用或干扰非本会话启动的进程"
46
+ - "确定性质量工具检查出的问题不得以压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等)静默规避:确需压制时,必须在压制处代码注释说明清楚理由,并经 exempt_issue_ids 申请豁免由报源 reviewer 裁定;不得以 fixed_issue_ids 冒充修复"
47
+ - "提交 opx_agent_submit:必传 verdict;修复完成的 issue 经 fixed_issue_ids 上报、不可修 issue 经 exempt_issue_ids 申请豁免"
48
+ constraints:
49
+ - "本会话无任务清单门禁(无 tasks.md 子任务),修复范围仅由 issue 清单与检查结果界定"
50
+ - "生产路径禁止 stub/fake/空实现/硬编码成功"
51
+ - "提交前自检必须覆盖已加载的全部 skill(含维度规范类)检查点,不得以工具检查结果替代维度规范自检"
52
+ - "禁止以压制手段静默规避确定性质量工具问题:压制必须附明确理由并经豁免申请流程裁定,不得以工具不再报错为由跳过申报"
53
+ - name: review
54
+ steps:
55
+ - id: verify_tool
56
+ agents:
57
+ - id: openspec-reviewer-tool
58
+ capability_tags: [quality-gate, efficiency, api-testing]
59
+ transitions:
60
+ on_pass: verify_task
61
+ on_fail: implement
62
+ instructions:
63
+ - "依据 opx_status 提供的变更检测结果分流:无代码/配置变更且本层无待复核/待裁定项 → 直接提交 passed,不运行全量工具检查,并以一条 step 名首段为 no_change 的 validation_steps 声明本轮整体豁免必做清单(completed=false + 结构化 skip_reason 注明判定依据);无变更但有本层待复核/待裁定项 → 仅复核/裁定待处理项后提交,不跑全量工具扫描;有变更 → 加载质量门类 skill 获取工具清单与映射规则,按已加载质量门 skill 必做清单逐项执行全部确定性工具检查(代码格式/架构约束/静态分析/单元测试编译/深度扫描——按必做清单中的对应项逐项执行并逐项申报),将工具输出映射为统一 issue 结构后提交"
64
+ - "审查锚点按视图「审查范围」:PR 区间形态为 base..head 区间(首轮)或检查点增量(修复轮);全量形态为整个代码库——全量形态下的工具违规无论是否本轮引入一律映射为 issue,不得以不在变更清单为由丢弃"
65
+ - "「有变更」分支先审查上方「本次变更证据」区块的变更文件清单与区间 diff 内容后自行判断:若确认本次变更仅为注释/文档性且无逻辑影响,可自行决定跳过全量工具检查直接提交 passed(validation_steps 以 step=no_change 申报整体豁免必做清单);无法确认或含逻辑/结构变更,必须顺序运行全部确定性工具检查;裁量仅免除全量工具扫描,不免除复核/裁定——本层 blocking issue 仍须处理至终态后提交"
66
+ - "审查区间 diff 与 git log,识别变更中新增的压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等):视为豁免申请,核对是否已在压制处代码注释或 commit message 说明理由并申报豁免;未申报或理由不成立 → 报 issue(file=压制位置、line=压制行、rule=被压制规则名,severity 至少 Low);理由成立 → 经 exempt_adjudications 裁定 dismissed"
67
+ - "报 issue 时必须显式声明归因维度 dimension(style/architecture/performance/security/maintainability):本 reviewer 不归属任一质量维度,dimension 即该 issue 归属的目标质量维度"
68
+ - "复核本层已修复待复核(review 态)issue:通过置 done,不通过驳回(附驳回原因,issue 回待修复并累计修复未过次数)"
69
+ - "对视图「待裁定是否可豁免」区块中的豁免申请经 exempt_adjudications 裁定(dismissed/rejected)"
70
+ - "提交 opx_agent_submit,必带 verdict 参数(passed / failed)"
71
+ - "validation_steps 必须按已加载质量门 skill 必做清单逐项申报(completed=true 附执行结果;无法执行的必做项置 completed=false,并以结构化 skip_reason 申报降级理由,格式:{\"item\":\"<必做项>\",\"category\":\"<降级类别>\",\"adjudication\":\"user_response|unattended_auto|env_unavailable\",\"note\":\"<说明>\"});遗漏必做项或缺结构化降级理由的 passed 提交会被提交门禁拒绝"
72
+ - "报静态分析类 issue 时,rule 字段必填官方规则全名(命名格式遵循已加载的质量门类 skill 的规则导航索引),漏填将导致项目级豁免清单无法生效"
73
+ - "报静态分析类 issue 时,description 必须含:①复现命令(非静默形态,含必要开关);②实测数据(命中类清单/计数);③规则默认阈值与当前项目配置对比(引用可改的配置文件路径)"
74
+ constraints:
75
+ - "工具调用边界:仅可调用 opx_status、opx_agent_submit、question(需要用户协助处理时);可 bash 运行工具;禁止 edit/write 影响代码运行的内容;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
76
+ - "即使无 issue 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
77
+ - "Info 级 issue 的 description/suggestion 禁止阶段/时机表述(如'可后续处理')"
78
+ - "压制类 issue 严重级别至少 Low"
79
+ - id: verify_task
80
+ agents:
81
+ - id: openspec-reviewer-task
82
+ capability_tags: [api-testing, efficiency, dev-practices, cleanup]
83
+ transitions:
84
+ on_pass: verify_quality
85
+ on_fail: implement
86
+ instructions:
87
+ - "回归验证层(独立审查场景,无 spec 验收标准——以既有测试与行为基线为锚)逐项验证:①构建通过;②既有测试全量执行不引入新失败——首轮跑出的存量失败按 issue 上报进入处置通道(审并修模式进修复闭环或豁免,只审模式进报告),修复轮出现「上轮未登记的新失败」即回归、verdict=failed 并报 issue(说明失败用例与归因);③可启动服务时按已加载技术栈 skill 的隔离启动规范隔离启动做冒烟与既有 API 测试脚本执行(存在时),验证后幂等清理"
88
+ - "验证完成后(含 verdict=failed 轮次)按已加载的清理规范执行幂等清理:停止残留的服务实例、清理本会话的隔离环境、删除残留的隔离扫描项目;无残留即通过,清理结果纳入 validation_steps 申报"
89
+ - "无法启动服务或不存在 API 测试脚本时,③置 completed=false 并以结构化 skip_reason 申报(格式:{\"item\":\"<对应验证项>\",\"category\":\"<降级类别>\",\"adjudication\":\"user_response|unattended_auto|env_unavailable\",\"note\":\"<说明>\"}),注明判定依据"
90
+ - "报 issue 时必须显式声明归因维度 dimension(style/architecture/performance/security/maintainability):本 reviewer 不归属任一质量维度,dimension 即该 issue 归属的目标质量维度"
91
+ - "复核本层已修复待复核(review 态)issue:通过置 done,不通过驳回(附驳回原因,issue 回待修复并累计修复未过次数)"
92
+ - "对视图「待裁定是否可豁免」区块中的豁免申请经 exempt_adjudications 裁定"
93
+ - "按 opx_status 操作指引完成全部验证项,提交前将每项验证结果与证据填入 validation_steps 参数(未执行的项须填 skip_reason 注明原因),不得省略任何一项"
94
+ - "提交 opx_agent_submit,必带 verdict"
95
+ constraints:
96
+ - "工具调用边界:仅可调用 opx_status、opx_agent_submit;可 bash 启动服务/执行检查;禁止 edit/write 影响代码运行的内容;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
97
+ - "即使无 issue 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
98
+ - "禁止为腾出端口或资源而停止、复用或干扰非本会话启动的进程(含用户或其他会话运行的服务实例);验证所需环境一律按隔离标识隔离启动"
99
+ - id: verify_quality
100
+ agents:
101
+ - id: openspec-reviewer-style
102
+ capability_tags: [style, efficiency, tool-improvement]
103
+ - id: openspec-reviewer-architecture
104
+ capability_tags: [architecture, efficiency, tool-improvement]
105
+ - id: openspec-reviewer-performance
106
+ capability_tags: [performance, efficiency, tool-improvement]
107
+ - id: openspec-reviewer-security
108
+ capability_tags: [security, efficiency, tool-improvement]
109
+ - id: openspec-reviewer-maintainability
110
+ capability_tags: [maintainability, efficiency, tool-improvement]
111
+ transitions:
112
+ on_pass: done
113
+ on_fail: implement
114
+ instructions:
115
+ - "加载匹配维度 skill,审查锚点按视图「审查范围」(PR 区间形态为 base..head 区间 + 修复轮检查点增量,全量形态为整个代码库),结合 skill 规范与领域知识做拓展审查(skill 仅为最低基准);顺带发现的其他缺陷按本维度严重级别标准报 issue,禁止静默丢弃"
116
+ - "审查新 issue 前先查看视图「待复核」区块中的既有 issue(仅本维度报源),避免语义重复"
117
+ - "报本维度新 issue 前先判断该问题能否通过调整确定性质量扫描工具配置(规则收紧/新增规则/配置增强)统一收敛:可工具化 → 双报业务 issue(file=违规现场)与工具改进 issue(file=规则/配置文件,suggestion 含规则草案+验证命令,末尾标 [tool_eligible],且与业务 issue 同维度),两条 file 不同、方案不同,不构成语义重复,必须双报;不可工具化 → 仅报业务 issue。工具化判断细节与规则草案模板遵循已加载的工具改进类 skill,未加载时仅报业务 issue、跳过工具改进环节;提交时在 validation_steps 中记录工具化判断结论"
118
+ - "复核自己报的本维度已修复待复核(review 态)issue:通过置 done,不通过驳回(附驳回原因,issue 回待修复并累计修复未过次数)"
119
+ - "对视图「待裁定是否可豁免」区块中的豁免申请经 exempt_adjudications 裁定(dismissed/rejected)"
120
+ - "提交 opx_agent_submit,必带 verdict 与 validation_steps(验证步骤执行摘要)"
121
+ constraints:
122
+ - "禁止运行确定性工具检查(包括但不限于 linter/formatter/静态分析/编译/测试/架构约束检查等);运行权归工具检查层,本约束不排斥基于已加载 skill 做工具化可行性判断并提交工具改进 issue"
123
+ - "工具调用边界:仅可调用 opx_status、opx_agent_submit;禁止 edit/write 影响代码运行的内容,仅输出审查报告;文档/注释等不影响代码运行的内容发现不一致/过时问题时直接修正,不为此报 issue"
124
+ - "即使无 issue 也必须提交 verdict=passed;passed=false 时必须有至少一个归属本维度的 Low+ issue 作为理由"
@@ -94,7 +94,7 @@
94
94
 
95
95
  - **架构师**(analyze,方案审查):文档一致性复核与方案评估——以 design.md 为输入审查架构模式合规性与方案合理性,仅可编辑 md 文档直接修复文档问题,信息齐备时输出执行边界、存在缺口时提交 blockers
96
96
  - **工具维度审查者**(tool review,full 模式三层门禁第一层):执行确定性工具检查,将工具输出映射为统一 issue 结构;简单(simple)模式亦承担该职责
97
- - **任务维度审查者**(task review,full 模式三层门禁第二层):验证 task 产出完整性、启动服务并检查健康、独立执行 API 测试并审查质量、审查测试代码质量;验证完成后清理本变更启动的环境(停止残留服务、清理隔离环境);简单(simple)模式亦承担该职责
97
+ - **任务维度审查者**(task review,full 模式三层门禁第二层):验证 task 产出完整性、启动服务并检查健康、独立执行 API 测试并审查质量、审查测试代码质量;验证完成后清理本变更启动的环境(停止残留服务、清理隔离环境);简单(simple)模式亦承担该职责。独立审查会话(不绑定 change 的 thorough 颗粒度)中该身份为回归验证层——无 spec 验收标准,锚点为既有测试与行为基线(构建通过、既有测试不引入新失败、可启动服务时冒烟与既有 API 测试脚本执行);判例与严重级别体系沿用
98
98
  - **质量维度审查者**(quality review,full 模式三层门禁第三层):按分派维度(style / architecture / performance / security / maintainability)审查
99
99
  - **simple 合并审查者**:以分级复验与对抗审查承担 simple 模式合并审查——低成本确定性工具实跑,高成本项核验 dev 自检申报并以核心链路抽样重放验证,对抗性判断(测试代码质量、业务行为真实性、关键数据产出核验)保持独立视角不重走 dev 路径;验证完成后清理本变更启动的环境(停止残留服务、清理隔离环境)
100
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ifrankwang/openspec-agents",
3
- "version": "0.132.1",
3
+ "version": "0.133.1",
4
4
  "description": "OpenSpec change orchestration plugin for AI coding agents (Claude Code, Codex, ZCode, DeepSeek Harness, OpenCode)",
5
5
  "author": "ifrankwang",
6
6
  "license": "MIT",
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * 生成产物为一个完整插件目录:plugin.json 清单 + agents/*.md(从 assets/agents 转换、
8
8
  * 排除主代理模板、frontmatter 仅保留 name/description)+ skills/<名>/(含 orchestrator 与
9
- * reference/ 附属文件)+ assets/workflows/(task.yaml workflow 定义,bundle 内按部署深度
9
+ * reference/ 附属文件)+ assets/workflows/(task/review 系列 workflow 定义,bundle 内按部署深度
10
10
  * 逐级上溯探测读取)+ .mcp.json(stdio + 自包含 bundle 入口 + 当前项目 worktree + 默认
11
11
  * 无人值守)+ .mcp-server/cli.mjs 自包含 bundle(node 直接执行,不依赖 node_modules)。
12
12
  */
package/src/core/git.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import path from "path"
2
2
  import { execFile } from "node:child_process"
3
- import { readFile, writeFile, stat, rm } from "node:fs/promises"
3
+ import { readFile, writeFile, stat, rm, realpath } from "node:fs/promises"
4
4
 
5
5
  /** 判断路径是否存在于磁盘(fs 层判断,git 命令结果不作为存在性依据)。 */
6
6
  async function pathExists(p: string): Promise<boolean> {
@@ -157,6 +157,18 @@ export async function getMergeBase(worktree: string, baseBranch: string): Promis
157
157
  return runGit(worktree, ["merge-base", "HEAD", baseBranch])
158
158
  }
159
159
 
160
+ /** 校验本地分支存在(refs/heads/<name> 严格本地命名空间,不含远端跟踪 ref)。 */
161
+ export async function isLocalBranch(worktree: string, branch: string): Promise<boolean> {
162
+ const r = await runGitChecked(worktree, ["rev-parse", "--verify", `refs/heads/${branch}`])
163
+ return r.success
164
+ }
165
+
166
+ /** 列出全部本地分支名(refs/heads 命名空间)。 */
167
+ export async function listLocalBranches(worktree: string): Promise<string[]> {
168
+ const out = await runGit(worktree, ["for-each-ref", "--format=%(refname:short)", "refs/heads"])
169
+ return out.split("\n").map((l) => l.trim()).filter(Boolean)
170
+ }
171
+
160
172
  export async function isWorktreeClean(worktree: string): Promise<boolean> {
161
173
  const out = await runGit(worktree, ["status", "--porcelain"])
162
174
  return out.length === 0
@@ -205,30 +217,79 @@ export async function markTaskGroupCheckboxesComplete(
205
217
  }
206
218
 
207
219
  /**
208
- * 收尾裸合并:将任务组分支合并进基础分支,全程不触碰任何工作目录(worktreeless 底层命令)。
220
+ * 收尾合并:将任务组分支合并进基础分支。
209
221
  *
210
- * 主仓库工作区状态(当前分支、未提交改动、index)不受影响;冲突时目标分支引用未动、磁盘无半成品。
211
- * 序列:
222
+ * 先解析主仓库根(后续命令一律以主仓库根为 -C 目标,不沿用入参目录),再按目标分支的检出状态分流:
212
223
  * 1. `merge-base --is-ancestor` 幂等检查:源分支已是目标分支祖先(如人工解决冲突后重调)直接成功,
213
224
  * 不产生空合并提交;
214
- * 2. `merge-tree --write-tree`(需 git ≥ 2.38)内存试算合并树:退出码 1 = 冲突,直接返回;
215
- * 3. `commit-tree` 生成双父合并提交,`update-ref` 以试算前目标分支 oid 作旧值校验推进(CAS,防并发覆盖)。
225
+ * 2. 检出检测(`worktree list --porcelain`,按目标分支匹配 `branch refs/heads/<X>` 行):
226
+ * - 目标分支正被其它 linked worktree 检出,或正被主仓库检出且主仓库有未提交改动 →
227
+ * 返回 blockedMessage(未执行任何 git 变更动作,由上层转 blocked 文案)——
228
+ * 单靠 update-ref 推进分支指针会使检出方暂存区/工作区与分支引用失步(status 显示整套反向改动);
229
+ * - 目标分支正被主仓库检出且干净:仍先 `merge-tree --write-tree` 内存试算(冲突零副作用返回),
230
+ * 无冲突则执行真实 `git merge --no-ff`(由 git 维护检出方 index/工作区;失败时 `merge --abort`
231
+ * 兜底恢复后按冲突形态返回,理论上仅竞态会走到此分支);
232
+ * 3. 无人检出(worktreeless 底层命令,主仓库工作区状态不受影响;冲突时目标分支引用未动、磁盘无半成品):
233
+ * `merge-tree --write-tree`(需 git ≥ 2.38)内存试算合并树:退出码 1 = 冲突,直接返回;
234
+ * `commit-tree` 生成双父合并提交,`update-ref` 以试算前目标分支 oid 作旧值校验推进(CAS,防并发覆盖)。
216
235
  *
217
- * @param repoDir 仓库内任意目录(底层命令不读取工作区,不影响 HEAD/index)
236
+ * @param repoDir 仓库内任意目录(作为主仓库根解析起点)
218
237
  */
219
238
  export async function mergeBranchToTarget(
220
239
  repoDir: string,
221
240
  sourceBranch: string,
222
241
  targetBranch: string
223
- ): Promise<{ success: boolean; conflict: boolean }> {
224
- const ancestor = await runGitChecked(repoDir, ["merge-base", "--is-ancestor", sourceBranch, targetBranch])
242
+ ): Promise<{ success: boolean; conflict: boolean; blockedMessage?: string }> {
243
+ const mainRepo = (await resolveMainRepoRoot(repoDir)) ?? repoDir
244
+
245
+ const ancestor = await runGitChecked(mainRepo, ["merge-base", "--is-ancestor", sourceBranch, targetBranch])
225
246
  if (ancestor.success) return { success: true, conflict: false }
226
247
  // is-ancestor 退出码 1 = 尚未并入(继续合并);其余为分支缺失等真实错误
227
248
  if (ancestor.exitCode !== 1) {
228
249
  throw new Error(`无法判断 "${sourceBranch}" 是否已并入 "${targetBranch}":${ancestor.stderr}`)
229
250
  }
230
251
 
231
- const mergeTree = await runGitChecked(repoDir, ["merge-tree", "--write-tree", "--messages", targetBranch, sourceBranch])
252
+ const checkedOutPath = await findWorktreeCheckingOut(mainRepo, targetBranch)
253
+ if (checkedOutPath) {
254
+ if (!(await sameDir(checkedOutPath, mainRepo))) {
255
+ return {
256
+ success: false,
257
+ conflict: false,
258
+ blockedMessage: [
259
+ `- **原因**: 目标分支 \`${targetBranch}\` 正被工作树 \`${checkedOutPath}\` 检出;自动推进分支指针会使该工作树的暂存区/工作区与分支引用失步,本次未执行任何合并动作。`,
260
+ `- **处理**: 请在该工作树手动执行 \`git merge ${sourceBranch}\`,或移除该工作树(\`git worktree remove ${checkedOutPath}\`)后重试。`,
261
+ ].join("\n"),
262
+ }
263
+ }
264
+ const status = await runGit(mainRepo, ["status", "--porcelain"])
265
+ if (status.trim().length > 0) {
266
+ return {
267
+ success: false,
268
+ conflict: false,
269
+ blockedMessage: [
270
+ `- **原因**: 目标分支 \`${targetBranch}\` 正被主仓库检出,且主仓库存在未提交改动;为避免覆盖本地改动,本次未执行任何合并动作(分支引用未动、无半成品)。`,
271
+ `- **处理**: 请先 commit 或 stash 主仓库改动后重试;或自行执行 \`git merge ${sourceBranch}\`(真实合并自带脏工作区保护)。`,
272
+ ].join("\n"),
273
+ }
274
+ }
275
+ const mergeTree = await runGitChecked(mainRepo, ["merge-tree", "--write-tree", "--messages", targetBranch, sourceBranch])
276
+ if (!mergeTree.success) {
277
+ // merge-tree 退出码 1 = 冲突;其余为分支缺失等真实错误
278
+ if (mergeTree.exitCode !== 1) {
279
+ throw new Error(`无法试算 "${sourceBranch}" → "${targetBranch}" 的合并:${mergeTree.stderr}`)
280
+ }
281
+ return { success: false, conflict: true }
282
+ }
283
+ const mergeResult = await runGitChecked(mainRepo, ["merge", "--no-ff", sourceBranch])
284
+ if (!mergeResult.success) {
285
+ // 理论上仅竞态会走到此分支:兜底中止,避免主仓库停留在合并中间态
286
+ await runGitChecked(mainRepo, ["merge", "--abort"])
287
+ return { success: false, conflict: true }
288
+ }
289
+ return { success: true, conflict: false }
290
+ }
291
+
292
+ const mergeTree = await runGitChecked(mainRepo, ["merge-tree", "--write-tree", "--messages", targetBranch, sourceBranch])
232
293
  if (!mergeTree.success) {
233
294
  // merge-tree 退出码 1 = 冲突;其余为分支缺失等真实错误
234
295
  if (mergeTree.exitCode !== 1) {
@@ -239,17 +300,17 @@ export async function mergeBranchToTarget(
239
300
  const treeOid = mergeTree.stdout.split("\n")[0].trim()
240
301
  if (!treeOid) throw new Error("merge-tree 未返回树对象,无法生成合并提交。")
241
302
 
242
- const targetOid = (await runGit(repoDir, ["rev-parse", targetBranch])).trim()
243
- const sourceOid = (await runGit(repoDir, ["rev-parse", sourceBranch])).trim()
303
+ const targetOid = (await runGit(mainRepo, ["rev-parse", targetBranch])).trim()
304
+ const sourceOid = (await runGit(mainRepo, ["rev-parse", sourceBranch])).trim()
244
305
  if (!targetOid || !sourceOid) {
245
306
  throw new Error(`无法解析分支 OID:target="${targetOid}" source="${sourceOid}"`)
246
307
  }
247
- const commitOid = (await runGit(repoDir, [
308
+ const commitOid = (await runGit(mainRepo, [
248
309
  "commit-tree", treeOid, "-p", targetOid, "-p", sourceOid, "-m", `Merge branch '${sourceBranch}'`,
249
310
  ])).trim()
250
311
  if (!commitOid) throw new Error("git commit-tree 失败:无法创建合并提交。")
251
312
 
252
- const updateRef = await runGitChecked(repoDir, ["update-ref", `refs/heads/${targetBranch}`, commitOid, targetOid])
313
+ const updateRef = await runGitChecked(mainRepo, ["update-ref", `refs/heads/${targetBranch}`, commitOid, targetOid])
253
314
  if (!updateRef.success) {
254
315
  throw new Error(`基础分支 "${targetBranch}" 已被并发推进,合并提交未写入(update-ref 旧值校验失败):${updateRef.stderr}`)
255
316
  }
@@ -263,7 +324,8 @@ export async function discoverDiskWorktrees(worktree: string): Promise<{ branch:
263
324
  const m = line.match(/^(\S+)\s+[0-9a-f]+\s+\[(.+?)\]/)
264
325
  if (m) {
265
326
  const branch = m[2].trim()
266
- if (branch.startsWith("task-group/")) {
327
+ // 独立审查会话 worktree 分支(review/<sessionId>)与任务组分支(task-group/)同为可恢复磁盘痕迹
328
+ if (branch.startsWith("task-group/") || branch.startsWith("review/")) {
267
329
  result.push({ branch, path: m[1].trim() })
268
330
  }
269
331
  }
@@ -315,18 +377,53 @@ function parsePorcelainPaths(out: string): string[] {
315
377
  }
316
378
 
317
379
  /**
318
- * 检测主仓库下 openspec 文档是否存在未提交变更(修改/新增/改名),用于编排者视图的主分支污染诊断。
319
- * 兼容两种形态:`.git` 为目录(入参即主仓库,repoRoot 取自身)与 `.git` 为文件(走 discoverRepoRoot)。
320
- * 无法解析主仓库路径或主仓库干净时返回 null。
380
+ * 解析主仓库根:`.git` 为目录则该目录即主仓库根;`.git` 为文件(linked worktree)时经 discoverRepoRoot 推导。
381
+ * 解析失败(非 git 仓库等)返回 null,降级行为由调用方决定。
321
382
  */
322
- export async function detectMainRepoPollution(worktreePath: string): Promise<{ repoRoot: string; files: string[] } | null> {
323
- let repoRoot: string
383
+ async function resolveMainRepoRoot(worktreePath: string): Promise<string | null> {
324
384
  try {
325
385
  const st = await stat(path.join(worktreePath, ".git"))
326
- repoRoot = st.isDirectory() ? worktreePath : await discoverRepoRoot(worktreePath)
386
+ return st.isDirectory() ? worktreePath : await discoverRepoRoot(worktreePath)
327
387
  } catch {
328
388
  return null
329
389
  }
390
+ }
391
+
392
+ /** 跨 symlink 比较两路径是否指向同一目录(macOS /tmp → /private/tmp 等形态);realpath 失败退回字面 resolve 比较。 */
393
+ async function sameDir(a: string, b: string): Promise<boolean> {
394
+ try {
395
+ return (await realpath(a)) === (await realpath(b))
396
+ } catch {
397
+ return path.resolve(a) === path.resolve(b)
398
+ }
399
+ }
400
+
401
+ /**
402
+ * 在 `worktree list --porcelain` 输出中查找检出指定分支的 worktree 路径。
403
+ * porcelain 以空行分块,每块 `worktree <path>` 开头;bare 或 detached 块无 `branch` 行,跳过。
404
+ */
405
+ async function findWorktreeCheckingOut(repoRoot: string, branch: string): Promise<string | null> {
406
+ const out = await runGit(repoRoot, ["worktree", "list", "--porcelain"])
407
+ for (const block of out.split("\n\n")) {
408
+ let wtPath = ""
409
+ let wtBranch = ""
410
+ for (const line of block.split("\n")) {
411
+ if (line.startsWith("worktree ")) wtPath = line.slice("worktree ".length).trim()
412
+ else if (line.startsWith("branch ")) wtBranch = line.slice("branch ".length).trim()
413
+ }
414
+ if (wtPath && wtBranch === `refs/heads/${branch}`) return wtPath
415
+ }
416
+ return null
417
+ }
418
+
419
+ /**
420
+ * 检测主仓库下 openspec 文档是否存在未提交变更(修改/新增/改名),用于编排者视图的主分支污染诊断。
421
+ * 兼容两种形态:`.git` 为目录(入参即主仓库,repoRoot 取自身)与 `.git` 为文件(走 discoverRepoRoot)。
422
+ * 无法解析主仓库路径或主仓库干净时返回 null。
423
+ */
424
+ export async function detectMainRepoPollution(worktreePath: string): Promise<{ repoRoot: string; files: string[] } | null> {
425
+ const repoRoot = await resolveMainRepoRoot(worktreePath)
426
+ if (!repoRoot) return null
330
427
  // 健壮性:git 调用失败(如非 git 仓库/权限/损坏)时返回 null,不使 opx_status 整体抛错;
331
428
  // 污染诊断属编排者视图的辅助信息,缺失不应阻断状态视图渲染。
332
429
  let out: string
@@ -343,6 +440,8 @@ export async function detectMainRepoPollution(worktreePath: string): Promise<{ r
343
440
  export interface DetectChangesResult {
344
441
  files: string[]
345
442
  hasNonDocChange: boolean
443
+ /** 全量审查哨兵:kind=review 且 scopeType=full 时置 true(审查锚点为全量代码库,非「未检出变更」)。 */
444
+ scopeFull?: boolean
346
445
  }
347
446
 
348
447
  /**
@@ -361,11 +460,14 @@ export interface DetectChangesResult {
361
460
  * @param worktreePath worktree 路径
362
461
  * @param opts.checkpoint 上次工具检查点的 commit sha;undefined/null 视为无检查点
363
462
  * @param opts.baseRef 无检查点时的兜底基准 ref(worktree 创建时的 merge-base)
463
+ * @param opts.scopeFull 全量审查哨兵(独立审查会话 full 形态):不界定变更区间,直接返回
464
+ * scopeFull=true 供视图渲染「全量代码库」锚点(而非误报「未检出变更」)
364
465
  */
365
466
  export async function detectChanges(
366
467
  worktreePath: string,
367
- opts: { checkpoint?: string | null; baseRef?: string | null },
468
+ opts: { checkpoint?: string | null; baseRef?: string | null; scopeFull?: boolean },
368
469
  ): Promise<DetectChangesResult> {
470
+ if (opts.scopeFull) return { files: [], hasNonDocChange: false, scopeFull: true }
369
471
  const files: string[] = []
370
472
  let gitFailed = false
371
473
  const range = opts.checkpoint || opts.baseRef || undefined
package/src/core/state.ts CHANGED
@@ -274,6 +274,9 @@ export async function readStateByChangeId(worktree: string, changeId: string): P
274
274
  // 透传固化模式:读取不改动 mode(缺 mode 的旧变更保持缺省,消费端读时兜底 full,不在此写回);
275
275
  // init 在允许窗口(切组/重制当前组,其他任务组均终态或从未激活)内可显式更新 state.mode
276
276
  mode: legacy.mode,
277
+ // 独立审查会话透传:kind/reviewScope 由 init 固化,缺省视为 change 会话(读时兜底,不写回旧 state)
278
+ kind: legacy.kind,
279
+ reviewScope: legacy.reviewScope,
277
280
  }
278
281
  if (needsUpgrade) {
279
282
  // 迁移产物一次性落盘固定单轨形态,避免每次读取都重建(首次写新目录前幂等迁移旧数据)