@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.
- package/README.md +91 -0
- package/assets/agents/openspec-architect.md +61 -0
- package/assets/agents/openspec-developer.md +21 -0
- package/assets/agents/openspec-main.md +35 -0
- package/assets/agents/openspec-reviewer-architecture.md +60 -0
- package/assets/agents/openspec-reviewer-maintainability.md +59 -0
- package/assets/agents/openspec-reviewer-performance.md +49 -0
- package/assets/agents/openspec-reviewer-security.md +51 -0
- package/assets/agents/openspec-reviewer-style.md +50 -0
- package/assets/agents/openspec-reviewer-task.md +40 -0
- package/assets/agents/openspec-reviewer-tool.md +30 -0
- package/assets/dashboard/index.html +295 -0
- package/assets/skills/api-conventions/SKILL.md +60 -0
- package/assets/skills/api-test/SKILL.md +175 -0
- package/assets/skills/architecture-evolution/SKILL.md +60 -0
- package/assets/skills/db-conventions/SKILL.md +62 -0
- package/assets/skills/ddd-architecture/SKILL.md +261 -0
- package/assets/skills/environment-cleanup/SKILL.md +44 -0
- package/assets/skills/java-api-conventions/SKILL.md +30 -0
- package/assets/skills/java-code-maintainability/SKILL.md +62 -0
- package/assets/skills/java-code-performance/SKILL.md +49 -0
- package/assets/skills/java-code-style/SKILL.md +54 -0
- package/assets/skills/java-db-conventions/SKILL.md +44 -0
- package/assets/skills/java-ddd-architecture/SKILL.md +533 -0
- package/assets/skills/java-dev-practices/SKILL.md +110 -0
- package/assets/skills/java-quality-gate/SKILL.md +455 -0
- package/assets/skills/java-quality-gate/reference/pmd-rules.md +16 -0
- package/assets/skills/java-quality-tool-improve/SKILL.md +142 -0
- package/assets/skills/java-security/SKILL.md +59 -0
- package/assets/skills/java-technical-debt/SKILL.md +36 -0
- package/assets/skills/orchestrator/SKILL.md +65 -0
- package/assets/skills/quality-gate/SKILL.md +84 -0
- package/assets/skills/security-baseline/SKILL.md +101 -0
- package/assets/skills/technical-debt/SKILL.md +78 -0
- package/assets/workflows/task.yaml +177 -0
- package/package.json +79 -0
- package/src/adapters/agent-md.ts +37 -0
- package/src/adapters/claude-code/index.ts +35 -0
- package/src/adapters/codex/index.ts +161 -0
- package/src/adapters/mcp-common/cli.ts +64 -0
- package/src/adapters/mcp-common/index.ts +137 -0
- package/src/adapters/mcp-common/json-schema.ts +53 -0
- package/src/adapters/mcp-common/server.ts +157 -0
- package/src/adapters/opencode/agents.ts +80 -0
- package/src/adapters/opencode/dashboard.ts +64 -0
- package/src/adapters/opencode/index.ts +34 -0
- package/src/adapters/opencode/json-schema.ts +60 -0
- package/src/adapters/opencode/skills.ts +19 -0
- package/src/adapters/opencode/tools.ts +78 -0
- package/src/adapters/plugin-common/index.ts +173 -0
- package/src/adapters/zcode/index.ts +32 -0
- package/src/core/constants.ts +46 -0
- package/src/core/dashboard.ts +101 -0
- package/src/core/derive.ts +146 -0
- package/src/core/exemptions.ts +168 -0
- package/src/core/git.ts +373 -0
- package/src/core/index.ts +76 -0
- package/src/core/namespace.ts +12 -0
- package/src/core/paths.ts +23 -0
- package/src/core/provider.ts +74 -0
- package/src/core/state.ts +357 -0
- package/src/core/task-children.ts +59 -0
- package/src/core/tasks-md.ts +89 -0
- package/src/core/tools/gate.ts +165 -0
- package/src/core/tools/lifecycle.ts +645 -0
- package/src/core/tools/schemas.ts +363 -0
- package/src/core/tools/submit.ts +863 -0
- package/src/core/tools/types.ts +129 -0
- package/src/core/types.ts +135 -0
- package/src/core/views.ts +195 -0
- package/src/core/workflow/collector.ts +150 -0
- package/src/core/workflow/engine.ts +576 -0
- package/src/core/workflow/index.ts +56 -0
- package/src/core/workflow/loader.ts +277 -0
- package/src/core/workflow/poller.ts +159 -0
- package/src/core/workflow/reset.ts +93 -0
- package/src/core/workflow/status.ts +1010 -0
- package/src/core/workflow/submit.ts +331 -0
- package/src/core/workflow/types.ts +100 -0
- package/src/core/workflow/writeback.ts +48 -0
- package/src/dashboard/page.ts +19 -0
- package/src/index.ts +2 -0
- package/src/skills/resolve.ts +95 -0
- package/src/skills/scan.ts +29 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
import type { OrchestrateState } from "../types.ts"
|
|
2
|
+
import type { ToolContext, AgentSubmitParams } from "./types.ts"
|
|
3
|
+
import type { WorkItem, Severity, StepConfig } from "../workflow/types.ts"
|
|
4
|
+
import { loadWorkflowFile, TASK_WORKFLOW_PATH, type LoadedWorkflow } from "../workflow/loader.ts"
|
|
5
|
+
import {
|
|
6
|
+
submitForStep, adjudicateExempt, adjudicateRecheck, assertSubmitRouting, chainPassAdvance,
|
|
7
|
+
} from "../workflow/submit.ts"
|
|
8
|
+
import {
|
|
9
|
+
createInitialWorkItem, checkpointTriggered,
|
|
10
|
+
applyCheckpointContinue, applyCheckpointGiveup,
|
|
11
|
+
getStepVerdict, clearStepTags, isBlockingSeverity, isInfoSeverity, isTerminalPhase, applyTransition,
|
|
12
|
+
} from "../workflow/engine.ts"
|
|
13
|
+
import { resetReviewTagsOnFix, dedupeNewChildren, resolveChildIssueFields } from "../workflow/reset.ts"
|
|
14
|
+
import { agentToReviewDimension, agentToReviewLayer, readIssueSource } from "../constants.ts"
|
|
15
|
+
import { REVIEW_DIMENSIONS } from "../types.ts"
|
|
16
|
+
import type { Dimension } from "../types.ts"
|
|
17
|
+
import { taskChildrenOf, taskChildById, normalizeTaskChildIds, taskListOf, issueChildrenOf } from "../task-children.ts"
|
|
18
|
+
import { markTaskGroupCheckboxesComplete, reconcileMainPollution, getCurrentHead } from "../git.ts"
|
|
19
|
+
import { assertIssueFilesWithin } from "../paths.ts"
|
|
20
|
+
import {
|
|
21
|
+
readStateByWorktree, writeState, getLockPath, acquireLock, releaseLock,
|
|
22
|
+
} from "../state.ts"
|
|
23
|
+
import { readExemptions, applyExemptionDowngrade, writeDismissedExemption } from "../exemptions.ts"
|
|
24
|
+
import {
|
|
25
|
+
assertStructuredSkipReasons, uncoveredMustDo, isValidSkipData, SKIP_REASON_FORMAT,
|
|
26
|
+
} from "./gate.ts"
|
|
27
|
+
|
|
28
|
+
/** 读取 task workflow 配置(assets/workflows/task.yaml),进程内缓存。 */
|
|
29
|
+
const loadTaskWorkflow = () => loadWorkflowFile(TASK_WORKFLOW_PATH)
|
|
30
|
+
|
|
31
|
+
/** issue id 归一化:去 # 前缀(review 工具按 tg.issue 序号引用时可能带 #)。 */
|
|
32
|
+
function normalizeIssueId(id: string): string {
|
|
33
|
+
return String(id).trim().replace(/^#/, "")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 判定本次 review 提交是否「仅携带裁定类参数」(recheck_adjudications / exempt_adjudications,无其他实质参数)。
|
|
38
|
+
* 供重复提交守卫的补交豁免:reviewer 上次 passed 提交漏带复核/豁免裁定导致本层 review 态 issue
|
|
39
|
+
* 阻塞门禁时(见 engine.blockedSupplementAgents 推导),允许仅补带 recheck_adjudications /
|
|
40
|
+
* exempt_adjudications 重提解除阻塞;空提交或携带其他实质参数(含 fixed_issue_ids /
|
|
41
|
+
* exempt_issue_ids / new_children)的重复提交仍被守卫拒绝。
|
|
42
|
+
*/
|
|
43
|
+
function isSupplementOnly(params: AgentSubmitParams): boolean {
|
|
44
|
+
const hasAdjudication =
|
|
45
|
+
(params.recheck_adjudications?.length ?? 0) > 0 ||
|
|
46
|
+
(params.exempt_adjudications?.length ?? 0) > 0
|
|
47
|
+
if (!hasAdjudication) return false
|
|
48
|
+
return (
|
|
49
|
+
(params.fixed_issue_ids?.length ?? 0) === 0 &&
|
|
50
|
+
(params.exempt_issue_ids?.length ?? 0) === 0 &&
|
|
51
|
+
(params.new_children?.length ?? 0) === 0 &&
|
|
52
|
+
(params.verified_tasks?.length ?? 0) === 0 &&
|
|
53
|
+
(params.failed_tasks?.length ?? 0) === 0 &&
|
|
54
|
+
!params.boundary_expansion &&
|
|
55
|
+
(params.validation_steps?.length ?? 0) === 0 &&
|
|
56
|
+
params.test_results === undefined
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 统一裁定一致性守卫:任一裁定参数含「驳回(rejected)」时,提交级 verdict 必须为 failed。
|
|
62
|
+
* - exempt_adjudications 含 action==="rejected"(issue 回 todo 继续修复)→ verdict 必须为 failed
|
|
63
|
+
* - recheck_adjudications 含 verdict==="rejected"(issue 回 todo + refix_count 递增 + 写 reject_reason)→ verdict 必须为 failed
|
|
64
|
+
* 驳回意味着该 issue 需修复,审查不能判定通过:rejected+passed 会让 review 态 issue 以非终态滞留,
|
|
65
|
+
* 门禁要求本层 blocking issue 终态(stepCanPass=false 且无补交推导),形成静默死锁。
|
|
66
|
+
* dismissed/passed 是合法组合(裁定完成且维度通过),不受此守卫限制。
|
|
67
|
+
* 零副作用:守卫在一切裁定写入(adjudicateExempt/adjudicateRecheck)与 submitForStep 应用 verdict 之前调用,
|
|
68
|
+
* 拒绝时保证零状态变更(含混合裁定清单)。
|
|
69
|
+
*/
|
|
70
|
+
function assertRejectedAdjudicationNotPassed(params: AgentSubmitParams): void {
|
|
71
|
+
if (params.verdict !== "passed") return
|
|
72
|
+
const rejectedExempt = params.exempt_adjudications?.find((adj) => adj.action === "rejected")
|
|
73
|
+
if (rejectedExempt) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`豁免裁定失败:issue "${rejectedExempt.issue_id}" 被驳回(rejected),该 issue 需修复,verdict 必须为 failed,不能以 passed 提交。`
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
const rejectedRecheck = params.recheck_adjudications?.find((adj) => adj.verdict === "rejected")
|
|
79
|
+
if (rejectedRecheck) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`复核失败:issue "${rejectedRecheck.issue_id}" 被驳回(rejected),该 issue 需修复,verdict 必须为 failed,不能以 passed 提交。`
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 以 tg.issue 原始 id 解析 child(externalId 优先,兜底 issue: 前缀),供豁免裁定定位。
|
|
87
|
+
* issue child 优先匹配(与 childById 语义对称):task child 短数字 id 可能与 issue externalId 撞车,
|
|
88
|
+
* fixed/exempt 归因解析须命中 issue child,未命中再兜底 task child。 */
|
|
89
|
+
function resolveChildByIssueId(item: WorkItem, issueId: string): WorkItem | null {
|
|
90
|
+
const match = (c: WorkItem): boolean =>
|
|
91
|
+
c.externalId === issueId || c.id === issueId || c.id === `issue:${issueId}`
|
|
92
|
+
return issueChildrenOf(item).find(match) ?? taskChildrenOf(item).find(match) ?? null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 定位当前 taskGroup 对应的活跃 task WorkItem。
|
|
97
|
+
* 单轨下 workItems 为唯一事实源(init 构造 / 旧格式读兼容自动升级),缺失即未初始化。
|
|
98
|
+
*/
|
|
99
|
+
function resolveTaskWorkItem(state: OrchestrateState): WorkItem {
|
|
100
|
+
const taskId = `task:${state.taskGroupId}`
|
|
101
|
+
const item = state.workItems.find((w) => w.id === taskId)
|
|
102
|
+
if (!item) {
|
|
103
|
+
throw new Error(`工作项 "${taskId}" 缺失,请重新调用 opx_orch_init 初始化。`)
|
|
104
|
+
}
|
|
105
|
+
return item
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 正常提交路径的固定返回体:提交状态与推进详情对子代理无意义,仅告知提交成功并结束会话。 */
|
|
109
|
+
const SUBMIT_OK_MESSAGE = "提交成功,请直接结束当前会话"
|
|
110
|
+
|
|
111
|
+
// ─── M1a 参数面处理:task children / metadata.blockers / 分层重置 ───
|
|
112
|
+
|
|
113
|
+
interface ItemBlocker {
|
|
114
|
+
id: string
|
|
115
|
+
sourceRole: string
|
|
116
|
+
taskId: string | null
|
|
117
|
+
category: string
|
|
118
|
+
description: string
|
|
119
|
+
evidence: string
|
|
120
|
+
attemptedActions: string
|
|
121
|
+
options: string[]
|
|
122
|
+
status: "awaiting_user" | "resolved"
|
|
123
|
+
userResponse: string | null
|
|
124
|
+
architectConclusion: string | null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 读取 metadata.blockers(analyze/implement 上报的 blocker 清单)。 */
|
|
128
|
+
function itemBlockers(item: WorkItem): ItemBlocker[] {
|
|
129
|
+
return Array.isArray(item.metadata["blockers"]) ? (item.metadata["blockers"] as ItemBlocker[]) : []
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 新增 blocker 到 metadata.blockers,id 按 b{n} 递增,status=awaiting_user。 */
|
|
133
|
+
function addItemBlockers(item: WorkItem, raw: NonNullable<AgentSubmitParams["blockers"]>): void {
|
|
134
|
+
const list = itemBlockers(item)
|
|
135
|
+
let nextId = list.reduce((max, b) => Math.max(max, Number(b.id.replace(/^b/, "")) || 0), 0) + 1
|
|
136
|
+
for (const b of raw) {
|
|
137
|
+
list.push({
|
|
138
|
+
id: `b${nextId++}`,
|
|
139
|
+
sourceRole: b.source_role,
|
|
140
|
+
taskId: b.task_id ?? null,
|
|
141
|
+
category: b.category,
|
|
142
|
+
description: b.description,
|
|
143
|
+
evidence: b.evidence,
|
|
144
|
+
attemptedActions: b.attempted_actions,
|
|
145
|
+
options: b.options ?? [],
|
|
146
|
+
status: "awaiting_user",
|
|
147
|
+
userResponse: null,
|
|
148
|
+
architectConclusion: null,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
item.metadata["blockers"] = list
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 汇总 fixed/exempt issue 的归因分层(报源层 + quality 维度),供 resetReviewTagsOnFix 使用。
|
|
155
|
+
* fixed(实际修改代码)触发 quality 维 tag 清除(含显式声明维度的跨维归因标签);exempt(豁免=接受现状、
|
|
156
|
+
* 不修改代码)不触发 quality 维 tag 清除——已 passed 的维度无需因豁免重审。 */
|
|
157
|
+
function collectFixedExemptLayers(
|
|
158
|
+
item: WorkItem,
|
|
159
|
+
fixedIds: string[],
|
|
160
|
+
exemptIds: string[],
|
|
161
|
+
): { fixedSourcePhases: string[]; exemptSourcePhases: string[]; touchedQualityDims: string[] } {
|
|
162
|
+
const fixedSourcePhases = new Set<string>()
|
|
163
|
+
const exemptSourcePhases = new Set<string>()
|
|
164
|
+
const touchedQualityDims = new Set<string>()
|
|
165
|
+
/** 归因维 tag 纳入(仅 fixed):报源层为 quality(本维 issue,含缺省维度)或显式声明了维度(tool/task 跨维归因标签)
|
|
166
|
+
* 都计入——跨维 issue 修复后目标维 verify_quality tag 须清,否则回退重审期该维恒 failed 永不分派(死锁)。
|
|
167
|
+
* exempt 不调用本函数:豁免不改代码,清除维度 tag 会触发无实际待办的重复调度。 */
|
|
168
|
+
const touchQualityDim = (child: WorkItem): void => {
|
|
169
|
+
const f = resolveChildIssueFields(child)
|
|
170
|
+
const declaredDim = (REVIEW_DIMENSIONS as readonly string[]).includes(child.metadata["dimension"] as string)
|
|
171
|
+
? (child.metadata["dimension"] as Dimension)
|
|
172
|
+
: undefined
|
|
173
|
+
if (f.sourcePhase === "quality" || declaredDim) touchedQualityDims.add(declaredDim ?? f.dimension)
|
|
174
|
+
// 报源维度兜底:quality 报源未显式声明 dimension(缺省回落 style)时,补报源 reviewer 自身维度 tag——
|
|
175
|
+
// 否则仅清 style 而报源维残留,回退重审期该维恒 failed 多一轮不必要重审。
|
|
176
|
+
if (!declaredDim && f.sourcePhase === "quality") {
|
|
177
|
+
const source = readIssueSource(child) ?? ""
|
|
178
|
+
const sourceDim = agentToReviewDimension(source)
|
|
179
|
+
if (sourceDim) touchedQualityDims.add(sourceDim)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const id of fixedIds) {
|
|
183
|
+
const child = resolveChildByIssueId(item, id)
|
|
184
|
+
if (!child) continue
|
|
185
|
+
// 报源层 tag 必清(谁提谁裁定:tool 报的跨维 issue 修复后 verify_tool 必重跑)
|
|
186
|
+
fixedSourcePhases.add(resolveChildIssueFields(child).sourcePhase)
|
|
187
|
+
touchQualityDim(child)
|
|
188
|
+
}
|
|
189
|
+
for (const id of exemptIds) {
|
|
190
|
+
const child = resolveChildByIssueId(item, id)
|
|
191
|
+
if (!child) continue
|
|
192
|
+
// 报源层 tag 语义保留(tool/task 报源 exempt 仍清本层 tag——「谁提谁裁定」派发报源 reviewer 裁定豁免申请的通道),
|
|
193
|
+
// 但不触发 touchQualityDim:exempt 不改代码,quality 维度 tag 保留 passed。
|
|
194
|
+
exemptSourcePhases.add(resolveChildIssueFields(child).sourcePhase)
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
fixedSourcePhases: [...fixedSourcePhases],
|
|
198
|
+
exemptSourcePhases: [...exemptSourcePhases],
|
|
199
|
+
touchedQualityDims: [...touchedQualityDims],
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 由 new_children 参数构造 issue child,透传全部分子段到 metadata(source/dimension/file/line/suggestion/rule/root_cause_guess)。
|
|
204
|
+
* 报源层由 metadata.source 反推(见 agentToReviewLayer),不再写入 source_phase。
|
|
205
|
+
* dimension 提交语义:quality reviewer(agentToReviewDimension 命中)由报源维度推断写入;
|
|
206
|
+
* tool/task reviewer 透传显式声明的归因维度(校验循环已保证必填且合法)。 */
|
|
207
|
+
function buildIssueChild(nc: NonNullable<AgentSubmitParams["new_children"]>[number], sourceAgent: string): WorkItem {
|
|
208
|
+
const child = createInitialWorkItem({
|
|
209
|
+
id: nc.id,
|
|
210
|
+
source: sourceAgent,
|
|
211
|
+
externalId: nc.id,
|
|
212
|
+
type: "issue",
|
|
213
|
+
title: nc.title,
|
|
214
|
+
description: nc.description,
|
|
215
|
+
severity: nc.severity as Severity | undefined,
|
|
216
|
+
})
|
|
217
|
+
child.metadata["source"] = sourceAgent
|
|
218
|
+
const sourceDim = agentToReviewDimension(sourceAgent)
|
|
219
|
+
child.metadata["dimension"] = sourceDim ?? nc.dimension
|
|
220
|
+
if (nc.file) child.metadata["file"] = nc.file
|
|
221
|
+
if (nc.line !== undefined) child.metadata["line"] = nc.line
|
|
222
|
+
if (nc.suggestion) child.metadata["suggestion"] = nc.suggestion
|
|
223
|
+
if (nc.rule) child.metadata["rule"] = nc.rule
|
|
224
|
+
if (nc.root_cause_guess) child.metadata["root_cause_guess"] = nc.root_cause_guess
|
|
225
|
+
return child
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 清空 review 三个验证 step 的裁决 tags(审查未过/任务驳回回退时复核标记重置,下次进入 review 各层重新分派)。 */
|
|
229
|
+
function clearReviewVerificationTags(item: WorkItem): void {
|
|
230
|
+
clearStepTags(item, "verify_tool")
|
|
231
|
+
clearStepTags(item, "verify_task")
|
|
232
|
+
clearStepTags(item, "verify_quality")
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** blocker 提交后的 reset 助手:task children 全 todo + 清 review 层验证标记(对齐旧 resetForBlocker)。 */
|
|
236
|
+
function resetTasksForBlocker(item: WorkItem): void {
|
|
237
|
+
for (const child of taskChildrenOf(item)) {
|
|
238
|
+
child.phase = "todo"
|
|
239
|
+
delete child.metadata["reject_reason"]
|
|
240
|
+
}
|
|
241
|
+
clearReviewVerificationTags(item)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** analyze step 参数处理:execution_boundary(passed 必传)、blockers/blocker_updates、无未解决 blocker 门禁。 */
|
|
245
|
+
function handleAnalyzeParams(item: WorkItem, params: AgentSubmitParams): void {
|
|
246
|
+
if (params.verdict === "passed" && !params.execution_boundary) {
|
|
247
|
+
throw new Error("analyze step 提交 passed 时必须提供 execution_boundary。")
|
|
248
|
+
}
|
|
249
|
+
if (params.execution_boundary) {
|
|
250
|
+
item.metadata["execution_boundary"] = params.execution_boundary
|
|
251
|
+
}
|
|
252
|
+
if (params.blockers?.length) addItemBlockers(item, params.blockers)
|
|
253
|
+
if (params.blocker_updates?.length) {
|
|
254
|
+
for (const u of params.blocker_updates) {
|
|
255
|
+
const blocker = itemBlockers(item).find((b) => b.id === u.blocker_id)
|
|
256
|
+
if (!blocker) throw new Error(`blocker "${u.blocker_id}" 不存在于 metadata.blockers 中。`)
|
|
257
|
+
if (blocker.status !== "awaiting_user") throw new Error(`blocker "${u.blocker_id}" 状态不是 awaiting_user,无法更新。`)
|
|
258
|
+
blocker.userResponse = u.user_response
|
|
259
|
+
blocker.status = "resolved"
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (params.verdict === "passed" && itemBlockers(item).some((b) => b.status !== "resolved")) {
|
|
263
|
+
throw new Error("存在未解决的 blocker,无法以 passed 提交 analyze step。请先通过 blocker_updates 处理全部 blocker。")
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** implement step 参数处理:blocker(on_fail)、completed_task_ids + 覆盖门禁、self_check_results。 */
|
|
268
|
+
function handleImplementParams(item: WorkItem, params: AgentSubmitParams): void {
|
|
269
|
+
if (params.blocker) {
|
|
270
|
+
if (params.verdict !== "failed") {
|
|
271
|
+
throw new Error("blocker 参数仅支持 verdict=failed 提交(on_fail 回退 analyze)。")
|
|
272
|
+
}
|
|
273
|
+
addItemBlockers(item, [params.blocker])
|
|
274
|
+
resetTasksForBlocker(item)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const tasks = taskListOf(item)
|
|
279
|
+
const completed = normalizeTaskChildIds(params.completed_task_ids ?? [], item)
|
|
280
|
+
const validIds = new Set(tasks.map((t) => t.id))
|
|
281
|
+
for (const id of completed) {
|
|
282
|
+
if (!validIds.has(id)) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`completed_task_ids 中包含无效 task id: "${id}"。\n有效 task ID: ${Array.from(validIds).sort((a, b) => Number(a) - Number(b)).join(", ")}`
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// 仅 todo 态 task child 置 review(submitted 语义)+ 清 reject_reason;
|
|
289
|
+
// done 态(已 verified)跳过不降级——质量层回退后已验证任务不得重新验证。
|
|
290
|
+
for (const id of completed) {
|
|
291
|
+
const child = taskChildById(item, id)
|
|
292
|
+
if (child && child.phase === "todo") {
|
|
293
|
+
child.phase = "review"
|
|
294
|
+
delete child.metadata["reject_reason"]
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// 按条目状态重投影(而非 id 集合)判定遗漏:置 review 循环之后仍存在任何 open/rejected 条目即显式失败。
|
|
298
|
+
// 重复 id 的第二套条目(id 已在 completed_task_ids 中但状态未变更)与真遗漏条目都会在此暴露,
|
|
299
|
+
// 避免旧逻辑按 id 集合判定漏检而让下游 forwardGatePassed 静默死锁。
|
|
300
|
+
const remaining = taskListOf(item).filter(
|
|
301
|
+
(t) => t.status === "open" || t.status === "rejected"
|
|
302
|
+
)
|
|
303
|
+
if (remaining.length > 0) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`以下 task 处于 open/rejected 状态且未在 completed_task_ids 中:\n` +
|
|
306
|
+
remaining.map((t) => `- #${t.id}(${t.status}) ${t.title}`).join("\n") +
|
|
307
|
+
`\n请将未完成的 task 列在 completed_task_ids 中,或改用 blocker 上报阻塞。`
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (params.self_check_results) {
|
|
312
|
+
item.metadata["self_check_results"] = params.self_check_results
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** 把 boundary_expansion 的目录/包合并进执行边界(metadata 为事实源,缺失则忽略)。 */
|
|
317
|
+
function mergeBoundaryInto(item: WorkItem, expansion: NonNullable<AgentSubmitParams["boundary_expansion"]>): void {
|
|
318
|
+
const boundary = item.metadata["execution_boundary"] as {
|
|
319
|
+
allowed_directories: string[]
|
|
320
|
+
allowed_packages: string[]
|
|
321
|
+
notes: string
|
|
322
|
+
} | undefined
|
|
323
|
+
if (!boundary) return
|
|
324
|
+
for (const dir of expansion.allowed_directories ?? []) {
|
|
325
|
+
if (!boundary.allowed_directories.includes(dir)) boundary.allowed_directories.push(dir)
|
|
326
|
+
}
|
|
327
|
+
for (const pkg of expansion.allowed_packages ?? []) {
|
|
328
|
+
if (!boundary.allowed_packages.includes(pkg)) boundary.allowed_packages.push(pkg)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* verdict=failed 必须有具体不通过理由(对齐 main assertPassedConsistency)。
|
|
334
|
+
* 理由判定(认遗留 issue 或实际接受的 new_children):
|
|
335
|
+
* - verify_task:本次 failed_tasks 非空,或 new_children 含 Low+,或存在未终态的 Low+ task 报源层阻塞 child
|
|
336
|
+
* - verify_tool:本次 new_children 含 Low+,或存在未终态的 Low+ tool 报源层阻塞 child
|
|
337
|
+
* - verify_quality:本次 new_children 含 Low+ 且维度属于当前提交 agent,或存在未终态的 Low+ quality 报源层
|
|
338
|
+
* 阻塞 child 且 dimension 属于当前提交 agent 维度(新报与遗留理由均按维度过滤,F3;报源层由 source 反推)
|
|
339
|
+
* - verify_cleanup:本次 new_children 含 Low+,或存在未终态的 Low+ 阻塞 child(不按维度/报源层过滤——
|
|
340
|
+
* 收尾层须对全部残留阻塞负责,任何层的遗留阻塞都构成不通过理由)
|
|
341
|
+
* 理由判定在 dedupeNewChildren 之后调用(F4):传入的 newChildren 为已去重的 accepted,重复新报不构成理由。
|
|
342
|
+
* 不满足即抛错,handleReviewParams 在 submitForStep 之前调用,零状态变更。
|
|
343
|
+
*/
|
|
344
|
+
function assertFailedHasReason(
|
|
345
|
+
item: WorkItem,
|
|
346
|
+
params: AgentSubmitParams,
|
|
347
|
+
newChildren: WorkItem[],
|
|
348
|
+
stepId: string,
|
|
349
|
+
agent: string,
|
|
350
|
+
): void {
|
|
351
|
+
const hasNewBlocking = newChildren.some((c) => isBlockingSeverity(c.severity))
|
|
352
|
+
const existingBlocking = item.children.filter((c) => !isTerminalPhase(c.phase) && isBlockingSeverity(c.severity))
|
|
353
|
+
let layerName: string
|
|
354
|
+
let hasReason: boolean
|
|
355
|
+
if (stepId === "verify_task") {
|
|
356
|
+
layerName = "任务层"
|
|
357
|
+
hasReason =
|
|
358
|
+
(params.failed_tasks?.length ?? 0) > 0 ||
|
|
359
|
+
hasNewBlocking ||
|
|
360
|
+
existingBlocking.some((c) => resolveChildIssueFields(c).sourcePhase === "task")
|
|
361
|
+
} else if (stepId === "verify_tool") {
|
|
362
|
+
layerName = "工具层"
|
|
363
|
+
hasReason = hasNewBlocking || existingBlocking.some((c) => resolveChildIssueFields(c).sourcePhase === "tool")
|
|
364
|
+
} else if (stepId === "verify_cleanup") {
|
|
365
|
+
layerName = "收尾层"
|
|
366
|
+
hasReason = hasNewBlocking || existingBlocking.length > 0
|
|
367
|
+
} else {
|
|
368
|
+
const dimension = agentToReviewDimension(agent)
|
|
369
|
+
layerName = dimension ? `AI 审查层(${dimension})` : "AI 审查层"
|
|
370
|
+
// 新报与遗留阻塞理由均按当前 agent 维度过滤(对齐 main:verify_quality failed 理由须归属本维)。
|
|
371
|
+
const dimMatches = (c: WorkItem): boolean => resolveChildIssueFields(c).dimension === dimension
|
|
372
|
+
hasReason =
|
|
373
|
+
newChildren.some((c) => isBlockingSeverity(c.severity) && dimMatches(c)) ||
|
|
374
|
+
existingBlocking.some(
|
|
375
|
+
(c) => resolveChildIssueFields(c).sourcePhase === "quality" && dimMatches(c),
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
if (!hasReason) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`${layerName} 审核声称 passed=false,但不存在未解决的阻塞 issue。passed=false 时必须提供至少一个 Low+ issue 或 failed_task_id 作为不通过理由。`
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** review step(verify_tool/verify_task/verify_quality)参数处理。 */
|
|
386
|
+
function handleReviewParams(
|
|
387
|
+
item: WorkItem,
|
|
388
|
+
params: AgentSubmitParams,
|
|
389
|
+
newChildren: WorkItem[],
|
|
390
|
+
stepId: string,
|
|
391
|
+
opts: { capabilityTags: string[]; skipMustDoGate: boolean },
|
|
392
|
+
): void {
|
|
393
|
+
// passed=true 与 Low+ 新报一致性:passed 只能带 Info 新报
|
|
394
|
+
if (params.verdict === "passed" && newChildren.some((c) => isBlockingSeverity(c.severity))) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
"review step 声称 passed=true,但 new_children 中包含 Low 及以上级别 issue。passed=true 只能带 Info 新报;有 Low+ issue 时必须设 verdict=failed。"
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
// boundary_expansion 仅 passed=false 允许
|
|
400
|
+
if (params.boundary_expansion) {
|
|
401
|
+
if (params.verdict === "passed") {
|
|
402
|
+
throw new Error("passed=true 时不允许边界扩展。boundary_expansion 仅 verdict=failed 有效。")
|
|
403
|
+
}
|
|
404
|
+
mergeBoundaryInto(item, params.boundary_expansion)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (params.test_results) item.metadata["test_results"] = params.test_results
|
|
408
|
+
|
|
409
|
+
if (params.validation_steps) {
|
|
410
|
+
// 结构化 skip_reason 校验(升级既有「非空即可」):未完成项必须带合法结构化降级声明
|
|
411
|
+
assertStructuredSkipReasons(params.validation_steps)
|
|
412
|
+
item.metadata["validation_steps"] = params.validation_steps
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 必做清单覆盖度门禁:verdict=passed 且非补交豁免(仅裁定参数)时,validation_steps 须逐项覆盖
|
|
416
|
+
// 当前 agent 能力集解析出的质量门 skill 必做清单(must_do)。遗漏必做项且无有效降级证据 → 拒绝提交。
|
|
417
|
+
// 未声明 must_do 的 skill / 解析不到质量门 skill 的 step 自动跳过(不误伤 verify_task/verify_quality)。
|
|
418
|
+
if (params.verdict === "passed" && !opts.skipMustDoGate) {
|
|
419
|
+
const uncovered = uncoveredMustDo(opts.capabilityTags, params.validation_steps)
|
|
420
|
+
if (uncovered.length > 0) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
`提交的验证步骤未覆盖质量门 skill 必做清单,缺少以下必做项:${uncovered.join("、")}。\n` +
|
|
423
|
+
`请按已加载质量门类 skill 的必做清单逐项执行并逐项申报结果(completed=true);` +
|
|
424
|
+
`确实无法执行的必做项须以结构化 skip_reason 申报降级理由` +
|
|
425
|
+
`(格式:${SKIP_REASON_FORMAT})。`
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (params.verified_tasks?.length || params.failed_tasks?.length) {
|
|
431
|
+
const tasks = taskListOf(item)
|
|
432
|
+
const validIds = new Set(tasks.map((t) => t.id))
|
|
433
|
+
const verified = normalizeTaskChildIds(params.verified_tasks ?? [], item)
|
|
434
|
+
const failed = (params.failed_tasks ?? []).map((f) => ({
|
|
435
|
+
task_id: normalizeTaskChildIds([f.task_id], item)[0],
|
|
436
|
+
reason: f.reason,
|
|
437
|
+
}))
|
|
438
|
+
for (const id of [...verified, ...failed.map((f) => f.task_id)]) {
|
|
439
|
+
if (!validIds.has(id)) {
|
|
440
|
+
throw new Error(`非法 task id: "${id}"。\n合法 id:${Array.from(validIds).join(", ")}。`)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (params.verdict === "passed" && failed.length > 0) {
|
|
444
|
+
throw new Error("passed=true 时不允许提供 failed_tasks;有任务未通过必须设 verdict=failed。")
|
|
445
|
+
}
|
|
446
|
+
// review 态 task child(submitted 语义)必须被 verified/failed 覆盖
|
|
447
|
+
const submitted = tasks.filter((t) => t.status === "submitted")
|
|
448
|
+
const covered = new Set([...verified, ...failed.map((f) => f.task_id)])
|
|
449
|
+
const uncovered = submitted.filter((t) => !covered.has(t.id))
|
|
450
|
+
if (uncovered.length > 0) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`以下 submitted task 未被 verified_tasks 或 failed_tasks 覆盖:\n` +
|
|
453
|
+
uncovered.map((t) => `- #${t.id} ${t.title}`).join("\n")
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
for (const id of verified) {
|
|
457
|
+
const child = taskChildById(item, id)
|
|
458
|
+
if (child && child.phase === "review") {
|
|
459
|
+
child.phase = "done"
|
|
460
|
+
delete child.metadata["reject_reason"]
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
for (const f of failed) {
|
|
464
|
+
const child = taskChildById(item, f.task_id)
|
|
465
|
+
if (child && child.phase === "review") {
|
|
466
|
+
child.phase = "todo"
|
|
467
|
+
child.metadata["reject_reason"] = f.reason
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* 通用 step 提交(opx_agent_submit 的 execute)。
|
|
475
|
+
*
|
|
476
|
+
* 单轨操作 state.workItems:定位活跃 task WorkItem → 按 step 处理业务参数 → 调 submitForStep 驱动
|
|
477
|
+
* workflow 状态机 → 把 item 的 phase/children/tags 变更直接持久化回 state.workItems(唯一事实源)。
|
|
478
|
+
*
|
|
479
|
+
* 全局写锁:整个 execute 的读-改-写全程持有 acquireLock/releaseLock,覆盖所有角色提交,
|
|
480
|
+
* 防止多 agent 并行 read-modify-write 丢 tag。越权/校验失败抛错时零落盘。
|
|
481
|
+
*
|
|
482
|
+
* checkpoint_decision 为检查点决策入口:仅当 item 处于检查点态(_checkpoint 标记
|
|
483
|
+
* 或 retryCount 达上限且存在未终态 children)时可用,直接应用决策并落盘,不走提交路径。
|
|
484
|
+
*/
|
|
485
|
+
export async function agentSubmitExecute(params: AgentSubmitParams, ctx: ToolContext): Promise<string> {
|
|
486
|
+
const lockPath = await getLockPath(ctx.worktree, params.change_id)
|
|
487
|
+
await acquireLock(lockPath)
|
|
488
|
+
try {
|
|
489
|
+
const state = await readStateByWorktree(ctx.worktree, params.change_id)
|
|
490
|
+
if (!state) throw new Error("编排会话未初始化。请先调用 opx_orch_init。")
|
|
491
|
+
const workflow = loadTaskWorkflow()
|
|
492
|
+
const item = resolveTaskWorkItem(state)
|
|
493
|
+
|
|
494
|
+
if (params.checkpoint_decision) {
|
|
495
|
+
return applyCheckpointDecision(params, item, workflow, ctx, state)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// worktree 就绪硬门禁:worktree_path 缺失时拒绝提交(checkpoint_decision 不走提交路径,已在上方绕过)。
|
|
499
|
+
// 所有 step(analyze/implement/verify_tool/verify_task/verify_quality)统一受此门禁约束。
|
|
500
|
+
const wtPath = typeof item.metadata["worktree_path"] === "string" ? item.metadata["worktree_path"] : undefined
|
|
501
|
+
if (!wtPath) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
"worktree 未就绪,当前拒绝提交。\n请编排者先调用 opx_orch_set_worktree 确保 worktree 就绪后再提交。"
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 参数归一化:issue id 去 # 前缀(fixed/exempt/adjudication/recheck 均接受带 # 的 tg.issue 序号引用)
|
|
508
|
+
if (params.fixed_issue_ids) params.fixed_issue_ids = params.fixed_issue_ids.map(normalizeIssueId)
|
|
509
|
+
if (params.exempt_issue_ids) params.exempt_issue_ids = params.exempt_issue_ids.map(normalizeIssueId)
|
|
510
|
+
if (params.exempt_adjudications) {
|
|
511
|
+
for (const adj of params.exempt_adjudications) adj.issue_id = normalizeIssueId(adj.issue_id)
|
|
512
|
+
}
|
|
513
|
+
if (params.recheck_adjudications) {
|
|
514
|
+
for (const adj of params.recheck_adjudications) adj.issue_id = normalizeIssueId(adj.issue_id)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 无效 fixed/exempt id 入口显式拒绝(不静默吞掉修复/豁免声明):未命中的 id 抛错并列出可用 id 清单。
|
|
518
|
+
// 以 resolveChildByIssueId 做权威判定,与 submitForStep 的 childById 及 collectFixedExemptLayers 消费端解析语义一致。
|
|
519
|
+
const knownChildIds = new Set<string>()
|
|
520
|
+
for (const c of item.children) {
|
|
521
|
+
knownChildIds.add(c.id)
|
|
522
|
+
if (c.externalId) knownChildIds.add(c.externalId)
|
|
523
|
+
}
|
|
524
|
+
const availableIssueIds = (): string => [...knownChildIds].sort().join(", ")
|
|
525
|
+
for (const id of params.fixed_issue_ids ?? []) {
|
|
526
|
+
if (!resolveChildByIssueId(item, id)) {
|
|
527
|
+
throw new Error(`fixed_issue_ids 中包含无效 issue id: "${id}"。\n可用 issue ID: ${availableIssueIds()}`)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
for (const id of params.exempt_issue_ids ?? []) {
|
|
531
|
+
const child = resolveChildByIssueId(item, id)
|
|
532
|
+
if (!child) {
|
|
533
|
+
throw new Error(`exempt_issue_ids 中包含无效 issue id: "${id}"。\n可用 issue ID: ${availableIssueIds()}`)
|
|
534
|
+
}
|
|
535
|
+
// Info 级 issue 不阻塞任何流程,豁免无意义且白耗一轮申报/裁定:developer 提交豁免申请的唯一入口在此拦截。
|
|
536
|
+
// 存量的已带 exempt_request 标记的 Info issue 不受影响(adjudicateExempt 裁定路径不经过此处)。
|
|
537
|
+
if (isInfoSeverity(child.severity)) {
|
|
538
|
+
throw new Error(`豁免申请失败:issue "${child.id}" 为 Info 级 issue,不阻塞提交,无需申请豁免。`)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
for (const id of params.recheck_adjudications?.map((a) => a.issue_id) ?? []) {
|
|
542
|
+
if (!resolveChildByIssueId(item, id)) {
|
|
543
|
+
throw new Error(`recheck_adjudications 中包含无效 issue id: "${id}"。\n可用 issue ID: ${availableIssueIds()}`)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// 重复提交守卫(仅 review step):同 step 同 agent 已以 passed 通过后不允许重复提交;failed 允许重提
|
|
548
|
+
// (回退重审期 failed tag 未被归因清空时须可重提,如 verify_task 仅 failed_tasks 驳回)。
|
|
549
|
+
// 补交豁免:仅携带裁定类参数(recheck_adjudications / exempt_adjudications,无其他实质参数)的补交放行——
|
|
550
|
+
// 上次提交漏带复核/豁免裁定导致本层 review 态 issue 阻塞门禁时(engine.blockedSupplementAgents 已推导
|
|
551
|
+
// 应补交的报源 reviewer),reviewer 可补带裁定重提解除阻塞;空提交/携带其他实质参数(含 fixed_issue_ids /
|
|
552
|
+
// exempt_issue_ids)的重复提交仍被拒绝。
|
|
553
|
+
// 守卫在一切裁定(豁免/复核)之前执行,保证守卫拒绝时零副作用。step 归属按 stepMap 直接判定
|
|
554
|
+
// (不依赖 assertSubmitRouting,保持越权 step 提交的裁定拦截语义不变)。
|
|
555
|
+
const guardStepPhase = workflow.stepMap.get(params.step_id)?.phase.name
|
|
556
|
+
if (
|
|
557
|
+
guardStepPhase === "review" &&
|
|
558
|
+
getStepVerdict(item, params.step_id, ctx.agent) === "passed" &&
|
|
559
|
+
!isSupplementOnly(params)
|
|
560
|
+
) {
|
|
561
|
+
throw new Error(`重复提交守卫:agent "${ctx.agent}" 已在 step "${params.step_id}" 以 passed 通过,不允许重复提交。`)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// 裁定一致性守卫(统一判定 exempt/recheck 驳回组合):任一裁定参数含 rejected 且提交级 verdict=passed
|
|
565
|
+
// 即抛错(驳回 issue 需修复,审查不能判定通过)。守卫在一切裁定(adjudicateExempt/adjudicateRecheck)写入
|
|
566
|
+
// 与 submitForStep 应用 verdict 之前执行,拒绝时零副作用(含混合裁定清单)。
|
|
567
|
+
assertRejectedAdjudicationNotPassed(params)
|
|
568
|
+
|
|
569
|
+
// 本次提交待写入项目级跨 change 豁免清单的 issue child(仅 dismissed 收集)。
|
|
570
|
+
// 只收集不落盘:submitForStep 成功后再统一写入,保证同批后续校验(路由 / new_children /
|
|
571
|
+
// assertFailedHasReason 等)抛错时清单与内存状态同步零变更。
|
|
572
|
+
const pendingExemptions: WorkItem[] = []
|
|
573
|
+
|
|
574
|
+
// 先裁定豁免申请(越权/不可路由/跨维抛错):submitForStep 尚未执行,保证越权裁定零副作用。
|
|
575
|
+
for (const adj of params.exempt_adjudications ?? []) {
|
|
576
|
+
const child = resolveChildByIssueId(item, adj.issue_id)
|
|
577
|
+
if (!child) {
|
|
578
|
+
throw new Error(`豁免裁定失败:issue "${adj.issue_id}" 不存在于 item "${item.id}" 的 children 中。`)
|
|
579
|
+
}
|
|
580
|
+
adjudicateExempt(item, workflow, { issueId: child.id, agentKey: ctx.agent, action: adj.action })
|
|
581
|
+
// dismissed 且携带 rule 时记入待落盘豁免清单(后续 change 同 rule+file+line 命中按 Info
|
|
582
|
+
// 处理,避免 worktree 全量扫描把存量已豁免问题反复报为阻塞)。此处仅收集,submitForStep
|
|
583
|
+
// 成功后再统一调用 writeDismissedExemption 落盘——同批后续校验抛错时清单不落盘(零状态变更
|
|
584
|
+
// 一致性)。giveup 走 applyCheckpointGiveup,不经此路径,天然排除。
|
|
585
|
+
if (adj.action === "dismissed") {
|
|
586
|
+
pendingExemptions.push(child)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// 复核已修复待复核(review 态)issue:passed→done,rejected→todo + refix_count + reject_reason。
|
|
591
|
+
// 谁提谁裁定,越权/非 review 态/缺驳回原因抛错零副作用,先于 submitForStep 执行使门禁按新 phase 判定。
|
|
592
|
+
for (const adj of params.recheck_adjudications ?? []) {
|
|
593
|
+
const child = resolveChildByIssueId(item, adj.issue_id)
|
|
594
|
+
if (!child) {
|
|
595
|
+
throw new Error(`复核失败:issue "${adj.issue_id}" 不存在于 item "${item.id}" 的 children 中。`)
|
|
596
|
+
}
|
|
597
|
+
adjudicateRecheck(item, workflow, {
|
|
598
|
+
issueId: child.id,
|
|
599
|
+
agentKey: ctx.agent,
|
|
600
|
+
verdict: adj.verdict,
|
|
601
|
+
rejectReason: adj.reject_reason,
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// 路由/归属校验前置(与 submitForStep 共用),越权/错 step 在参数处理前拦截。
|
|
606
|
+
assertSubmitRouting(workflow, item, params.step_id, ctx.agent)
|
|
607
|
+
const stepPhase = workflow.stepMap.get(params.step_id)!.phase.name
|
|
608
|
+
|
|
609
|
+
if ((params.new_children?.length ?? 0) > 0 && stepPhase !== "review") {
|
|
610
|
+
throw new Error(`仅 review 阶段的 step 允许提报新 issue(new_children),当前 step "${params.step_id}" 属于 "${stepPhase}" 阶段,拒绝提报。`)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// worktree 编辑边界强化(56ddfe9 意图):review 提报的 issue 文件路径必须位于 worktree 内,路径逃逸拒绝。
|
|
614
|
+
if (stepPhase === "review" && (params.new_children?.length ?? 0) > 0) {
|
|
615
|
+
assertIssueFilesWithin(params.new_children as Array<{ file?: string }>, wtPath ?? ctx.worktree)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// new_children 空字段入口显式拒绝:空 id/title/description 会污染后续引用与去重 key,禁止入库。
|
|
619
|
+
// dimension 提交语义按报源身份分类(校验在一切状态写入之前,抛错零副作用):
|
|
620
|
+
// - quality reviewer(agentToReviewDimension 命中):维度由报源推断;显式填写须与报源维度一致,
|
|
621
|
+
// 不一致抛错(等价推断不抛错);未填写由 buildIssueChild 推断写入。
|
|
622
|
+
// - tool/task reviewer:dimension 必填(归因维度),缺失或非法值抛错。
|
|
623
|
+
const sourceDim = agentToReviewDimension(ctx.agent)
|
|
624
|
+
for (const nc of params.new_children ?? []) {
|
|
625
|
+
if (!nc.id || !nc.id.trim()) {
|
|
626
|
+
throw new Error("new_children 中的 issue id 不能为空,请为每个新 issue 指定唯一 id。")
|
|
627
|
+
}
|
|
628
|
+
if (!nc.title || !nc.title.trim()) {
|
|
629
|
+
throw new Error(`new_children 中 issue "${nc.id}" 的 title 不能为空。`)
|
|
630
|
+
}
|
|
631
|
+
if (!nc.description || !nc.description.trim()) {
|
|
632
|
+
throw new Error(`new_children 中 issue "${nc.id}" 的 description 不能为空。`)
|
|
633
|
+
}
|
|
634
|
+
if (sourceDim) {
|
|
635
|
+
if (nc.dimension !== undefined) {
|
|
636
|
+
if (!(REVIEW_DIMENSIONS as readonly string[]).includes(nc.dimension)) {
|
|
637
|
+
throw new Error(
|
|
638
|
+
`new_children 中 issue "${nc.id}" 的 dimension 非法:"${nc.dimension}"。合法维度:${REVIEW_DIMENSIONS.join(", ")}。`
|
|
639
|
+
)
|
|
640
|
+
}
|
|
641
|
+
if (nc.dimension !== sourceDim) {
|
|
642
|
+
throw new Error(
|
|
643
|
+
`new_children 中 issue "${nc.id}" 显式声明的 dimension "${nc.dimension}" 与报源维度 "${sourceDim}" 不一致。quality reviewer 报 issue 的维度由报源推断,显式填写时必须与报源维度一致。`
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
} else {
|
|
648
|
+
if (nc.dimension === undefined) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
`new_children 中 issue "${nc.id}" 未声明归因维度 dimension。tool/task reviewer 报 issue 必须显式声明 dimension(归因维度),合法值:${REVIEW_DIMENSIONS.join(", ")}。`
|
|
651
|
+
)
|
|
652
|
+
}
|
|
653
|
+
if (!(REVIEW_DIMENSIONS as readonly string[]).includes(nc.dimension)) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`new_children 中 issue "${nc.id}" 的 dimension 非法:"${nc.dimension}"。合法维度:${REVIEW_DIMENSIONS.join(", ")}。`
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const newChildren = (params.new_children ?? []).map((nc) => buildIssueChild(nc, ctx.agent))
|
|
662
|
+
|
|
663
|
+
if (stepPhase === "review") {
|
|
664
|
+
// 必做清单覆盖度门禁取「当前提交 agent 的能力集」:verify_quality 多 agent 各自 capability 解析
|
|
665
|
+
// 不出质量门 skill(自动跳过),verify_tool 的 reviewer 按 quality-gate 能力集校验。
|
|
666
|
+
const stepCfg = workflow.stepMap.get(params.step_id)!.step
|
|
667
|
+
const agentCfg = stepCfg.agents.find((a) => a.id === ctx.agent)
|
|
668
|
+
handleReviewParams(item, params, newChildren, params.step_id, {
|
|
669
|
+
capabilityTags: agentCfg?.capability_tags ?? [],
|
|
670
|
+
// 补交豁免(仅裁定参数,无实质提交)不重复校验必做清单覆盖度
|
|
671
|
+
skipMustDoGate: isSupplementOnly(params),
|
|
672
|
+
})
|
|
673
|
+
} else if (stepPhase === "todo") {
|
|
674
|
+
handleAnalyzeParams(item, params)
|
|
675
|
+
} else if (stepPhase === "in_progress") {
|
|
676
|
+
handleImplementParams(item, params)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const { accepted } = dedupeNewChildren(item, newChildren)
|
|
680
|
+
|
|
681
|
+
// 命中项目级跨 change 豁免清单降级(顺序敏感:必须在 assertFailedHasReason 之前):
|
|
682
|
+
// 存量已裁定豁免的 (rule+file+line) 本轮再报时降为 Info 级,不阻塞、无需再次豁免;
|
|
683
|
+
// 否则仅含存量豁免问题的 change 中 reviewer 能 failed 却无真实待办,形成死锁。
|
|
684
|
+
if (stepPhase === "review" && accepted.length > 0) {
|
|
685
|
+
const store = await readExemptions(ctx.worktree)
|
|
686
|
+
applyExemptionDowngrade(accepted, store)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// verdict=failed 必须有具体不通过理由:理由判定在去重之后,仅依据实际接受的 new_children——
|
|
690
|
+
// 重复新报(或与既有 child 同 key 被去重)不构成不通过理由,避免守卫放行但实际零新增 issue。
|
|
691
|
+
if (stepPhase === "review" && params.verdict === "failed") {
|
|
692
|
+
assertFailedHasReason(item, params, accepted, params.step_id, ctx.agent)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// dev 修复/豁免后按归因分层重置 review 验证标记(仅 implement step)
|
|
696
|
+
if (stepPhase === "in_progress" && (params.fixed_issue_ids?.length || params.exempt_issue_ids?.length)) {
|
|
697
|
+
resetReviewTagsOnFix(
|
|
698
|
+
item,
|
|
699
|
+
collectFixedExemptLayers(item, params.fixed_issue_ids ?? [], params.exempt_issue_ids ?? []),
|
|
700
|
+
)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
submitForStep(item, workflow, {
|
|
704
|
+
stepId: params.step_id,
|
|
705
|
+
agentKey: ctx.agent,
|
|
706
|
+
verdict: params.verdict,
|
|
707
|
+
fixedIds: params.fixed_issue_ids,
|
|
708
|
+
exemptIds: params.exempt_issue_ids,
|
|
709
|
+
newChildren: accepted,
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
// arch_submit 主仓库污染文档自动合并兜底(56ddfe9 意图):analyze step(架构师)以 passed 提交后,
|
|
713
|
+
// 若主仓库本 change 目录下存在未提交 openspec 文档污染,并入 worktree 分支并清理主仓库工作树。
|
|
714
|
+
if (
|
|
715
|
+
stepPhase === "todo" &&
|
|
716
|
+
params.step_id === "analyze" &&
|
|
717
|
+
params.verdict === "passed" &&
|
|
718
|
+
wtPath &&
|
|
719
|
+
typeof item.metadata["branch_name"] === "string"
|
|
720
|
+
) {
|
|
721
|
+
const baseRef = typeof item.metadata["base_ref"] === "string" ? item.metadata["base_ref"] : null
|
|
722
|
+
await reconcileMainPollution(ctx.worktree, wtPath, {
|
|
723
|
+
changeId: state.changeId,
|
|
724
|
+
baseRef,
|
|
725
|
+
})
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// G19:verify_task passed 且全部 task child 达终态(done)→ 同步 tasks.md 复选框 [ ] → [x]
|
|
729
|
+
if (
|
|
730
|
+
params.step_id === "verify_task" &&
|
|
731
|
+
params.verdict === "passed" &&
|
|
732
|
+
taskChildrenOf(item).every((c) => isTerminalPhase(c.phase))
|
|
733
|
+
) {
|
|
734
|
+
const worktreePath =
|
|
735
|
+
typeof item.metadata["worktree_path"] === "string" ? item.metadata["worktree_path"] : ctx.worktree
|
|
736
|
+
await markTaskGroupCheckboxesComplete(worktreePath, params.change_id, state.taskGroupId)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// tool review 检查点增量检测(A3):verify_tool 的 reviewer-tool 提交成功后记录当前 HEAD,
|
|
740
|
+
// 供下次重进 verify_tool 时仅对比「检查点 → 当前 HEAD」区间的变更(无代码/配置变更时直提 passed,
|
|
741
|
+
// 省去全量工具扫描)。写入位于 submitForStep 成功与 writeState 之间,任何异常路径均不落盘(零状态变更)。
|
|
742
|
+
if (params.step_id === "verify_tool" && agentToReviewLayer(ctx.agent) === "tool") {
|
|
743
|
+
const head = await getCurrentHead(wtPath)
|
|
744
|
+
if (head) item.metadata["_tool_review_checkpoint"] = head
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// 项目级豁免清单统一落盘(submitForStep 成功之后、writeState 之前,与 checkpoint 写入同一位次):
|
|
748
|
+
// 任何前置校验(路由 / new_children / dimension / assertFailedHasReason)抛错均不落盘。
|
|
749
|
+
// 写入失败仅告警,不阻断主流程(writeDismissedExemption 内部容错)。
|
|
750
|
+
for (const child of pendingExemptions) {
|
|
751
|
+
await writeDismissedExemption(ctx.worktree, child, { changeId: state.changeId, exemptedBy: ctx.agent })
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
await writeState(ctx.worktree, state)
|
|
755
|
+
return SUBMIT_OK_MESSAGE
|
|
756
|
+
} finally {
|
|
757
|
+
releaseLock(lockPath)
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* 检查点决策应用:校验 item 处于检查点态(_checkpoint 标记或 retryCount 达上限且
|
|
763
|
+
* 存在未终态 children)后,按决策应用引擎状态变更,并落盘 state。
|
|
764
|
+
* 决策应用后不经过 submitForStep 提交路径。
|
|
765
|
+
*/
|
|
766
|
+
async function applyCheckpointDecision(
|
|
767
|
+
params: AgentSubmitParams,
|
|
768
|
+
item: WorkItem,
|
|
769
|
+
workflow: LoadedWorkflow,
|
|
770
|
+
ctx: ToolContext,
|
|
771
|
+
state: OrchestrateState,
|
|
772
|
+
): Promise<string> {
|
|
773
|
+
const decision = params.checkpoint_decision as "continue" | "giveup"
|
|
774
|
+
const stepId = item.currentStep
|
|
775
|
+
if (!stepId) throw new Error(`检查点决策失败:item "${item.id}" 无 currentStep,无法定位当前检查点 step。`)
|
|
776
|
+
const entry = workflow.stepMap.get(stepId)
|
|
777
|
+
if (!entry) throw new Error(`检查点决策失败:当前 step "${stepId}" 未在 workflow 中声明。`)
|
|
778
|
+
const step = entry.step
|
|
779
|
+
const atCheckpoint = item.metadata["_checkpoint"] === true || checkpointTriggered(item, workflow, step)
|
|
780
|
+
if (!atCheckpoint) throw new Error(`检查点决策失败:当前不在检查点状态,无法执行 ${decision}。`)
|
|
781
|
+
if (decision === "continue") {
|
|
782
|
+
applyCheckpointContinue(item, step)
|
|
783
|
+
} else {
|
|
784
|
+
// giveup 必做清单核对(堵 giveup 侧门):当前 step 若含质量门 skill 必做清单(must_do),
|
|
785
|
+
// 未覆盖项必须有结构化降级理由(checkpoint_skip_reasons),否则拒绝 giveup——
|
|
786
|
+
// 杜绝「放弃审查 → 自动推进 → 收尾合码」的无痕绕过通道。非质量门 step 或未声明 must_do
|
|
787
|
+
// 的 skill 环境下 uncovered 为空,giveup 行为不受影响(与「编排改动须评估全流程连锁影响」一致)。
|
|
788
|
+
const stepCaps = Array.from(new Set(step.agents.flatMap((a) => a.capability_tags)))
|
|
789
|
+
const prevSteps = (item.metadata["validation_steps"] as Array<{ step: string; completed: boolean }> | undefined)
|
|
790
|
+
const uncovered = uncoveredMustDo(stepCaps, prevSteps)
|
|
791
|
+
if (uncovered.length > 0) {
|
|
792
|
+
const provided = new Map<string, unknown>()
|
|
793
|
+
for (const r of params.checkpoint_skip_reasons ?? []) {
|
|
794
|
+
if (isValidSkipData(r) && typeof r.item === "string") provided.set(r.item, r)
|
|
795
|
+
}
|
|
796
|
+
const missing = uncovered.filter((m) => !provided.has(m))
|
|
797
|
+
if (missing.length > 0) {
|
|
798
|
+
throw new Error(
|
|
799
|
+
`giveup 无法应用:当前 step 的质量门必做清单存在未覆盖项,须逐项提供结构化降级理由。\n` +
|
|
800
|
+
`缺少降级理由的必做项:${missing.join("、")}\n` +
|
|
801
|
+
`请通过 checkpoint_skip_reasons 逐项提供(giveup 未覆盖项不可跳过申报):\n` +
|
|
802
|
+
`[${SKIP_REASON_FORMAT}]`
|
|
803
|
+
)
|
|
804
|
+
}
|
|
805
|
+
// 留痕:giveup 时对未覆盖必做项的结构化降级申报,供收尾审计(giveup 不再无痕放行)
|
|
806
|
+
item.metadata["_giveup_validation"] = params.checkpoint_skip_reasons
|
|
807
|
+
}
|
|
808
|
+
applyCheckpointGiveup(item, step)
|
|
809
|
+
// giveup 留痕:写来源元数据供审计,避免「放弃」变成无痕放行。
|
|
810
|
+
item.metadata["_giveup"] = true
|
|
811
|
+
// B3:giveup 同步处理 blockers——未 resolved 条目随放弃置 resolved(记录原因),
|
|
812
|
+
// 与收尾门禁(lifecycle.ts 的 unresolved blockers 拦截)对齐,避免 giveup 后无法收尾。
|
|
813
|
+
for (const blocker of itemBlockers(item)) {
|
|
814
|
+
if (blocker.status !== "resolved") {
|
|
815
|
+
blocker.status = "resolved"
|
|
816
|
+
blocker.architectConclusion = "随放弃处理"
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
// B1:giveup 应用后沿 on_pass 链式推进(末位 giveup → done 可收尾;非末位 → 落下一个待执行 step),
|
|
820
|
+
// 复用 submitForStep 的推进路径(chainPassAdvance)。giveup 已把所有未解决 children 置 cancelled、
|
|
821
|
+
// 当前 step 全 passed,applyTransition 的 done 拦截(task children 终态检查)应恒通过;
|
|
822
|
+
// 若仍被拦截(异常态),原因写 _advance_block_reason 供 opx_status 视图诊断。
|
|
823
|
+
const advance = applyTransition(item, workflow, "pass")
|
|
824
|
+
if (advance.advanced) {
|
|
825
|
+
delete item.metadata["_advance_block_reason"]
|
|
826
|
+
chainPassAdvance(item, workflow, advance.target)
|
|
827
|
+
} else {
|
|
828
|
+
item.metadata["_advance_block_reason"] = advance.reason
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
await writeState(ctx.worktree, state)
|
|
832
|
+
return renderCheckpointDecisionResult(decision, item, step)
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** 渲染检查点决策结果 markdown:决策已应用 + 后续动作。 */
|
|
836
|
+
function renderCheckpointDecisionResult(decision: "continue" | "giveup", item: WorkItem, step: StepConfig): string {
|
|
837
|
+
if (decision === "continue") {
|
|
838
|
+
return [
|
|
839
|
+
"## ✅ 检查点决策已应用:continue",
|
|
840
|
+
"",
|
|
841
|
+
`- **step**: \`${step.id}\` 的 agent 裁决已重置为 pending,可重新提交该 step。`,
|
|
842
|
+
`- **当前 phase**: ${item.phase}`,
|
|
843
|
+
"",
|
|
844
|
+
].join("\n")
|
|
845
|
+
}
|
|
846
|
+
const cancelled = item.children.filter((c) => c.phase === "cancelled").map((c) => c.id)
|
|
847
|
+
const blockReason = typeof item.metadata["_advance_block_reason"] === "string"
|
|
848
|
+
? item.metadata["_advance_block_reason"]
|
|
849
|
+
: null
|
|
850
|
+
const lines = [
|
|
851
|
+
"## ✅ 检查点决策已应用:giveup",
|
|
852
|
+
"",
|
|
853
|
+
`- **step**: \`${step.id}\` 已标记 completed(所有 agent 视为 passed)。`,
|
|
854
|
+
`- **未解决 children**(${cancelled.length} 个)已置 cancelled:${cancelled.join("、") || "(无)"}`,
|
|
855
|
+
`- **当前 phase**: ${item.phase}${item.currentStep ? `(step \`${item.currentStep}\`)` : ""}`,
|
|
856
|
+
]
|
|
857
|
+
if (blockReason) {
|
|
858
|
+
lines.push(`- **⚠️ 推进被拦截**: ${blockReason}`)
|
|
859
|
+
lines.push(`- 请用 \`opx_status\` 查看诊断,必要时 \`opx_orch_init(recovery=...)\` 恢复。`)
|
|
860
|
+
}
|
|
861
|
+
lines.push("")
|
|
862
|
+
return lines.join("\n")
|
|
863
|
+
}
|