@ifrankwang/openspec-agents 0.118.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.
Files changed (84) hide show
  1. package/README.md +91 -0
  2. package/assets/agents/openspec-architect.md +61 -0
  3. package/assets/agents/openspec-developer.md +21 -0
  4. package/assets/agents/openspec-main.md +35 -0
  5. package/assets/agents/openspec-reviewer-architecture.md +60 -0
  6. package/assets/agents/openspec-reviewer-maintainability.md +59 -0
  7. package/assets/agents/openspec-reviewer-performance.md +49 -0
  8. package/assets/agents/openspec-reviewer-security.md +51 -0
  9. package/assets/agents/openspec-reviewer-style.md +50 -0
  10. package/assets/agents/openspec-reviewer-task.md +40 -0
  11. package/assets/agents/openspec-reviewer-tool.md +30 -0
  12. package/assets/dashboard/index.html +295 -0
  13. package/assets/skills/api-conventions/SKILL.md +60 -0
  14. package/assets/skills/api-test/SKILL.md +175 -0
  15. package/assets/skills/architecture-evolution/SKILL.md +60 -0
  16. package/assets/skills/db-conventions/SKILL.md +62 -0
  17. package/assets/skills/ddd-architecture/SKILL.md +261 -0
  18. package/assets/skills/environment-cleanup/SKILL.md +44 -0
  19. package/assets/skills/java-api-conventions/SKILL.md +30 -0
  20. package/assets/skills/java-code-maintainability/SKILL.md +62 -0
  21. package/assets/skills/java-code-performance/SKILL.md +49 -0
  22. package/assets/skills/java-code-style/SKILL.md +54 -0
  23. package/assets/skills/java-db-conventions/SKILL.md +44 -0
  24. package/assets/skills/java-ddd-architecture/SKILL.md +533 -0
  25. package/assets/skills/java-dev-practices/SKILL.md +110 -0
  26. package/assets/skills/java-quality-gate/SKILL.md +455 -0
  27. package/assets/skills/java-quality-gate/reference/pmd-rules.md +16 -0
  28. package/assets/skills/java-quality-tool-improve/SKILL.md +142 -0
  29. package/assets/skills/java-security/SKILL.md +59 -0
  30. package/assets/skills/java-technical-debt/SKILL.md +36 -0
  31. package/assets/skills/orchestrator/SKILL.md +65 -0
  32. package/assets/skills/quality-gate/SKILL.md +84 -0
  33. package/assets/skills/security-baseline/SKILL.md +101 -0
  34. package/assets/skills/technical-debt/SKILL.md +78 -0
  35. package/assets/workflows/task.yaml +177 -0
  36. package/package.json +79 -0
  37. package/src/adapters/agent-md.ts +37 -0
  38. package/src/adapters/claude-code/index.ts +35 -0
  39. package/src/adapters/codex/index.ts +161 -0
  40. package/src/adapters/mcp-common/cli.ts +64 -0
  41. package/src/adapters/mcp-common/index.ts +137 -0
  42. package/src/adapters/mcp-common/json-schema.ts +53 -0
  43. package/src/adapters/mcp-common/server.ts +157 -0
  44. package/src/adapters/opencode/agents.ts +80 -0
  45. package/src/adapters/opencode/dashboard.ts +64 -0
  46. package/src/adapters/opencode/index.ts +34 -0
  47. package/src/adapters/opencode/json-schema.ts +60 -0
  48. package/src/adapters/opencode/skills.ts +19 -0
  49. package/src/adapters/opencode/tools.ts +78 -0
  50. package/src/adapters/plugin-common/index.ts +173 -0
  51. package/src/adapters/zcode/index.ts +32 -0
  52. package/src/core/constants.ts +46 -0
  53. package/src/core/dashboard.ts +101 -0
  54. package/src/core/derive.ts +146 -0
  55. package/src/core/exemptions.ts +168 -0
  56. package/src/core/git.ts +373 -0
  57. package/src/core/index.ts +76 -0
  58. package/src/core/namespace.ts +12 -0
  59. package/src/core/paths.ts +23 -0
  60. package/src/core/provider.ts +74 -0
  61. package/src/core/state.ts +357 -0
  62. package/src/core/task-children.ts +59 -0
  63. package/src/core/tasks-md.ts +89 -0
  64. package/src/core/tools/gate.ts +165 -0
  65. package/src/core/tools/lifecycle.ts +645 -0
  66. package/src/core/tools/schemas.ts +363 -0
  67. package/src/core/tools/submit.ts +863 -0
  68. package/src/core/tools/types.ts +129 -0
  69. package/src/core/types.ts +135 -0
  70. package/src/core/views.ts +195 -0
  71. package/src/core/workflow/collector.ts +150 -0
  72. package/src/core/workflow/engine.ts +576 -0
  73. package/src/core/workflow/index.ts +56 -0
  74. package/src/core/workflow/loader.ts +277 -0
  75. package/src/core/workflow/poller.ts +159 -0
  76. package/src/core/workflow/reset.ts +93 -0
  77. package/src/core/workflow/status.ts +1010 -0
  78. package/src/core/workflow/submit.ts +331 -0
  79. package/src/core/workflow/types.ts +100 -0
  80. package/src/core/workflow/writeback.ts +48 -0
  81. package/src/dashboard/page.ts +19 -0
  82. package/src/index.ts +2 -0
  83. package/src/skills/resolve.ts +95 -0
  84. package/src/skills/scan.ts +29 -0
