@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.
@@ -202,25 +202,58 @@ export const recoverySchema: JSONSchema = {
202
202
  export const orchInitSchema: JSONSchema = {
203
203
  type: "object",
204
204
  properties: {
205
- change_id: { type: "string", minLength: 1, description: "OpenSpec 变更 ID" },
205
+ change_id: { type: "string", minLength: 1, description: "OpenSpec 变更 ID(change 会话入口;与 review_scope 二选一)" },
206
206
  task_group_id: {
207
207
  type: "string",
208
208
  minLength: 1,
209
- description: "要初始化的任务组 ID。无 recovery 重复调用当前组时保留进度;切换任务组时仅初始化目标组。",
209
+ description: "要初始化的任务组 ID(change 会话入口)。无 recovery 重复调用当前组时保留进度;切换任务组时仅初始化目标组。独立审查会话恢复时可不传。",
210
210
  },
211
211
  base_branch: {
212
212
  type: "string",
213
- description: "基准分支名(如 main、develop),用于计算 merge-base 和 worktree fork 源。未传则自动从当前 git 分支推导。",
213
+ description: "基准分支名(如 main、develop),用于计算 merge-base 和 worktree fork 源。未传则自动从当前 git 分支推导。仅 change 会话入口有效。",
214
214
  },
215
215
  recovery: recoverySchema,
216
216
  mode: {
217
217
  type: "string",
218
218
  enum: ["full", "simple"],
219
219
  description:
220
- "流程模式选择:full=完整流程(analyze→implement→三重审查+收尾验证);simple=精简流程(implement→quality_review→done,缺省)。首次新建编排状态时固化;已开始的变更仅在允许窗口内可更新:切换任务组(其他任务组均已完成或从未激活)或 recovery.phase=task_analysis 重制当前组(其他任务组同样须已完成或从未激活);其余场景传不同 mode 将报错。",
220
+ "流程模式选择(仅 change 会话入口有效,独立审查会话不使用):full=完整流程(analyze→implement→三重审查+收尾验证);simple=精简流程(implement→quality_review→done,缺省)。首次新建编排状态时固化;已开始的变更仅在允许窗口内可更新:切换任务组(其他任务组均已完成或从未激活)或 recovery.phase=task_analysis 重制当前组(其他任务组同样须已完成或从未激活);其余场景传不同 mode 将报错。",
221
+ },
222
+ review_scope: {
223
+ type: "object",
224
+ description:
225
+ "独立审查会话入口(不绑定 OpenSpec change):与 change_id/task_group_id 互斥——同传报错、都不传报错,两种入口二选一。会话 id 由审查范围确定性推导并在返回体回传,后续所有工具以 change_id 形式传入该会话 id。",
226
+ properties: {
227
+ scope_type: {
228
+ type: "string",
229
+ enum: ["pr", "full"],
230
+ description: "审查范围:pr=按 base_ref..head_ref 本地分支区间审查;full=全量代码库审查",
231
+ },
232
+ base_ref: {
233
+ type: "string",
234
+ minLength: 1,
235
+ description: "pr 形态的基准本地分支。缺省自动推导(优先 main、其次 master)。须为本地分支(origin/* 等远端 ref 不受支持——收尾合并只推进本地分支引用)",
236
+ },
237
+ head_ref: {
238
+ type: "string",
239
+ minLength: 1,
240
+ description: "pr 形态的头部本地分支(审查目标分支,pr 必传)。须为本地分支(origin/* 等远端 ref 不受支持)",
241
+ },
242
+ granularity: {
243
+ type: "string",
244
+ enum: ["simple", "thorough"],
245
+ description: "审查颗粒度:simple=单层合并审查;thorough=三层审查(工具检查→回归验证→五维度质量审查)",
246
+ },
247
+ fix: {
248
+ type: "string",
249
+ enum: ["none", "fix"],
250
+ description: "修复策略:none=只审不修(issue 报告即交付物,审查失败直接收口出报告);fix=审并修(审查失败回退修复,修复后重审)",
251
+ },
252
+ },
253
+ required: ["scope_type", "granularity", "fix"],
254
+ additionalProperties: false,
221
255
  },
222
256
  },
223
- required: ["change_id", "task_group_id"],
224
257
  additionalProperties: false,
225
258
  }
226
259
 
@@ -17,8 +17,18 @@ export interface ToolResult {
17
17
  }
18
18
 
19
19
  export interface InitParams {
20
- change_id: string
21
- task_group_id: string
20
+ /** change 会话入口必传;独立审查入口(review_scope)不传(会话 id 由 review_scope 推导并在返回体回传)。 */
21
+ change_id?: string
22
+ /** change 会话入口必传;独立审查会话用固定虚拟组 review(recovery 时可缺省)。 */
23
+ task_group_id?: string
24
+ /** 独立审查会话入口:与 change_id/task_group_id 互斥(同传报错、都不传报错)。 */
25
+ review_scope?: {
26
+ scope_type: "pr" | "full"
27
+ base_ref?: string
28
+ head_ref?: string
29
+ granularity: "simple" | "thorough"
30
+ fix: "none" | "fix"
31
+ }
22
32
  base_branch?: string
23
33
  recovery?: {
24
34
  phase: BuildPhaseTarget
package/src/core/types.ts CHANGED
@@ -34,6 +34,27 @@ export type QualityLayerProgress = Record<ReviewDimension, DimensionVerdict>
34
34
  /** 流程模式:full(默认,analyze → implement → 三重审查+收尾验证)或 simple(implement → quality_review → done)。 */
35
35
  export type WorkflowMode = "full" | "simple"
36
36
 
37
+ /** 独立审查会话的审查范围类型:pr=按 base..head 分支区间审查,full=全量代码库审查。 */
38
+ export type ReviewScopeType = "pr" | "full"
39
+ /** 独立审查颗粒度:thorough=三层审查(verify_tool → verify_task → verify_quality),simple=单层合并审查(quality_review)。 */
40
+ export type ReviewGranularity = "simple" | "thorough"
41
+ /** 独立审查修复策略:none=只审不修(issue 报告即交付物),fix=审并修(失败回 implement 修复闭环)。 */
42
+ export type ReviewFixPolicy = "none" | "fix"
43
+
44
+ /** 独立审查会话的审查范围(opx_orch_init 的 review_scope 参数固化,不绑定 OpenSpec change)。 */
45
+ export interface ReviewScope {
46
+ scopeType: ReviewScopeType
47
+ /** pr 形态的基准本地分支(init 推导后固化;full 形态缺省)。 */
48
+ baseRef?: string
49
+ /** pr 形态的头部本地分支(审查目标分支;full 形态缺省)。 */
50
+ headRef?: string
51
+ granularity: ReviewGranularity
52
+ fix: ReviewFixPolicy
53
+ }
54
+
55
+ /** 独立审查会话的固定虚拟任务组 id(workItem id 为 task:review,无 tasks.md / task children)。 */
56
+ export const REVIEW_TASK_GROUP_ID = "review"
57
+
37
58
  export interface ExecutionBoundary {
38
59
  allowed_directories: string[]
39
60
  allowed_packages: string[]
@@ -144,4 +165,9 @@ export interface OrchestrateState {
144
165
  * (切组且其他任务组均已完成或从未激活,或 recovery.phase=task_analysis 重制当前组)内
145
166
  * 可被 mode 参数更新。 */
146
167
  mode?: WorkflowMode
168
+ /** 会话形态:review=独立审查会话(不绑定 OpenSpec change,无 tasks.md 解析 / mode 字段);
169
+ * 缺省视为 change 会话(读时兜底,不写回旧 state)。workflow 选择按 kind 优先于 mode。 */
170
+ kind?: "review"
171
+ /** kind=review 时的审查范围(granularity 决定 workflow 文件、fix 决定 fail 方向转移策略)。 */
172
+ reviewScope?: ReviewScope
147
173
  }
package/src/core/views.ts CHANGED
@@ -108,7 +108,20 @@ export function renderWorktreeSection(
108
108
  if (tg.worktreePath) {
109
109
  lines.push(`- **路径**: \`${tg.worktreePath}\``)
110
110
  lines.push(`- **分支**: \`${tg.branchName || "(none)"}\``)
111
- if (tg.baseRef) lines.push(`- **变更范围**: 用 \`git -C ${tg.worktreePath} diff --name-only ${tg.baseRef}..HEAD\` 查询本 change 全部已提交变更文件`)
111
+ if (state.kind === "review") {
112
+ // 独立审查会话:渲染「审查范围」锚点——pr 形态为 base..head 区间(base_ref 存 merge-base 锚点),
113
+ // full 形态为全量代码库(base_ref 不设,不得渲染为「未检出变更」)
114
+ if (state.reviewScope?.scopeType === "pr" && tg.baseRef) {
115
+ lines.push(
116
+ `- **审查范围**: PR 区间审查(${state.reviewScope.baseRef}..${state.reviewScope.headRef})——` +
117
+ `用 \`git -C ${tg.worktreePath} diff --name-only ${tg.baseRef}..HEAD\` 查询本区间已提交变更文件`,
118
+ )
119
+ } else {
120
+ lines.push("- **审查范围**: 全量代码库审查(无区间界定,工具检查与审查覆盖整个代码库)")
121
+ }
122
+ } else if (tg.baseRef) {
123
+ lines.push(`- **变更范围**: 用 \`git -C ${tg.worktreePath} diff --name-only ${tg.baseRef}..HEAD\` 查询本 change 全部已提交变更文件`)
124
+ }
112
125
  lines.push("- **⚠️ 约束**: 所有读写和 git 操作均在此目录下进行;严禁直接修改主仓库/主分支路径下的文件(如 `<repo>/openspec/...`)")
113
126
  lines.push("- **路径解析**: 推荐阅读文档均为相对 worktree 路径的引用,一律以 worktree 路径为基准解析,禁止从主仓库根目录解析")
114
127
  } else {
@@ -310,6 +310,16 @@ export function suspendItem(item: WorkItem, reason: string): void {
310
310
 
311
311
  export type TransitionDirection = "pass" | "fail"
312
312
 
313
+ /** fix=none 只审会话的修复策略 metadata 键(引擎无 state 访问,item.metadata 为引擎消费单一事实源,
314
+ * 由 init 从 state.reviewScope.fix 写入)。 */
315
+ export const REVIEW_FIX_POLICY_KEY = "review_fix_policy"
316
+
317
+ /** 读取 item 的独立审查修复策略(非 review 会话返回 undefined)。 */
318
+ export function reviewFixPolicyOf(item: WorkItem): "none" | "fix" | undefined {
319
+ const v = item.metadata[REVIEW_FIX_POLICY_KEY]
320
+ return v === "none" || v === "fix" ? v : undefined
321
+ }
322
+
313
323
  export interface TransitionResult {
314
324
  advanced: boolean
315
325
  reason?: string
@@ -326,6 +336,18 @@ function phaseOrderIndex(phase: WorkItemPhase): number {
326
336
  return WORK_ITEM_PHASES.indexOf(phase)
327
337
  }
328
338
 
339
+ /** done 转移的统一收口:不走 forwardGatePassed(短路),置 done 前显式检查 task children 全部终态
340
+ * (未完成子任务不得收尾;issue blocking 检查已在 stepCanPass 处理;review 会话无 task children 天然通过)。 */
341
+ function applyDoneTransition(item: WorkItem): TransitionResult {
342
+ const unfinishedTasks = item.children.filter((child) => child.type === "task" && !isTerminalPhase(child.phase))
343
+ if (unfinishedTasks.length > 0) {
344
+ return { advanced: false, reason: "存在未完成的子任务(task children 未达终态),无法进入 done。" }
345
+ }
346
+ item.phase = "done"
347
+ item.currentStep = null
348
+ return { advanced: true, target: "done" }
349
+ }
350
+
329
351
  /**
330
352
  * 沿当前 step 的 transition 推进状态机。
331
353
  * - done: item 进入终态(phase=done)并触发写回
@@ -346,16 +368,15 @@ export function applyTransition(
346
368
 
347
369
  const target = direction === "pass" ? current.step.transitions.on_pass : current.step.transitions.on_fail
348
370
 
371
+ // fix=none 只审会话:fail 方向转移目标 implement 为拓扑统一声明的死 step(review workflow 的
372
+ // on_fail 一律写 implement),策略短路直达 done——不可拦截转移:failed tag 滞留会让审查者被
373
+ // 无限重派死锁。done 收口只检查 task children 终态,review 会话无 task children 天然通过,
374
+ // issue 报告即交付物(收尾 openIssues 门禁对 fix=none 放宽)。
375
+ if (direction === "fail" && target === "implement" && reviewFixPolicyOf(item) === "none") {
376
+ return applyDoneTransition(item)
377
+ }
349
378
  if (target === "done") {
350
- // done 转移不走 forwardGatePassed(短路),置 done 前须显式检查 task children 全部终态
351
- // (未完成子任务不得收尾;issue blocking 检查已在 stepCanPass 处理)。
352
- const unfinishedTasks = item.children.filter((child) => child.type === "task" && !isTerminalPhase(child.phase))
353
- if (unfinishedTasks.length > 0) {
354
- return { advanced: false, reason: "存在未完成的子任务(task children 未达终态),无法进入 done。" }
355
- }
356
- item.phase = "done"
357
- item.currentStep = null
358
- return { advanced: true, target: "done" }
379
+ return applyDoneTransition(item)
359
380
  }
360
381
  if (target === "halt") {
361
382
  suspendItem(item, "halt")
@@ -510,6 +531,17 @@ export function recommendForItem(item: WorkItem, workflow: LoadedWorkflow): Engi
510
531
  }
511
532
  }
512
533
 
534
+ // fix=none 只审会话防御:fail 方向已被策略短路映射 done,implement 逻辑上不可达;
535
+ // 若因状态异常(如 recovery 误置)落进 implement,不推荐分派(否则只审会话出现修复动作)。
536
+ if (current.step.id === "implement" && reviewFixPolicyOf(item) === "none") {
537
+ return {
538
+ status: "blocked",
539
+ stepId: current.step.id,
540
+ agents: [],
541
+ blockedReason: "只审(fix=none)会话不可进入 implement(修复步骤逻辑上不可达),请核对 state 后按需调用 opx_orch_init(recovery=...) 恢复。",
542
+ }
543
+ }
544
+
513
545
  if (checkpointTriggered(item, workflow, current.step)) {
514
546
  return {
515
547
  status: "checkpoint",
@@ -54,12 +54,21 @@ export function resolveTaskWorkflowPath(moduleUrl: string): string {
54
54
  export const TASK_WORKFLOW_PATH = resolveTaskWorkflowPath(import.meta.url)
55
55
  /** simple 流程文件(implement → quality_review → done),与 task.yaml 同目录随插件 bundle 分发。 */
56
56
  export const SIMPLE_WORKFLOW_PATH = resolveWorkflowFilePath(import.meta.url, "task-simple.yaml")
57
+ /** 独立审查会话(thorough 颗粒度)流程文件:三层审查 + implement 修复闭环。 */
58
+ export const REVIEW_WORKFLOW_PATH = resolveWorkflowFilePath(import.meta.url, "review.yaml")
59
+ /** 独立审查会话(simple 颗粒度)流程文件:单层合并审查 + implement 修复闭环。 */
60
+ export const REVIEW_SIMPLE_WORKFLOW_PATH = resolveWorkflowFilePath(import.meta.url, "review-simple.yaml")
57
61
 
58
62
  /**
59
- * 按 state.mode 选择 workflow 文件:simple → task-simple.yaml,其余(full 或旧 state 缺 mode)→ task.yaml。
60
- * 旧 state 无 mode 字段一律按 full 处理(读时兜底,不写回),与 D2 固化语义一致。
63
+ * 选择 workflow 文件:kind 优先于 mode——
64
+ * - kind=review(独立审查会话,无 mode 字段):按 reviewScope.granularity 选 review.yaml / review-simple.yaml;
65
+ * - change 会话:mode=simple → task-simple.yaml,其余(full 或旧 state 缺 mode)→ task.yaml。
66
+ * 旧 state 无 mode 一律按 full 处理(读时兜底,不写回),与 D2 固化语义一致。
61
67
  */
62
- export function resolveWorkflowPath(state: Pick<OrchestrateState, "mode">): string {
68
+ export function resolveWorkflowPath(state: Pick<OrchestrateState, "mode" | "kind" | "reviewScope">): string {
69
+ if (state.kind === "review") {
70
+ return state.reviewScope?.granularity === "simple" ? REVIEW_SIMPLE_WORKFLOW_PATH : REVIEW_WORKFLOW_PATH
71
+ }
63
72
  return state.mode === "simple" ? SIMPLE_WORKFLOW_PATH : TASK_WORKFLOW_PATH
64
73
  }
65
74
 
@@ -71,7 +71,7 @@ export function renderWorkflowStatusView(
71
71
  return renderSuspended(item)
72
72
  }
73
73
  if (item.phase === "done" || item.phase === "cancelled") {
74
- return renderTerminalPhase(item)
74
+ return renderTerminalPhase(item, options.state)
75
75
  }
76
76
  if (rec.status === "blocked") {
77
77
  if (caller.orchestrator) {
@@ -155,8 +155,11 @@ function renderSuspended(item: WorkItem): string {
155
155
  ].join("\n")
156
156
  }
157
157
 
158
- /** 终态渲染:done 区分已完成(completed_at 已写)与待收尾;cancelled 呈现已取消。 */
159
- function renderTerminalPhase(item: WorkItem): string {
158
+ /** 终态渲染:done 区分已完成(completed_at 已写)与待收尾;cancelled 呈现已取消。
159
+ * 独立审查会话(kind=review)待收口时渲染审查结果报告视图(issue 按严重级别/维度/文件位置归并,
160
+ * 只审模式的交付物)——fix=none 下 issue 停留 todo 态、step tag=failed、item=done 为合法组合,
161
+ * 报告视图不渲染豁免/复核操作提示(该通道在只审模式下是死路,无修复者消费裁定结论)。 */
162
+ function renderTerminalPhase(item: WorkItem, state?: OrchestrateState): string {
160
163
  if (item.phase === "cancelled") {
161
164
  return [
162
165
  "# 🚫 任务组已取消",
@@ -165,6 +168,27 @@ function renderTerminalPhase(item: WorkItem): string {
165
168
  "",
166
169
  ].join("\n")
167
170
  }
171
+ if (state?.kind === "review") {
172
+ if (item.metadata["completed_at"] !== undefined) {
173
+ return [
174
+ "# ✅ 独立审查会话已完成",
175
+ "",
176
+ `- **完成时间**: ${item.metadata["completed_at"]}`,
177
+ "",
178
+ "审查结果摘要见收尾返回体(opx_orch_complete_task_group)。",
179
+ "",
180
+ ].join("\n")
181
+ }
182
+ const lines = [
183
+ "# 🏁 独立审查已完成,待收尾",
184
+ "",
185
+ renderReviewReport(item),
186
+ "",
187
+ "全部审查层已收口。调用 `opx_orch_complete_task_group` 完成收尾(只审模式销毁 worktree 不合并,审并修模式合并回目标分支)。",
188
+ "",
189
+ ]
190
+ return lines.join("\n")
191
+ }
168
192
  if (item.metadata["completed_at"] !== undefined) {
169
193
  return [
170
194
  "# ✅ 任务组已完成",
@@ -183,6 +207,42 @@ function renderTerminalPhase(item: WorkItem): string {
183
207
  ].join("\n")
184
208
  }
185
209
 
210
+ /** 独立审查报告视图:issue 按严重级别/维度/文件位置归并的最终报告形态(views 同构渲染共享)。 */
211
+ function renderReviewReport(item: WorkItem): string {
212
+ const issues = issueChildrenOf(item)
213
+ if (issues.length === 0) return "未报出任何 issue。"
214
+ const bySeverity = new Map<string, number>()
215
+ const byDimension = new Map<string, number>()
216
+ for (const c of issues) {
217
+ const sev = c.severity ?? "Info"
218
+ const f = resolveChildIssueFields(c)
219
+ bySeverity.set(sev, (bySeverity.get(sev) ?? 0) + 1)
220
+ byDimension.set(f.dimension, (byDimension.get(f.dimension) ?? 0) + 1)
221
+ }
222
+ const lines = [
223
+ "## 审查结果报告",
224
+ "",
225
+ `- **issue 总数**: ${issues.length}`,
226
+ `- **按严重级别**: ${[...bySeverity.entries()].map(([k, v]) => `${k}=${v}`).join("、")}`,
227
+ `- **按维度**: ${[...byDimension.entries()].map(([k, v]) => `${k}=${v}`).join("、")}`,
228
+ "",
229
+ ]
230
+ for (const sev of ["Critical", "High", "Medium", "Low", "Info"]) {
231
+ const group = issues.filter((c) => (c.severity ?? "Info") === sev)
232
+ if (group.length === 0) continue
233
+ lines.push(`### ${sev}(${group.length} 个)`, "")
234
+ for (const c of group) {
235
+ const id = c.externalId ?? c.id.replace(/^issue:/, "")
236
+ const f = resolveChildIssueFields(c)
237
+ lines.push(`- Issue #${id} | 维度:${f.dimension}${f.file ? ` | \`${formatFilePath(f.file, f.line)}\`` : ""}`)
238
+ lines.push(` - 描述:${c.description}`)
239
+ if (typeof c.metadata["suggestion"] === "string") lines.push(` - 建议:${c.metadata["suggestion"]}`)
240
+ }
241
+ lines.push("")
242
+ }
243
+ return lines.join("\n")
244
+ }
245
+
186
246
  /** blocked 视图:调用者若为当前 step 轮次 agent 用 blocked_agent 文案,否则用通用 blocked 文案。
187
247
  * verify_* 层当前 agent 且存在本层待复核(review 态)blocking issue 时,列出待复核清单并给出
188
248
  * recheck_adjudications 自助恢复指引(上次提交漏带复核导致门禁阻塞的可补交解除);
@@ -499,7 +559,8 @@ function renderAgentWorking(
499
559
 
500
560
  // tool review 检查点增量三分支(A4):仅 verify_tool 的 reviewer-tool 且提供了变更检测结果时生效;
501
561
  // toolChanges 缺省(未预计算)时维持既有全量渲染,不误伤其他 agent/step 的通用渲染。
502
- if (step?.id === "verify_tool" && agentToReviewLayer(ctxAgent) === "tool" && toolChanges) {
562
+ // 独立审查 full 形态(scopeFull 哨兵)不走三分支——审查锚点为全量代码库,非「未检出变更」。
563
+ if (step?.id === "verify_tool" && agentToReviewLayer(ctxAgent) === "tool" && toolChanges && !toolChanges.scopeFull) {
503
564
  const { active, pending } = toolPendingChildren(item, ctxAgent)
504
565
  if (!toolChanges.hasNonDocChange && active.length === 0 && pending.length === 0) {
505
566
  return renderToolDirectSubmit(exemptedHits)
@@ -564,7 +625,9 @@ function renderAgentWorking(
564
625
  // test_results),作为分级复验的事实输入——低成本实跑对照、高成本项核验申报+抽样重放均以该申报为
565
626
  // 证据源。仅 quality_review step 渲染(消费方是 reviewer);implement 视图与其余 step 视图不受影响
566
627
  //(dev 自己刚产出该申报,无需回显)。
567
- if (step?.id === "quality_review") {
628
+ // 开发者自检申报区块仅 change 会话渲染(simple 模式 quality_review 的分级复验事实输入);
629
+ // 独立审查会话无该数据源(无 dev 实施申报语义),不渲染空区块。
630
+ if (step?.id === "quality_review" && state.kind !== "review") {
568
631
  lines.push(...renderDevSelfCheckDeclaration(item.metadata))
569
632
  }
570
633
  lines.push(...renderAgentSummaries(readAgentSummaries(item), ctxAgent))
@@ -843,6 +906,14 @@ function renderToolChangesEvidence(
843
906
  const baseRef = tg.baseRef ?? undefined
844
907
  const rangeRef = checkpoint ?? baseRef
845
908
  const lines = [`## 本次变更证据(${label.title})`, ""]
909
+ // 独立审查 full 形态哨兵:审查锚点为全量代码库(无区间可界定),不渲染「未检出变更」误报
910
+ if (toolChanges.scopeFull) {
911
+ lines.push(
912
+ "- **口径**: 全量代码库审查(独立审查 full 形态)——无区间界定,工具检查与审查覆盖整个代码库",
913
+ "",
914
+ )
915
+ return [lines.join("\n")]
916
+ }
846
917
  if (checkpoint) {
847
918
  lines.push(
848
919
  `- **口径**: 本次为「${label.title}(${checkpoint}..HEAD)」的增量区间(含已提交与未提交的非 openspec 变更);` +