@@ -0,0 +1,1010 @@
1
+ import type { OrchestrateState, TaskGroupState, BlockerItem, ExecutionBoundary, Dimension } from "../types.ts"
2
+ import type { WorkItem, StepConfig, WorkflowCommon } from "./types.ts"
3
+ import { stepAgentIds, EXEMPT_REQUEST_KEY } from "./types.ts"
4
+ import { EXEMPTED_HIT_KEY } from "../exemptions.ts"
5
+ import type { ExemptionRecord } from "../exemptions.ts"
6
+ import type { LoadedWorkflow } from "./loader.ts"
7
+ import type { EngineRecommendation } from "./engine.ts"
8
+ import type { DetectChangesResult } from "../git.ts"
9
+ import { getStepVerdict, isTerminalPhase, isBlockingSeverity, phaseStepMismatch, REVIEW_STEP_TO_LAYER, blockingStepChildren } from "./engine.ts"
10
+ import { agentToReviewDimension, agentToReviewLayer, readIssueSource } from "../constants.ts"
11
+ import { resolveChildIssueFields } from "./reset.ts"
12
+ import { taskListOf, issueChildrenOf } from "../task-children.ts"
13
+ import {
14
+ renderSkillSuggestions, renderEfficiencySteps, renderWorktreeSection,
15
+ renderAgentSummaries, renderTaskItem, formatFilePath, formatSeverity,
16
+ isWorktreeReady, renderWorktreeNotReady, interpolateText, renderStateMismatchDiagnostic,
17
+ } from "../views.ts"
18
+ import { resolveMustDoForCaps, SKIP_REASON_FORMAT } from "../tools/gate.ts"
19
+
20
+ export interface WorkflowStatusViewOptions {
21
+ state: OrchestrateState
22
+ tg: TaskGroupState
23
+ /** 主仓库 openspec 污染诊断结果(56ddfe9 意图),orchestrator 分派视图渲染。 */
24
+ mainPollution?: { repoRoot: string; files: string[] } | null
25
+ /** verify_tool 的 reviewer-tool 工作视图变更检测结果(检查点增量检测,A4);缺省 undefined 走全量。 */
26
+ toolChanges?: DetectChangesResult
27
+ /** 本 change 命中项目级跨 change 豁免清单的存量问题数(工具层降级时统计,供视图汇总提示)。 */
28
+ exemptedHits?: number
29
+ /** 项目级跨 change 豁免清单条目(statusExecute 渲染前异步读取一次传入;renderChildIssue 逐条提示数据源,纯只读)。 */
30
+ exemptionItems?: ExemptionRecord[]
31
+ }
32
+
33
+ /** 状态视图调用者信息:agent 名用于渲染归属,orchestrator 表示编排视角(各 agent 主代理)路由。 */
34
+ export interface StatusViewCaller {
35
+ agent: string
36
+ orchestrator?: boolean
37
+ /** 调用者 agent 身份是否显式声明(MCP 形态下为是否携带 `_agent` 参数;OpenCode 直载形态恒 true)。
38
+ * false 且落入编排视角时,分派视图渲染补传 `_agent` 的身份提示(MCP 子代理首查死锁兜底)。 */
39
+ identityDeclared?: boolean
40
+ }
41
+
42
+ /**
43
+ * 新流动态视图:按调用者角色 + 引擎推荐(recommendForItem)渲染。
44
+ * - 终态(done/cancelled):呈现已完成/已取消状态(见 renderTerminalPhase)
45
+ * - checkpoint:呈现检查点状态 + continue/giveup 决策入口(复用既有文案)
46
+ * - suspended / blocked / terminal:分别呈现暂停原因 / 阻塞原因 / step 已通过
47
+ * - recommend:
48
+ * - 编排视角 → 分派视图(阶段进展 + children 统计 + blocker 汇总 + 下一步分派)
49
+ * - 调用者 ∈ 推荐 agents → ✅ 执行视图(skill 加载清单 + 按 step 渲染 children/blocker/会话摘要 + 操作指引)
50
+ * - 否则 → ⛔ 阶段门禁(复用既有 gate 文案)
51
+ */
52
+ export function renderWorkflowStatusView(
53
+ item: WorkItem,
54
+ workflow: LoadedWorkflow,
55
+ rec: EngineRecommendation,
56
+ caller: StatusViewCaller,
57
+ options: WorkflowStatusViewOptions,
58
+ ): string {
59
+ const ctxAgent = caller.agent
60
+ // 状态异常(phase ↔ step 归属错位)最高优先级:子代理一律拒绝执行;
61
+ // 编排视角继续走分派视图,由 renderOrchestratorDispatch 渲染 ⚠️ 诊断。
62
+ if (phaseStepMismatch(item, workflow)) {
63
+ if (!caller.orchestrator) {
64
+ return renderStateMismatch(item, workflow)
65
+ }
66
+ }
67
+ if (item.metadata["_checkpoint"] === true || rec.status === "checkpoint") {
68
+ return renderCheckpoint(rec, workflow, item)
69
+ }
70
+ if (rec.status === "suspended") {
71
+ return renderSuspended(item)
72
+ }
73
+ if (item.phase === "done" || item.phase === "cancelled") {
74
+ return renderTerminalPhase(item)
75
+ }
76
+ if (rec.status === "blocked") {
77
+ if (caller.orchestrator) {
78
+ return renderOrchestratorDispatch(options.state, item, workflow, rec, options.tg, options.mainPollution, caller.identityDeclared)
79
+ }
80
+ return renderBlocked(rec, item, workflow, ctxAgent)
81
+ }
82
+ if (rec.status === "terminal") {
83
+ if (caller.orchestrator) {
84
+ return renderOrchestratorDispatch(options.state, item, workflow, rec, options.tg, options.mainPollution, caller.identityDeclared)
85
+ }
86
+ return renderTerminal(rec)
87
+ }
88
+ if (caller.orchestrator) {
89
+ return renderOrchestratorDispatch(options.state, item, workflow, rec, options.tg, options.mainPollution, caller.identityDeclared)
90
+ }
91
+ if (rec.agents.includes(ctxAgent)) {
92
+ const step = rec.stepId ? (workflow.stepMap.get(rec.stepId)?.step ?? null) : null
93
+ return renderAgentWorking(item, rec, step, workflow.common, ctxAgent, options.state, options.tg, options.toolChanges, options.exemptedHits, exemptionHintCtx(options.exemptionItems))
94
+ }
95
+ return renderGate(rec, item, ctxAgent)
96
+ }
97
+
98
+ /** 渲染期豁免清单提示上下文(纯只读,不写状态):items 原始清单 + 预构建 (rule+file)→line 映射。
99
+ * 映射在渲染入口构建一次,逐条 issue 渲染时 O(1) 查映射,避免循环内重复扫描清单。 */
100
+ interface ExemptionHintCtx {
101
+ items: ExemptionRecord[]
102
+ ruleFileLines: Map<string, number>
103
+ }
104
+
105
+ /** 从豁免清单构建 (rule+file)→line 映射:rule 或 file 任一缺失的条目不参与(与 exemptionKeyOf 宁漏勿误一致)。 */
106
+ function buildRuleFileLineMap(items: ExemptionRecord[]): Map<string, number> {
107
+ const m = new Map<string, number>()
108
+ for (const it of items) {
109
+ if (typeof it.rule === "string" && it.rule !== "" && typeof it.file === "string" && it.file !== "") {
110
+ m.set([it.rule, it.file].join("\u0000"), it.line ?? 0)
111
+ }
112
+ }
113
+ return m
114
+ }
115
+
116
+ /** 豁免清单为空时不构建提示上下文(渲染链零额外开销)。 */
117
+ function exemptionHintCtx(items: ExemptionRecord[] | undefined): ExemptionHintCtx | undefined {
118
+ if (!items || items.length === 0) return undefined
119
+ return { items, ruleFileLines: buildRuleFileLineMap(items) }
120
+ }
121
+
122
+ function renderCheckpoint(rec: EngineRecommendation, workflow: LoadedWorkflow, item: WorkItem): string {
123
+ const round = rec.checkpoint?.retryCount ?? "?"
124
+ const step = rec.stepId ?? "(无)"
125
+ const lines = [
126
+ `# ⛔ 审查重试达到检查点(第 ${round} 轮)`,
127
+ "",
128
+ "需要用户决策。",
129
+ `唯一动作:调用 \`opx_agent_submit({ step_id: "${step}", checkpoint_decision: "continue" })\`(continue / giveup 二选一)推进。`,
130
+ "",
131
+ ]
132
+ // 质量门类 step(当前 step 能力集解析出质量门必做清单)giveup 时,未覆盖必做项须经
133
+ // checkpoint_skip_reasons 逐项申报结构化降级理由(操作指引层面,不解释门禁内部实现)。
134
+ const entry = rec.stepId ? workflow.stepMap.get(rec.stepId) : undefined
135
+ const stepCaps = Array.from(new Set((entry?.step.agents ?? []).flatMap((a) => a.capability_tags)))
136
+ if (resolveMustDoForCaps(stepCaps).length > 0) {
137
+ lines.push(
138
+ `质量门类 step 的 giveup 决策:未覆盖必做项须经 \`checkpoint_skip_reasons\` 逐项申报结构化降级理由(格式:\`${SKIP_REASON_FORMAT}\`),缺理由 giveup 不会被受理。`,
139
+ "",
140
+ )
141
+ }
142
+ return lines.join("\n")
143
+ }
144
+
145
+ function renderSuspended(item: WorkItem): string {
146
+ const reason = (item.metadata["suspend_reason"] as string) || "suspended"
147
+ return [
148
+ "# ⏸ WorkItem 已暂停",
149
+ "",
150
+ `- **原因**: ${reason}`,
151
+ `- **当前 step**: \`${item.currentStep ?? "(无)"}\``,
152
+ "",
153
+ "暂停期间跳过调度,请结束当前会话,不执行任何操作。",
154
+ "",
155
+ ].join("\n")
156
+ }
157
+
158
+ /** 终态渲染:done 区分已完成(completed_at 已写)与待收尾;cancelled 呈现已取消。 */
159
+ function renderTerminalPhase(item: WorkItem): string {
160
+ if (item.phase === "cancelled") {
161
+ return [
162
+ "# 🚫 任务组已取消",
163
+ "",
164
+ "- **说明**: 该 WorkItem 已取消,编排停止,请结束当前会话。",
165
+ "",
166
+ ].join("\n")
167
+ }
168
+ if (item.metadata["completed_at"] !== undefined) {
169
+ return [
170
+ "# ✅ 任务组已完成",
171
+ "",
172
+ `- **完成时间**: ${item.metadata["completed_at"]}`,
173
+ "",
174
+ "编排已完成并收尾。",
175
+ "",
176
+ ].join("\n")
177
+ }
178
+ return [
179
+ "# 🏁 任务组已完成,待收尾",
180
+ "",
181
+ "全部审核层已通过。调用 `opx_orch_complete_task_group` 合并分支并完成收尾。",
182
+ "",
183
+ ].join("\n")
184
+ }
185
+
186
+ /** blocked 视图:调用者若为当前 step 轮次 agent 用 blocked_agent 文案,否则用通用 blocked 文案。
187
+ * verify_* 层当前 agent 且存在本层待复核(review 态)blocking issue 时,列出待复核清单并给出
188
+ * recheck_adjudications 自助恢复指引(上次提交漏带复核导致门禁阻塞的可补交解除);
189
+ * 存在本层调用者可裁定的豁免申请(待裁定)时同样列出并给出 exempt_adjudications 自助恢复指引
190
+ * (rejected 须配 verdict=failed、dismissed 配 verdict=passed),两条阻塞解除路径均只对本层报源可见。 */
191
+ function renderBlocked(rec: EngineRecommendation, item: WorkItem, workflow: LoadedWorkflow, ctxAgent: string): string {
192
+ const step = rec.stepId ? (workflow.stepMap.get(rec.stepId)?.step ?? null) : null
193
+ const isCurrentAgent = step ? stepAgentIds(step).includes(ctxAgent) : false
194
+ const reason = rec.blockedReason ?? "(未知)"
195
+ if (isCurrentAgent) {
196
+ const layer = step ? REVIEW_STEP_TO_LAYER[step.id] : undefined
197
+ const children = issueChildrenOf(item)
198
+ const pendingRecheck = layer
199
+ ? children.filter((c) => isAgentOwnedIssue(c, ctxAgent) && isBlockingSeverity(c.severity))
200
+ : []
201
+ const pendingExempt = layer
202
+ ? children.filter((c) => isAdjudicableExempt(c, layer, ctxAgent) && isBlockingSeverity(c.severity))
203
+ : []
204
+ const lines = [
205
+ "# ⛔ 当前 step 阻塞中,等待编排处理",
206
+ "",
207
+ `- **原因**: ${reason}`,
208
+ "",
209
+ ]
210
+ if (pendingRecheck.length > 0 && step) {
211
+ lines.push(
212
+ ...renderBlockedAdjudicationBlock(
213
+ "本层待复核 issue",
214
+ pendingRecheck,
215
+ `调用 \`opx_agent_submit({ step_id: "${step.id}", verdict: "passed", recheck_adjudications: [{ issue_id: "<上述 issue id>", verdict: "passed" }] })\` 补带复核结论重提;` +
216
+ `复核通过后自动推进。复核不通过则传 verdict=rejected 并附 reject_reason 驳回(issue 回待修复并累计修复未过次数)。`,
217
+ ),
218
+ )
219
+ }
220
+ if (pendingExempt.length > 0 && step) {
221
+ lines.push(
222
+ ...renderBlockedAdjudicationBlock(
223
+ "本层待裁定豁免申请",
224
+ pendingExempt,
225
+ `调用 \`opx_agent_submit({ step_id: "${step.id}", verdict: "passed", exempt_adjudications: [{ issue_id: "<上述 issue id>", action: "dismissed" }] })\` 补交豁免裁定解除阻塞;` +
226
+ `裁定 dismissed(认可豁免)时 verdict 配 passed,裁定 rejected(驳回=需修复)时 verdict 必须配 failed(审查不能判定通过)。`,
227
+ ),
228
+ )
229
+ }
230
+ if (pendingRecheck.length === 0 && pendingExempt.length === 0) {
231
+ lines.push("你属于当前轮次角色,请勿自行推进,等待编排者解除阻塞后重新查询状态。")
232
+ }
233
+ lines.push("")
234
+ return lines.join("\n")
235
+ }
236
+ return [
237
+ "# ⛔ 当前无法推进(blocked)",
238
+ "",
239
+ `- **原因**: ${reason}`,
240
+ "",
241
+ ].join("\n")
242
+ }
243
+
244
+ /** blocked 视图裁定区块渲染:清单(Issue #id | severity)+ 自助恢复指引,children 为空时返回空数组。 */
245
+ function renderBlockedAdjudicationBlock(title: string, children: WorkItem[], guidance: string): string[] {
246
+ const lines = [`- **${title}(${children.length} 个)**:`]
247
+ for (const c of children) {
248
+ const id = c.externalId ?? c.id.replace(/^issue:/, "")
249
+ lines.push(` - Issue #${id} | ${formatSeverity(c.severity ?? "Info")}`)
250
+ }
251
+ lines.push("", `**自助恢复**:${guidance}`, "")
252
+ return lines
253
+ }
254
+
255
+ function renderTerminal(rec: EngineRecommendation): string {
256
+ return [
257
+ "# 🏁 当前 step 已通过",
258
+ "",
259
+ `- **说明**: ${rec.message ?? "step 已通过,沿 transitions.on_pass 推进(由 submit 工具执行状态迁移)。"}`,
260
+ "",
261
+ ].join("\n")
262
+ }
263
+
264
+ function renderOrchestratorDispatch(
265
+ state: OrchestrateState,
266
+ item: WorkItem,
267
+ workflow: LoadedWorkflow,
268
+ rec: EngineRecommendation,
269
+ tg: TaskGroupState,
270
+ mainPollution?: { repoRoot: string; files: string[] } | null,
271
+ identityDeclared?: boolean,
272
+ ): string {
273
+ const lines = [
274
+ "# 编排进度",
275
+ "",
276
+ `**变更**: ${state.changeId}`,
277
+ `**当前阶段**: ${item.phase}(step \`${rec.stepId ?? "(无)"}\`)`,
278
+ "",
279
+ ]
280
+ // 状态异常(phase ↔ step 归属错位)诊断:错位态禁止分派,提示 recovery 恢复。
281
+ if (phaseStepMismatch(item, workflow)) {
282
+ const entry = item.currentStep ? workflow.stepMap.get(item.currentStep) : undefined
283
+ lines.push("## ⚠️ 状态异常(phase ↔ step 归属不一致)", "")
284
+ lines.push(...renderStateMismatchDiagnostic(item.phase, item.currentStep, entry?.phase.name ?? null))
285
+ lines.push("")
286
+ lines.push("WorkItem 阶段与当前 step 归属错位,禁止分派任何子代理执行;请调用 `opx_orch_init(recovery=...)` 重置异常状态后重新调度。")
287
+ lines.push("")
288
+ }
289
+ // 推进被拦时附阻塞原因(优先取 submit 工具写入 metadata 的实际原因,次取引擎 blocked 推荐原因),
290
+ // 编排者据此决策(修复 / recovery / 收尾)
291
+ const metaReason = item.metadata["_advance_block_reason"]
292
+ const advanceBlockReason =
293
+ typeof metaReason === "string" && metaReason !== "" ? metaReason : rec.blockedReason
294
+ if (advanceBlockReason) {
295
+ lines.push(`**推进阻塞**: ${advanceBlockReason}`)
296
+ lines.push("")
297
+ }
298
+ lines.push(...renderProgressSection(item, workflow))
299
+ lines.push(...renderOrchestratorBlockers(item))
300
+ if (mainPollution && mainPollution.files.length > 0) {
301
+ lines.push("## ⚠️ 主仓库 openspec 污染", "")
302
+ lines.push(`检测到主仓库 \`${mainPollution.repoRoot}\` 下 openspec 文档存在未提交变更(修改/新增):`)
303
+ lines.push("")
304
+ for (const f of mainPollution.files) lines.push(`- \`${f}\``)
305
+ lines.push("")
306
+ lines.push("子代理文件操作应限定在 worktree 内,主仓库路径出现变更通常由误改主分支路径造成,请人工核对处理。")
307
+ lines.push("")
308
+ }
309
+ lines.push(...renderWorktreeSection(state, tg))
310
+ lines.push("## 下一步", "")
311
+ const agents = rec.agents
312
+ if (agents.length > 0) {
313
+ if (!isWorktreeReady(tg)) {
314
+ // 分派前置门禁:worktree 未就绪时不给出分派指令,指引编排者先补齐 worktree 再回来查状态分派
315
+ lines.push("分派前置条件未满足:当前存在待分派子代理,但 worktree 未就绪,暂不给出分派指令。")
316
+ lines.push("请先调用 `opx_orch_set_worktree` 确保 worktree 就绪,再回来查 `opx_status` 获取分派指令。")
317
+ } else {
318
+ const agentList = agents.map((a) => `\`${a}\``).join("、")
319
+ lines.push(`分派子代理:${agentList}。`)
320
+ if (agents.length > 1) {
321
+ lines.push("(多子代理相互独立,可在单条消息中并排分派,无需串行等待)")
322
+ }
323
+ }
324
+ } else {
325
+ // 防御出口:当前 step 存在 failed 残留 tag 但无待分派项 → 状态不一致。
326
+ // 引擎已按「全部非 pending 时回退 failed 维度」自愈重派,此处兜底提示编排者走 recovery
327
+ // 而非盲目回退,避免 failed tag 残留(如历史 state 报源缺失导致归因无法反推到对应维)静默死锁。
328
+ const failed = failedAgentsOnStep(item, workflow, rec.stepId)
329
+ if (failed.length > 0) {
330
+ lines.push("⚠️ 状态不一致:存在 failed 裁决残留但无待分派项,下一步无法正常推进。")
331
+ lines.push(` - 失败维度:${failed.map((a) => `\`${a}\``).join("、")}`)
332
+ lines.push(" 建议:调用 `opx_orch_init(recovery=...)` 重置对应层审查进度后重新验证,勿盲目回退 dev。")
333
+ } else if (rec.status === "blocked") {
334
+ // blocked 且无可推导 reviewer(全部为 todo/in_progress 态或报源缺失):输出阻塞 children 诊断清单,
335
+ // 替代信息量为零的占位文案,编排者据此判断是派 dev 修复还是走 recovery。
336
+ // 无阻塞 issue children(如跨 phase 门禁拦截的 task child 场景)回退占位文案,避免空区块。
337
+ const diag = renderBlockedChildrenDiagnostic(item, workflow, rec.stepId)
338
+ if (diag.length > 0) lines.push(...diag)
339
+ else lines.push("(无待分派项,请检查状态)")
340
+ } else {
341
+ lines.push("(无待分派项,请检查状态)")
342
+ }
343
+ }
344
+ lines.push("")
345
+ // MCP 形态兜底(身份未显式声明时):分派视图追加补传 `_agent` 的身份提示。提示正文直接复用
346
+ // workflow 配置 common 块中的 `_agent` 指引原文(单一文案来源,避免双源);配置缺失该条时静默降级。
347
+ // 覆盖场景:MCP 子代理未按分派 prompt 携带 `_agent` 首查状态,落入编排视角视图时能获得自救指引。
348
+ if (identityDeclared === false) {
349
+ const agentGuidance = workflow.common?.instructions?.find((i) => i.includes("_agent"))
350
+ if (agentGuidance) {
351
+ lines.push(`> **身份提示**:${interpolateText(agentGuidance, {})}`, "")
352
+ }
353
+ }
354
+ return lines.join("\n")
355
+ }
356
+
357
+ /** 当前 step 中 verdict=failed 的 agent 列表(无待分派项时的状态不一致诊断)。 */
358
+ function failedAgentsOnStep(item: WorkItem, workflow: LoadedWorkflow, stepId: string | null): string[] {
359
+ if (!stepId) return []
360
+ const step = workflow.stepMap.get(stepId)?.step
361
+ if (!step) return []
362
+ return step.agents.filter((a) => getStepVerdict(item, stepId, a.id) === "failed").map((a) => a.id)
363
+ }
364
+
365
+ /** blocked 且无可推导 reviewer 时的阻塞 children 诊断清单(归属/报源/阶段 + 处理建议)。 */
366
+ function renderBlockedChildrenDiagnostic(item: WorkItem, workflow: LoadedWorkflow, stepId: string | null): string[] {
367
+ if (!stepId) return []
368
+ const step = workflow.stepMap.get(stepId)?.step
369
+ if (!step) return []
370
+ const children = blockingStepChildren(item, step)
371
+ if (children.length === 0) return []
372
+ const lines = [
373
+ "⚠️ 当前 step 已全 passed 但存在阻塞 children,且无可补交裁定的 reviewer:",
374
+ "",
375
+ ]
376
+ for (const c of children) {
377
+ const id = c.externalId ?? c.id.replace(/^issue:/, "")
378
+ const f = resolveChildIssueFields(c)
379
+ const source = readIssueSource(c) ?? "(报源缺失)"
380
+ const state =
381
+ c.metadata[EXEMPT_REQUEST_KEY] !== undefined ? "豁免申请中" : c.phase === "review" ? "待复核" : `待处理(${c.phase})`
382
+ lines.push(`- Issue #${id} | ${formatSeverity(c.severity ?? "Info")} | 归属:${f.dimension} | 报源:${source} | ${state}`)
383
+ }
384
+ lines.push("")
385
+ lines.push("处理建议:待复核/豁免申请中条目需报源 reviewer 补交裁定(recheck_adjudications / exempt_adjudications);")
386
+ lines.push("待处理条目需 developer 修复;报源缺失时核对 state 文件后按需 `opx_orch_init(recovery=...)` 恢复。")
387
+ lines.push("")
388
+ return lines
389
+ }
390
+
391
+ /** orchestrator 阶段进展/审核进度:各 step:agent:verdict 汇总 + children 统计。 */
392
+ function renderProgressSection(item: WorkItem, workflow: LoadedWorkflow): string[] {
393
+ const lines = ["## 阶段进展 / 审核进度", ""]
394
+ lines.push("| step | agent | verdict |")
395
+ lines.push("|------|-------|---------|")
396
+ for (const phase of workflow.phases) {
397
+ if (isTerminalPhase(phase.name)) continue
398
+ for (const step of phase.steps) {
399
+ for (const agent of stepAgentIds(step)) {
400
+ lines.push(`| \`${step.id}\` | \`${agent}\` | ${getStepVerdict(item, step.id, agent)} |`)
401
+ }
402
+ }
403
+ }
404
+ lines.push("")
405
+ const counts = childStatusCounts(item)
406
+ lines.push("**children 统计**:", "")
407
+ lines.push(`- 待处理 ${counts.todo} · 待复核 ${counts.review} · 已验证 ${counts.done} · 已豁免 ${counts.cancelled}`)
408
+ if (counts.exempt > 0) lines.push(`- 豁免申请中 ${counts.exempt}`)
409
+ lines.push("")
410
+ return lines
411
+ }
412
+
413
+ /** children 状态统计:todo=待处理、review=待复核、done=已验证、cancelled=已豁免、exempt_request=豁免申请中。
414
+ * 待裁定豁免项(review 态 + exempt_request)只计入豁免申请中,不重复计入待复核/待处理。 */
415
+ function childStatusCounts(item: WorkItem): { todo: number; review: number; done: number; cancelled: number; exempt: number } {
416
+ const counts = { todo: 0, review: 0, done: 0, cancelled: 0, exempt: 0 }
417
+ // 仅统计 issue child(task child 不计入——子任务进度由 task 语义承载)
418
+ for (const c of issueChildrenOf(item)) {
419
+ const isExemptRequest = c.metadata[EXEMPT_REQUEST_KEY] !== undefined
420
+ if (isExemptRequest) counts.exempt++
421
+ switch (c.phase) {
422
+ case "todo": if (!isExemptRequest) counts.todo++; break
423
+ case "review": if (!isExemptRequest) counts.review++; break
424
+ case "done": counts.done++; break
425
+ case "cancelled": counts.cancelled++; break
426
+ }
427
+ }
428
+ return counts
429
+ }
430
+
431
+ /** orchestrator blocker 汇总(含状态)。 */
432
+ function renderOrchestratorBlockers(item: WorkItem): string[] {
433
+ const blockers = readBlockers(item)
434
+ if (blockers.length === 0) return []
435
+ const lines = ["## Blocker", ""]
436
+ for (const b of blockers) {
437
+ const status = b.status === "resolved" ? "✓ 已解决" : "⏳ 待处理"
438
+ lines.push(`- Blocker #${b.id} | ${status} | ${b.category}`)
439
+ lines.push(` - 来源:${b.sourceRole}${b.taskId ? `;Task #${b.taskId}` : ""}`)
440
+ lines.push(` - 描述:${b.description}`)
441
+ if (b.userResponse) lines.push(` - 用户答复:${b.userResponse}`)
442
+ }
443
+ lines.push("")
444
+ return lines
445
+ }
446
+
447
+ function renderGate(rec: EngineRecommendation, item: WorkItem, ctxAgent: string): string {
448
+ const expected = rec.agents.join(", ") || "(无)"
449
+ return [
450
+ "# ⛔ 阶段门禁",
451
+ "",
452
+ `当前阶段为 **${item.phase}**(step \`${rec.stepId ?? "(无)"}\`),未轮到你(**${ctxAgent}**)执行。`,
453
+ `当前预期角色为:\`${expected}\``,
454
+ "",
455
+ "请立即结束当前会话,不要执行任何操作。",
456
+ "",
457
+ ].join("\n")
458
+ }
459
+
460
+ /** 状态异常拒绝视图:phase ↔ step 归属错位时拒绝 agent 执行(复用 renderWorktreeNotReady 拒绝模式),指引 recovery 恢复。 */
461
+ function renderStateMismatch(item: WorkItem, workflow: LoadedWorkflow): string {
462
+ const entry = item.currentStep ? workflow.stepMap.get(item.currentStep) : undefined
463
+ return [
464
+ "# ⛔ 状态异常,当前拒绝执行",
465
+ "",
466
+ "WorkItem 阶段与当前 step 归属不一致(phase ↔ step 错位),状态异常:",
467
+ ...renderStateMismatchDiagnostic(item.phase, item.currentStep, entry?.phase.name ?? null),
468
+ "",
469
+ "请立即结束当前会话,不执行任何操作、不调用任何 `opx_*` 变更工具。",
470
+ "请报告编排者调用 `opx_orch_init(recovery=...)` 重置异常状态后重新调度。",
471
+ "",
472
+ ].join("\n")
473
+ }
474
+
475
+ function renderAgentWorking(
476
+ item: WorkItem,
477
+ rec: EngineRecommendation,
478
+ step: StepConfig | null,
479
+ common: WorkflowCommon | undefined,
480
+ ctxAgent: string,
481
+ state: OrchestrateState,
482
+ tg: TaskGroupState,
483
+ toolChanges?: DetectChangesResult,
484
+ exemptedHits?: number,
485
+ exemptionCtx?: ExemptionHintCtx,
486
+ ): string {
487
+ // worktree 就绪阻断(置于顶部):未就绪时拒绝执行,渲染 ⛔ 视图,不输出 ✅ 执行视图内容
488
+ if (!isWorktreeReady(tg)) {
489
+ return [
490
+ "# ⛔ worktree 未就绪,当前拒绝执行",
491
+ "",
492
+ ...renderWorktreeNotReady(),
493
+ "",
494
+ "请立即结束当前会话,不执行任何操作、不调用 `opx_agent_submit`。",
495
+ "报告编排者先调用 `opx_orch_set_worktree`,就绪后再回来执行。",
496
+ "",
497
+ ].join("\n")
498
+ }
499
+
500
+ // tool review 检查点增量三分支(A4):仅 verify_tool 的 reviewer-tool 且提供了变更检测结果时生效;
501
+ // toolChanges 缺省(未预计算)时维持既有全量渲染,不误伤其他 agent/step 的通用渲染。
502
+ if (step?.id === "verify_tool" && agentToReviewLayer(ctxAgent) === "tool" && toolChanges) {
503
+ const { active, pending } = toolPendingChildren(item, ctxAgent)
504
+ if (!toolChanges.hasNonDocChange && active.length === 0 && pending.length === 0) {
505
+ return renderToolDirectSubmit(exemptedHits)
506
+ }
507
+ if (!toolChanges.hasNonDocChange) {
508
+ return renderToolAdjudicateOnly(item, rec, ctxAgent, exemptedHits, exemptionCtx)
509
+ }
510
+ }
511
+ // skill 加载清单按当前调用者 agent 声明的 capability_tags 过滤(step 内各 agent 独立声明,互不相同)
512
+ const caps = step?.agents.find((a) => a.id === ctxAgent)?.capability_tags ?? []
513
+ // step 语义占位符插值上下文:动态值来源见各 key(缺值保留占位符原文,不阻断渲染)
514
+ const stepCtx: Record<string, string> = {}
515
+ if (tg.worktreePath) stepCtx["worktree_path"] = tg.worktreePath
516
+ stepCtx["change_id"] = state.changeId
517
+ if (rec.stepId) stepCtx["step_id"] = rec.stepId
518
+ stepCtx["phase"] = item.phase
519
+ stepCtx["agent"] = ctxAgent
520
+ stepCtx["base_branch"] = state.baseBranch
521
+ const boundary = readExecutionBoundary(item)
522
+ if (boundary) {
523
+ stepCtx["allowed_directories"] = boundary.allowed_directories.join(", ")
524
+ stepCtx["allowed_packages"] = boundary.allowed_packages.join(", ")
525
+ // notes 留空(架构师按规范无补充)时渲染为「无额外说明」,避免视图中出现字面 {{notes}}
526
+ stepCtx["notes"] = boundary.notes && boundary.notes.trim() !== "" ? boundary.notes : "无额外说明"
527
+ }
528
+ const lines = [
529
+ "# ✅ 当前轮到你执行",
530
+ "",
531
+ "完成本职工作后**必须**调用 `opx_agent_submit()` 提交。",
532
+ "按结果提交 `verdict=passed` 或 `verdict=failed`。",
533
+ "",
534
+ "---",
535
+ "",
536
+ "# 当前任务上下文",
537
+ "",
538
+ `**变更**: ${state.changeId}`,
539
+ `**阶段**: ${item.phase} | **step**: \`${rec.stepId ?? "(无)"}\``,
540
+ "",
541
+ ]
542
+ if ((exemptedHits ?? 0) > 0) {
543
+ lines.push(
544
+ `> **存量豁免提示**:本 change 存在 ${exemptedHits} 个命中项目级跨 change 豁免清单的存量问题(已按 Info 处理,不阻塞、无需重复豁免)。`,
545
+ "",
546
+ )
547
+ }
548
+ lines.push(...renderStepSemantics(step, common, stepCtx))
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。
555
+ if (step?.id === "verify_tool" && agentToReviewLayer(ctxAgent) === "tool" && toolChanges?.hasNonDocChange) {
556
+ lines.push(...renderToolChangesEvidence(item, tg, toolChanges))
557
+ }
558
+ lines.push(...renderAgentSummaries(readAgentSummaries(item), ctxAgent))
559
+ lines.push(...renderStepContext(item, step, ctxAgent, state, exemptionCtx))
560
+ lines.push(...renderSkillSuggestions(ctxAgent, caps))
561
+ lines.push("## 操作指引", "")
562
+ let n = 0
563
+ lines.push(`${n++}. 按上方「Skill 加载清单」逐项加载列出的 skill(不可跳步)`)
564
+ const eff = renderEfficiencySteps(n)
565
+ lines.push(...eff.lines)
566
+ n = eff.nextNum
567
+ if (tg.worktreePath && tg.baseRef) {
568
+ lines.push(`${n++}. 用上方「变更范围」命令获取本 change 全部已提交变更文件清单,作为本次工作范围`)
569
+ }
570
+ lines.push(`${n++}. 执行当前 step(\`${rec.stepId}\`)职责范围内的全部工作——遵循所有已加载 skill 的全部规范与约束`)
571
+ lines.push(`${n++}. 逐项检视所有已加载 skill 的 MUST 规范,确认全部满足(不满足则补做,不得跳过)`)
572
+ // 通用指引(common.instructions)→ step 专属指引(step.instructions)合并渲染为编号步骤
573
+ for (const instruction of common?.instructions ?? []) {
574
+ lines.push(`${n++}. ${interpolateText(instruction, stepCtx)}`)
575
+ }
576
+ for (const instruction of step?.instructions ?? []) {
577
+ lines.push(`${n++}. ${interpolateText(instruction, stepCtx)}`)
578
+ }
579
+ lines.push(`${n++}. 全部完成 → commit → \`opx_agent_submit({ step_id: "${rec.stepId}", verdict: "passed" })\``)
580
+ lines.push(`${n++}. 遇阻塞无法继续 → \`opx_agent_submit({ step_id: "${rec.stepId}", verdict: "failed" })\``)
581
+ lines.push("")
582
+ return lines.join("\n")
583
+ }
584
+
585
+ /** step.id → 上下文渲染类型映射:verify_* 与 review_* 语义一一对应,渲染路由由 step.id 推导。 */
586
+ type StepContextKind = "analyze" | "implement" | "review_tool" | "review_task" | "review_quality" | "review_cleanup"
587
+
588
+ const STEP_ID_TO_CONTEXT_KIND: Record<string, StepContextKind> = {
589
+ analyze: "analyze",
590
+ implement: "implement",
591
+ verify_tool: "review_tool",
592
+ verify_task: "review_task",
593
+ verify_quality: "review_quality",
594
+ verify_cleanup: "review_cleanup",
595
+ }
596
+
597
+ /** step.id 推导上下文渲染类型,未命中返回 undefined(优雅降级不渲染该区块)。 */
598
+ function stepContextKind(stepId: string): StepContextKind | undefined {
599
+ return STEP_ID_TO_CONTEXT_KIND[stepId]
600
+ }
601
+
602
+ /** 按 step 渲染动态上下文:由 step.id 推导渲染类型(未命中优雅降级返回空数组)。 */
603
+ function renderStepContext(item: WorkItem, step: StepConfig | null, ctxAgent: string, state: OrchestrateState, exemptionCtx?: ExemptionHintCtx): string[] {
604
+ if (!step) return []
605
+ const lines: string[] = []
606
+ switch (stepContextKind(step.id)) {
607
+ case "analyze":
608
+ lines.push(...renderAnalyzeMode(state))
609
+ lines.push(...renderAnalyzeBlockers(item))
610
+ break
611
+ case "implement":
612
+ lines.push(...renderDeveloperChildren(item, ctxAgent, exemptionCtx))
613
+ break
614
+ case "review_tool":
615
+ lines.push(...renderToolChildren(item, ctxAgent, exemptionCtx))
616
+ break
617
+ case "review_task":
618
+ lines.push(...renderTaskChildren(item, ctxAgent, exemptionCtx))
619
+ break
620
+ case "review_quality":
621
+ lines.push(...renderQualityChildren(item, ctxAgent, exemptionCtx))
622
+ break
623
+ case "review_cleanup":
624
+ lines.push(...renderCleanupChildren(item, ctxAgent, exemptionCtx))
625
+ break
626
+ }
627
+ return lines
628
+ }
629
+
630
+ /** step 语义字段渲染:constraints(common.constraints 与 step.constraints 合并,经占位符插值注入动态值,缺值保留原文)。 */
631
+ function renderStepSemantics(step: StepConfig | null, common: WorkflowCommon | undefined, ctx: Record<string, string>): string[] {
632
+ if (!step) return []
633
+ const lines: string[] = []
634
+ const constraints = [...(common?.constraints ?? []), ...(step.constraints ?? [])]
635
+ if (constraints.length > 0) {
636
+ lines.push("## 约束", "")
637
+ for (const c of constraints) lines.push(`- ${interpolateText(c, ctx)}`)
638
+ lines.push("")
639
+ }
640
+ return lines
641
+ }
642
+
643
+ // ─── children / blockers 渲染 ───
644
+
645
+ function readBlockers(item: WorkItem): BlockerItem[] {
646
+ const raw = item.metadata["blockers"]
647
+ return Array.isArray(raw) ? (raw as BlockerItem[]) : []
648
+ }
649
+
650
+ function readTasks(item: WorkItem): ReturnType<typeof taskListOf> {
651
+ return taskListOf(item)
652
+ }
653
+
654
+ function readAgentSummaries(item: WorkItem): Record<string, string> | undefined {
655
+ const raw = item.metadata["agent_summaries"]
656
+ return typeof raw === "object" && raw !== null ? (raw as Record<string, string>) : undefined
657
+ }
658
+
659
+ function readExecutionBoundary(item: WorkItem): ExecutionBoundary | null {
660
+ const raw = item.metadata["execution_boundary"]
661
+ return typeof raw === "object" && raw !== null ? (raw as ExecutionBoundary) : null
662
+ }
663
+
664
+ /** analyze step:确认模式渲染(有人值守/无人值守),架构师据此判断直接向用户确认还是自行裁决。 */
665
+ function renderAnalyzeMode(state: OrchestrateState): string[] {
666
+ const unattended = state.unattended === true
667
+ return [
668
+ "## 确认模式",
669
+ "",
670
+ unattended
671
+ ? "- 当前:**无人值守**——不向用户提问,自行选择最优解继续"
672
+ : "- 当前:**有人值守**——可直接向用户确认(需用户拍板时)",
673
+ "",
674
+ ]
675
+ }
676
+
677
+ /** analyze step:blocker 清单(awaiting_user/resolved)+ 处理指引。 */
678
+ function renderAnalyzeBlockers(item: WorkItem): string[] {
679
+ const blockers = readBlockers(item)
680
+ if (blockers.length === 0) return []
681
+ const lines = ["## Blocker", ""]
682
+ for (const b of blockers) {
683
+ const status = b.status === "resolved" ? "✓ 已解决" : "⏳ 待用户答复"
684
+ lines.push(`- Blocker #${b.id} | ${status} | ${b.category}`)
685
+ lines.push(` - 来源:${b.sourceRole}${b.taskId ? `;Task #${b.taskId}` : ""}`)
686
+ lines.push(` - 描述:${b.description}`)
687
+ if (b.evidence) lines.push(` - 证据:${b.evidence}`)
688
+ if (b.attemptedActions) lines.push(` - 已尝试:${b.attemptedActions}`)
689
+ if (b.options.length > 0) lines.push(` - 可选方案:${b.options.join(";")}`)
690
+ if (b.userResponse) lines.push(` - 用户答复:${b.userResponse}`)
691
+ if (b.architectConclusion) lines.push(` - 架构结论:${b.architectConclusion}`)
692
+ }
693
+ lines.push("")
694
+ if (blockers.some((b) => b.status !== "resolved")) {
695
+ lines.push("> 存在未解决 blocker:analyze step 无法以 passed 提交。请先经 blocker_updates 逐条置 resolved(user_response 填确认/裁决结论)后再提交。", "")
696
+ }
697
+ return lines
698
+ }
699
+
700
+ /** implement step:developer 待修复 children(仅 todo 态:review 态由对应 reviewer 复核、豁免申请走待裁定区块)。 */
701
+ function renderDeveloperChildren(item: WorkItem, ctxAgent: string, exemptionCtx?: ExemptionHintCtx): string[] {
702
+ const lines: string[] = []
703
+ // 仅 issue child 进入修复清单(task child 不得混入 issue 渲染)
704
+ const toFix = issueChildrenOf(item).filter((c) => isAgentOwnedIssue(c, ctxAgent))
705
+ if (toFix.length === 0) return lines
706
+ renderTodoFixBlocks(lines, toFix, exemptionCtx)
707
+ const highRefix = toFix.filter((c) => typeof c.metadata["refix_count"] === "number" && (c.metadata["refix_count"] as number) >= 2)
708
+ if (highRefix.length > 0) {
709
+ lines.push("## ⚠️ 修复多次未过的 issue", "")
710
+ for (const c of highRefix) {
711
+ const id = c.externalId ?? c.id.replace(/^issue:/, "")
712
+ lines.push(`- Issue #${id}(已 ${c.metadata["refix_count"]} 次修复未过)`)
713
+ }
714
+ lines.push("")
715
+ }
716
+ return lines
717
+ }
718
+
719
+ /** 调用者可裁定的豁免申请(谁提谁裁定):tool 层由报源反推层命中 tool。 */
720
+ function isToolAdjudicable(child: WorkItem): boolean {
721
+ return child.metadata[EXEMPT_REQUEST_KEY] !== undefined && agentToReviewLayer(readIssueSource(child)) === "tool"
722
+ }
723
+
724
+ /** 调用者可裁定的豁免申请(谁提谁裁定):task 层由报源反推层命中 task。 */
725
+ function isTaskAdjudicable(child: WorkItem): boolean {
726
+ return child.metadata[EXEMPT_REQUEST_KEY] !== undefined && agentToReviewLayer(readIssueSource(child)) === "task"
727
+ }
728
+
729
+ /** 调用者可裁定的豁免申请(谁提谁裁定):quality 层报源维度等于调用者维度。 */
730
+ function isQualityAdjudicable(child: WorkItem, dimension: Dimension | undefined): boolean {
731
+ if (child.metadata[EXEMPT_REQUEST_KEY] === undefined || !dimension) return false
732
+ return agentToReviewDimension(readIssueSource(child) ?? "") === dimension
733
+ }
734
+
735
+ /** blocked 视图待裁定豁免判定:按当前 step 层路由到「谁提谁裁定」判定(tool/task 按报源层、quality 按报源维度)。
736
+ * 与 isAgentOwnedIssue 互补:带 exempt_request 标记的 review 态项被主区块排除(isAgentOwnedIssue 返回 false),
737
+ * 但仍是本层调用者可裁定的豁免申请(blocked 补交路径,blockedSupplementAgents 据此派发报源 reviewer)。 */
738
+ function isAdjudicableExempt(child: WorkItem, layer: string, ctxAgent: string): boolean {
739
+ if (layer === "quality") return isQualityAdjudicable(child, agentToDimension(ctxAgent))
740
+ if (layer === "tool") return isToolAdjudicable(child)
741
+ if (layer === "task") return isTaskAdjudicable(child)
742
+ return false
743
+ }
744
+
745
+ /** issue 主区块归属判定(单一事实源):某 agent 该看到哪些 issue。
746
+ * - developer(implement step):仅 todo 态(排除已交复核的 review 态与豁免申请 exempt_request)
747
+ * - reviewer-tool / reviewer-task(verify_tool / verify_task step):仅本层报源(sourcePhase)的 review 态
748
+ * - reviewer-{dim}(verify_quality step):quality 报源且报源维度与调用者维度一致,仅 review 态
749
+ * 层归属一律经 resolveChildIssueFields(sourcePhase) 推导(内部含 source_phase 历史兜底),不裸用报源反推。
750
+ * exempt_request issue 归「待裁定」区块管,主区块收紧为 review-only 后天然不重复展示。 */
751
+ function isAgentOwnedIssue(child: WorkItem, ctxAgent: string): boolean {
752
+ const f = resolveChildIssueFields(child)
753
+ const layer = agentToReviewLayer(ctxAgent)
754
+ if (!layer) {
755
+ return child.phase === "todo" && child.metadata[EXEMPT_REQUEST_KEY] === undefined
756
+ }
757
+ // 待裁定豁免项(exempt_request 标记)归「待裁定」区块管:即使已进入 review 态也排除出主区块,
758
+ // 避免与待裁定区块重复展示;recheck 复核带标记项亦被工具层守卫拒绝(须走 exempt_adjudications 裁定)。
759
+ if (child.phase !== "review" || child.metadata[EXEMPT_REQUEST_KEY] !== undefined) return false
760
+ if (f.sourcePhase !== layer) return false
761
+ if (layer === "quality") {
762
+ const dim = agentToReviewDimension(ctxAgent)
763
+ if (!dim) return false
764
+ return agentToReviewDimension(readIssueSource(child) ?? "") === dim
765
+ }
766
+ return true
767
+ }
768
+
769
+ /** verify_tool 的 reviewer-tool 视角 children 收敛(单一事实源):active=本层待复核主区块、
770
+ * pending=本层待裁定豁免,二者口径均不分 severity(与 renderBlockedAdjudicationBlock 一致)。
771
+ * 三分支判定(A4)与 renderToolChildren 共用本函数,避免口径分叉。 */
772
+ function toolPendingChildren(item: WorkItem, ctxAgent: string): { active: WorkItem[]; pending: WorkItem[] } {
773
+ const issues = issueChildrenOf(item)
774
+ return {
775
+ active: issues.filter((c) => isAgentOwnedIssue(c, ctxAgent)),
776
+ pending: issues.filter((c) => isToolAdjudicable(c)),
777
+ }
778
+ }
779
+
780
+ /** verify_tool step:reviewer-tool 视角本层报源 review 态 issue + 调用者可裁定的豁免申请(待裁定)。 */
781
+ function renderToolChildren(item: WorkItem, ctxAgent: string, exemptionCtx?: ExemptionHintCtx): string[] {
782
+ const { active, pending } = toolPendingChildren(item, ctxAgent)
783
+ const lines: string[] = []
784
+ lines.push(...renderChildrenSection("Issue (待复核)", active, exemptionCtx))
785
+ lines.push(...renderChildrenSection("Issue (待裁定是否可豁免)", pending, exemptionCtx))
786
+ return lines
787
+ }
788
+
789
+ /** verify_tool 分支③(全量路径)证据区块:检查点增量口径的本次变更证据(文件清单 + 区间 diff 命令)。
790
+ * 有检查点时标注「本次为自上次工具检查(checkpoint..HEAD)增量区间」,与 Worktree 区块「变更范围」
791
+ * (baseRef..HEAD 整个 change 累计口径)区分;无检查点(首次进入)时标注基线兜底口径,与变更范围一致;
792
+ * 无检查点亦无基线基准时不渲染 diff 命令(无法界定区间)。仅渲染事实证据,不做变更性质判定——
793
+ * 裁量语义由 workflow 配置操作指引单源承载。 */
794
+ function renderToolChangesEvidence(item: WorkItem, tg: TaskGroupState, toolChanges: DetectChangesResult): string[] {
795
+ const checkpoint =
796
+ typeof item.metadata["_tool_review_checkpoint"] === "string" ? item.metadata["_tool_review_checkpoint"] : undefined
797
+ const baseRef = tg.baseRef ?? undefined
798
+ const rangeRef = checkpoint ?? baseRef
799
+ const lines = ["## 本次变更证据(自上次工具检查)", ""]
800
+ if (checkpoint) {
801
+ lines.push(
802
+ `- **口径**: 本次为「自上次工具检查(${checkpoint}..HEAD)」的增量区间(含已提交与未提交的非 openspec 变更);` +
803
+ `与上方「变更范围」(${baseRef ?? "(无)"}..HEAD 整个 change 累计口径)不同`,
804
+ "",
805
+ )
806
+ } else if (baseRef) {
807
+ lines.push(
808
+ `- **口径**: 本次区间以基线(${baseRef}..HEAD)兜底,与上方「变更范围」一致(首次进入,无上次工具检查记录;` +
809
+ "含已提交与未提交的非 openspec 变更)",
810
+ "",
811
+ )
812
+ } else {
813
+ lines.push("- **口径**: 无检查点且无基线基准,无法界定已提交变更区间;本次变更仅按检出文件与未提交变更提示", "")
814
+ }
815
+ if (toolChanges.files.length > 0) {
816
+ lines.push("- **变更文件清单**:", "")
817
+ for (const f of toolChanges.files) lines.push(` - \`${f}\``)
818
+ lines.push("")
819
+ } else {
820
+ lines.push("- **变更文件清单**: (本区间未检出非 openspec 变更文件)", "")
821
+ }
822
+ if (rangeRef) {
823
+ lines.push(`- **查看本次区间 diff**: \`git -C ${tg.worktreePath} diff ${rangeRef}..HEAD\``)
824
+ lines.push(" - 该命令仅覆盖已提交区间;若存在未提交变更,另用 `git status` / `git diff` 查看工作区改动")
825
+ lines.push("")
826
+ }
827
+ lines.push("")
828
+ return lines
829
+ }
830
+
831
+ /** verify_tool 三分支①(直提):自上次工具检查后无代码/配置变更且本层无待复核/待裁定项。
832
+ * 替换式最小视图:不渲染 worktree/变更范围区块(避免与既有 baseRef..HEAD 累计口径提示冲突),
833
+ * 文案只给结论,不展示检查点 hash。 */
834
+ function renderToolDirectSubmit(exemptedHits?: number): string {
835
+ const lines = [
836
+ "# ✅ 当前轮到你执行",
837
+ "",
838
+ "自上次工具检查后,worktree 无代码/配置变更(仅 openspec 文档变更或无变更),本层亦无待复核/待裁定 issue。",
839
+ "",
840
+ ]
841
+ if ((exemptedHits ?? 0) > 0) {
842
+ lines.push(
843
+ `> **存量豁免提示**:本 change 存在 ${exemptedHits} 个命中项目级跨 change 豁免清单的存量问题(已按 Info 处理,不阻塞、无需重复豁免)。`,
844
+ "",
845
+ )
846
+ }
847
+ lines.push(
848
+ "无需运行全量工具检查。",
849
+ "",
850
+ "## 操作指引",
851
+ "",
852
+ "1. 提交前须申报本轮必做清单处理结果:以一条 `step` 名首段为 `no_change` 的 validation_steps 条目声明本轮整体豁免必做清单(completed=false,并附结构化 skip_reason 注明判定依据),或按已加载质量门 skill 必做清单逐项申报(completed=true 附执行结果;无法执行项置 completed=false + 结构化 skip_reason)",
853
+ `2. 结构化 skip_reason 格式:\`${SKIP_REASON_FORMAT}\``,
854
+ `3. 调用 \`opx_agent_submit({ step_id: "verify_tool", verdict: "passed", validation_steps: <必做清单申报结果> })\` 提交通过后结束会话。`,
855
+ "",
856
+ )
857
+ return lines.join("\n")
858
+ }
859
+
860
+ /** verify_tool 三分支②(仅处理待复核项):无代码/配置变更但有本层待复核/待裁定项 →
861
+ * 仅复核/裁定待处理项,不跑全量工具扫描,处理完成后提交。 */
862
+ function renderToolAdjudicateOnly(item: WorkItem, rec: EngineRecommendation, ctxAgent: string, exemptedHits?: number, exemptionCtx?: ExemptionHintCtx): string {
863
+ const lines = [
864
+ "# ✅ 当前轮到你执行",
865
+ "",
866
+ "自上次工具检查后,worktree 无代码/配置变更,无需运行全量工具检查。",
867
+ "",
868
+ ]
869
+ if ((exemptedHits ?? 0) > 0) {
870
+ lines.push(
871
+ `> **存量豁免提示**:本 change 存在 ${exemptedHits} 个命中项目级跨 change 豁免清单的存量问题(已按 Info 处理,不阻塞、无需重复豁免)。`,
872
+ "",
873
+ )
874
+ }
875
+ lines.push("仅处理以下本层待复核 / 待裁定项,处理完成后提交:", "")
876
+ lines.push(...renderToolChildren(item, ctxAgent, exemptionCtx))
877
+ lines.push("## 操作指引", "")
878
+ lines.push(
879
+ "1. 逐项复核/裁定上方各项:待复核 issue 经 `recheck_adjudications` 复核(通过置 done、不通过驳回并附驳回原因);待裁定豁免经 `exempt_adjudications` 裁定(dismissed/rejected)",
880
+ )
881
+ lines.push(
882
+ "2. 提交前须申报本轮必做清单处理结果:以一条 `step` 名首段为 `no_change` 的 validation_steps 条目声明本轮整体豁免必做清单(completed=false + 结构化 skip_reason 注明判定依据),或按已加载质量门 skill 必做清单逐项申报",
883
+ )
884
+ lines.push(`3. 结构化 skip_reason 格式:\`${SKIP_REASON_FORMAT}\``)
885
+ lines.push(`4. 全部处理完成 → commit → \`opx_agent_submit({ step_id: "${rec.stepId}", verdict: "passed", validation_steps: <必做清单申报结果> })\``)
886
+ lines.push("")
887
+ return lines.join("\n")
888
+ }
889
+
890
+ /** verify_task step:task children 待验证列表 + task 层 issue 主区块 + 调用者可裁定的豁免申请(待裁定)。 */
891
+ function renderTaskChildren(item: WorkItem, ctxAgent: string, exemptionCtx?: ExemptionHintCtx): string[] {
892
+ const lines: string[] = []
893
+ const pendingTasks = readTasks(item).filter((t) => t.status === "submitted")
894
+ if (pendingTasks.length > 0) {
895
+ lines.push("## Task (待验证)", "")
896
+ for (const t of pendingTasks) lines.push(renderTaskItem(t))
897
+ lines.push("")
898
+ }
899
+ const issues = issueChildrenOf(item)
900
+ const own = issues.filter((c) => isAgentOwnedIssue(c, ctxAgent))
901
+ lines.push(...renderChildrenSection("Issue (待复核)", own, exemptionCtx))
902
+ const pending = issues.filter((c) => isTaskAdjudicable(c))
903
+ lines.push(...renderChildrenSection("Issue (待裁定是否可豁免)", pending, exemptionCtx))
904
+ return lines
905
+ }
906
+
907
+ /** verify_quality step:各维度 reviewer 渲染本维度报源 review 态 issue + 本维度豁免申请(待裁定)。 */
908
+ function renderQualityChildren(item: WorkItem, ctxAgent: string, exemptionCtx?: ExemptionHintCtx): string[] {
909
+ const dimension = agentToDimension(ctxAgent)
910
+ if (!dimension) return []
911
+ // task child 无 dimension 归因(resolveChildIssueFields 缺省 style),必须按 type 排除
912
+ const issues = issueChildrenOf(item)
913
+ const own = issues.filter((c) => isAgentOwnedIssue(c, ctxAgent))
914
+ const lines: string[] = []
915
+ lines.push(...renderChildrenSection("Issue (待复核)", own, exemptionCtx))
916
+ const pending = issues.filter((c) => isQualityAdjudicable(c, dimension))
917
+ lines.push(...renderChildrenSection("Issue (待裁定是否可豁免)", pending, exemptionCtx))
918
+ return lines
919
+ }
920
+
921
+ /** verify_cleanup step:developer 收尾视角。
922
+ * - 待修复:todo 态 issue(isAgentOwnedIssue 对 developer 仅命中 todo 态且非豁免申请)
923
+ * - 自报待复核:报源为当前 agent(openspec-developer)的 review 态 issue——收尾阶段自报自裁,
924
+ * 经 recheck_adjudications 自行复核收敛(isAgentOwnedIssue 只命中 todo 态,此处显式补充)
925
+ * - 自报待裁定豁免:报源为当前 agent 且带 exempt_request 标记——经 exempt_adjudications 自行裁定
926
+ */
927
+ function renderCleanupChildren(item: WorkItem, ctxAgent: string, exemptionCtx?: ExemptionHintCtx): string[] {
928
+ const issues = issueChildrenOf(item)
929
+ const lines: string[] = []
930
+ const toFix = issues.filter((c) => isAgentOwnedIssue(c, ctxAgent))
931
+ renderTodoFixBlocks(lines, toFix, exemptionCtx)
932
+ const ownReview = issues.filter(
933
+ (c) => readIssueSource(c) === ctxAgent && c.phase === "review" && c.metadata[EXEMPT_REQUEST_KEY] === undefined,
934
+ )
935
+ lines.push(...renderChildrenSection("Issue (自报待复核 · 本 step 自行复核裁定)", ownReview, exemptionCtx))
936
+ const ownPending = issues.filter(
937
+ (c) => readIssueSource(c) === ctxAgent && c.metadata[EXEMPT_REQUEST_KEY] !== undefined,
938
+ )
939
+ lines.push(...renderChildrenSection("Issue (自报豁免申请 · 待自行裁定)", ownPending, exemptionCtx))
940
+ return lines
941
+ }
942
+
943
+ /** 调用者 agent → 审查维度(DIMENSION_AGENT_MAP 反查)。 */
944
+ function agentToDimension(agent: string): Dimension | undefined {
945
+ return agentToReviewDimension(agent)
946
+ }
947
+
948
+ /** 渲染 children 区块:标题 + 逐条列表,空列表返回空数组。 */
949
+ function renderChildrenSection(title: string, children: WorkItem[], exemptionCtx?: ExemptionHintCtx): string[] {
950
+ if (children.length === 0) return []
951
+ const lines = [`## ${title}`, ""]
952
+ for (const c of children) lines.push(renderChildIssue(c, exemptionCtx))
953
+ lines.push("")
954
+ return lines
955
+ }
956
+
957
+ /** 待修复 issue 区块渲染(implement 与 verify_cleanup 两处待修复清单共用):blocking/info 拆分后渲染同标题区块。 */
958
+ function renderTodoFixBlocks(lines: string[], toFix: WorkItem[], exemptionCtx?: ExemptionHintCtx): void {
959
+ const blocking = toFix.filter((c) => isBlockingSeverity(c.severity))
960
+ const info = toFix.filter((c) => !isBlockingSeverity(c.severity))
961
+ if (blocking.length > 0) {
962
+ lines.push(...renderChildrenSection("Issue (待修复 · Low 及以上,必办)", blocking, exemptionCtx))
963
+ }
964
+ if (info.length > 0) {
965
+ lines.push(...renderChildrenSection("Issue (待修复 · Info,建议修复,不阻塞提交)", info, exemptionCtx))
966
+ }
967
+ }
968
+
969
+ /** 逐条 issue 的豁免相关只读提示(a:tool 层无规则名提示;b:疑似行号漂移提示)。纯渲染,不写任何状态。
970
+ * b 仅提示「(rule+file) 命中清单但 line 不同」的条目;已命中降级(exempted_hit 标记,
971
+ * 即 (rule+file+line) 完全命中)的 issue 不重复提示漂移。 */
972
+ function appendExemptionHints(
973
+ lines: string[],
974
+ child: WorkItem,
975
+ f: ReturnType<typeof resolveChildIssueFields>,
976
+ exemptionCtx: ExemptionHintCtx | undefined,
977
+ ): void {
978
+ const rule = typeof child.metadata["rule"] === "string" ? child.metadata["rule"] : ""
979
+ if (f.sourcePhase === "tool" && rule === "") {
980
+ lines.push(" - 无规则名:豁免结论不会写入跨 change 清单,下个 change 将重新报此问题")
981
+ }
982
+ if (exemptionCtx && rule !== "" && f.file !== "" && child.metadata[EXEMPTED_HIT_KEY] === undefined) {
983
+ const hitLine = exemptionCtx.ruleFileLines.get([rule, f.file].join("\u0000"))
984
+ if (hitLine !== undefined && hitLine !== f.line) {
985
+ lines.push(` - 疑似行号漂移:豁免清单记录 line=${hitLine},当前报 line=${f.line},请核对是否同一问题`)
986
+ }
987
+ }
988
+ }
989
+
990
+ /** 单个 issue child 渲染(参考 views.renderIssueItem 风格,字段来自 WorkItem.metadata)。 */
991
+ function renderChildIssue(child: WorkItem, exemptionCtx?: ExemptionHintCtx): string {
992
+ const f = resolveChildIssueFields(child)
993
+ const id = child.externalId ?? child.id.replace(/^issue:/, "")
994
+ const rule = typeof child.metadata["rule"] === "string" ? ` | ${child.metadata["rule"]}` : ""
995
+ const lines = [
996
+ `- Issue #${id}${rule ? ` | ${rule}` : ""}`,
997
+ ]
998
+ if (f.file) lines.push(` - 文件:${formatFilePath(f.file, f.line)}`)
999
+ lines.push(` - 描述:${child.description}`)
1000
+ appendExemptionHints(lines, child, f, exemptionCtx)
1001
+ if (child.metadata[EXEMPTED_HIT_KEY] !== undefined) {
1002
+ const rule = typeof child.metadata[EXEMPTED_HIT_KEY] === "string" ? child.metadata[EXEMPTED_HIT_KEY] : ""
1003
+ lines.push(` - ⚠️ 命中项目级豁免清单${rule ? `(rule=${rule})` : ""}的存量问题,已按 Info 处理,无需重复豁免`)
1004
+ }
1005
+ if (typeof child.metadata["suggestion"] === "string") lines.push(` - 建议:${child.metadata["suggestion"]}`)
1006
+ if (typeof child.metadata["reject_reason"] === "string") lines.push(` - 驳回原因:${child.metadata["reject_reason"]}`)
1007
+ if (typeof child.metadata["refix_count"] === "number") lines.push(` - 修复未过次数:${child.metadata["refix_count"]}`)
1008
+ if (child.metadata[EXEMPT_REQUEST_KEY] !== undefined) lines.push(" - 豁免申请中:等待裁定")
1009
+ return lines.join("\n")
1010
+ }