@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,576 @@
1
+ import type {
2
+ WorkItem,
3
+ WorkItemPhase,
4
+ Verdict,
5
+ StepConfig,
6
+ StepAdjudication,
7
+ } from "./types.ts"
8
+ import { WORK_ITEM_PHASES, BLOCKING_SEVERITIES, stepAgentIds, EXEMPT_REQUEST_KEY } from "./types.ts"
9
+ import type { LoadedWorkflow } from "./loader.ts"
10
+ import { tagKey } from "./types.ts"
11
+ import { reviewLayerFromMetadata, readIssueSource } from "../constants.ts"
12
+
13
+ const TERMINAL_PHASES: WorkItemPhase[] = ["done", "cancelled"]
14
+
15
+ /** review 三个验证 step → 其可裁定的 issue 报源层(层感知门禁归因依据)。 */
16
+ export const REVIEW_STEP_TO_LAYER: Record<string, "tool" | "task" | "quality"> = {
17
+ verify_tool: "tool",
18
+ verify_task: "task",
19
+ verify_quality: "quality",
20
+ }
21
+
22
+ /** 读取 issue child 的报源层(source 反推,source_phase 仅作历史兜底,缺省 tool)。 */
23
+ function childSourcePhase(child: WorkItem): "tool" | "task" | "quality" {
24
+ return reviewLayerFromMetadata(child)
25
+ }
26
+
27
+ export function isTerminalPhase(phase: WorkItemPhase): boolean {
28
+ return phase === "done" || phase === "cancelled"
29
+ }
30
+
31
+ export function isBlockingSeverity(severity: string | undefined): boolean {
32
+ return severity !== undefined && (BLOCKING_SEVERITIES as readonly string[]).includes(severity)
33
+ }
34
+
35
+ export function isInfoSeverity(severity: string | undefined): boolean {
36
+ return severity === "Info"
37
+ }
38
+
39
+ export function readInternalCount(item: WorkItem, key: string): number {
40
+ const v = item.metadata[key]
41
+ return typeof v === "number" ? v : 0
42
+ }
43
+
44
+ export function setInternalCount(item: WorkItem, key: string, value: number): void {
45
+ item.metadata[key] = value
46
+ }
47
+
48
+ export function createInitialWorkItem(input: {
49
+ id: string
50
+ source: string
51
+ externalId?: string
52
+ type: "task" | "issue"
53
+ title: string
54
+ description: string
55
+ labels?: string[]
56
+ severity?: WorkItem["severity"]
57
+ }): WorkItem {
58
+ return {
59
+ id: input.id,
60
+ source: input.source,
61
+ externalId: input.externalId,
62
+ type: input.type,
63
+ title: input.title,
64
+ description: input.description,
65
+ phase: "todo",
66
+ suspended: false,
67
+ currentStep: null,
68
+ tags: {},
69
+ metadata: {},
70
+ children: [],
71
+ labels: input.labels ?? [],
72
+ severity: input.severity,
73
+ }
74
+ }
75
+
76
+ export function effectiveMaxRetries(workflow: LoadedWorkflow, step: StepConfig): number {
77
+ return step.max_retries ?? workflow.max_retries
78
+ }
79
+
80
+ export function resolveCurrentStep(
81
+ item: WorkItem,
82
+ workflow: LoadedWorkflow,
83
+ ): { step: StepConfig; phaseName: WorkItemPhase } | null {
84
+ if (item.currentStep) {
85
+ const entry = workflow.stepMap.get(item.currentStep)
86
+ if (entry) return { step: entry.step, phaseName: entry.phase.name }
87
+ return null
88
+ }
89
+ for (const phase of workflow.phases) {
90
+ if (phase.name === item.phase && phase.steps.length > 0) {
91
+ return { step: phase.steps[0], phaseName: phase.name }
92
+ }
93
+ }
94
+ return null
95
+ }
96
+
97
+ /**
98
+ * phase↔step 归属一致性判定:currentStep 非空时,其配置归属 phase 与 item.phase 错位视为状态异常。
99
+ * - currentStep 为 null(todo 初始态 / 终态)恒为 false,不误报;
100
+ * - currentStep 指向未声明 step(resolveCurrentStep 返 null)视为 true(安全侧,宁拒勿放)。
101
+ * 正常一致态(各写入点原子一致)恒返回 false。
102
+ */
103
+ export function phaseStepMismatch(item: WorkItem, workflow: LoadedWorkflow): boolean {
104
+ if (item.currentStep === null) return false
105
+ const current = resolveCurrentStep(item, workflow)
106
+ if (!current) return true
107
+ return current.phaseName !== item.phase
108
+ }
109
+
110
+ export function getStepVerdict(item: WorkItem, stepId: string, agentKey: string): Verdict {
111
+ return item.tags[tagKey(stepId, agentKey)] ?? "pending"
112
+ }
113
+
114
+ /** 存在在途豁免申请(issue 带 exempt_request 标记且未终态)的报源 agent 集合。
115
+ * 豁免申请由报源 reviewer 裁定(谁提谁裁定):维度名下有未裁定豁免申请时必须被唤起履行裁定权(D1),
116
+ * 且该维度不得被「issue 全终态 → 翻 passed」自愈误翻(D2 前提排除)。
117
+ * 终态 issue 上的残留标记视为已裁定(adjudicateExempt 裁定即删标记),不计入在途。 */
118
+ export function agentsWithPendingExempt(item: WorkItem): string[] {
119
+ const agents = new Set<string>()
120
+ for (const child of item.children) {
121
+ if (child.metadata[EXEMPT_REQUEST_KEY] === undefined) continue
122
+ if (isTerminalPhase(child.phase)) continue
123
+ const source = readIssueSource(child)
124
+ if (source) agents.add(source)
125
+ }
126
+ return [...agents]
127
+ }
128
+
129
+ /** 限定到 step.agents 的在途豁免申请报源(recommendAgents 多 agent 分支唤起用)。 */
130
+ export function stepAgentsWithPendingExempt(item: WorkItem, step: StepConfig): string[] {
131
+ const stepAgentSet = new Set(stepAgentIds(step))
132
+ return agentsWithPendingExempt(item).filter((a) => stepAgentSet.has(a))
133
+ }
134
+
135
+ /** D2 维度翻盘前提:agent 名下存在 blocking 级 issue 且全部终态(done/cancelled)、无在途豁免申请。
136
+ * 不自动取消/裁定 issue(谁提谁裁定不变),仅收敛维度结论。报源维度归因按 source 反推
137
+ * (谁提谁裁定:维度 verdict 只反映其自身报源 issue 的状态)。
138
+ * 要求「至少一个 blocking issue」而非空集全称:failed 维度名下必有其报的 blocking issue
139
+ * (verify_quality failed 提交由 assertFailedHasReason 强制理由),空集是合成/异常态,
140
+ * 保持 failed 交由聚合自愈重派收敛,不做翻盘。 */
141
+ function agentBlockingResolved(item: WorkItem, agent: string): boolean {
142
+ if (agentsWithPendingExempt(item).includes(agent)) return false
143
+ const owned = item.children.filter(
144
+ (c) => c.type === "issue" && readIssueSource(c) === agent && isBlockingSeverity(c.severity),
145
+ )
146
+ if (owned.length === 0) return false
147
+ return owned.every((c) => isTerminalPhase(c.phase))
148
+ }
149
+
150
+ export function adjudicateStep(item: WorkItem, step: StepConfig): StepAdjudication {
151
+ const verdicts = step.agents.map((agent) => {
152
+ const v = getStepVerdict(item, step.id, agent.id)
153
+ // D2:仅多 agent step(verify_quality 5 维并行)做维度翻盘——维度 failed 但名下 blocking issue
154
+ // 全部终态且无在途豁免申请时视为 passed,打破「failed 且无待办」的不可收敛死状态。
155
+ // 单 agent step 不翻盘:failed 维度由 recommendAgents 单 agent 分支重派重提收敛,翻盘会使
156
+ // 已提交的 step 呈现 passed 却无后续提交触发推进,造成新的静默死锁。
157
+ if (v === "failed" && step.agents.length > 1 && agentBlockingResolved(item, agent.id)) return "passed"
158
+ return v
159
+ })
160
+ if (verdicts.length > 0 && verdicts.every((v) => v === "passed")) return "passed"
161
+ if (verdicts.some((v) => v === "failed")) return "failed"
162
+ return "pending"
163
+ }
164
+
165
+ export function recommendAgents(item: WorkItem, step: StepConfig): string[] {
166
+ if (step.always_run) return stepAgentIds(step)
167
+ // 多 agent step(verify_quality 5 维并行):
168
+ // - 存在 pending → 聚合等待期仅重派 pending(已 failed 维度不重复分派,避免单维失败过早重派);
169
+ // D1:维度名下有在途豁免申请(报源 reviewer 尚未裁定)时必须一并唤起——「谁提谁裁定」要求
170
+ // 报源 reviewer 履行裁定权,否则豁免申请滞留、维度恒 failed 且永不重派(聚合等待期死锁)。
171
+ // - 无任何 pending → 引擎级自愈:回退返回 failed 维度(failed tag 残留如历史 state
172
+ // 归因无法反推到对应维导致的重派,静默死锁转为可见循环并被既有机制收敛;语义与 main 分支非 passed 全重派收敛)。
173
+ // 该分支已覆盖 failed 维(含带在途豁免申请的 failed 维),无需再叠加 D1。
174
+ if (step.agents.length > 1) {
175
+ const pending = step.agents.filter((agent) => getStepVerdict(item, step.id, agent.id) === "pending")
176
+ if (pending.length > 0) {
177
+ const exemptPending = stepAgentsWithPendingExempt(item, step)
178
+ const dispatch = new Set(pending.map((a) => a.id))
179
+ for (const id of exemptPending) dispatch.add(id)
180
+ return [...dispatch]
181
+ }
182
+ return step.agents.filter((agent) => getStepVerdict(item, step.id, agent.id) === "failed").map((a) => a.id)
183
+ }
184
+ // 单 agent step:返回非 passed 的 agent(analyze 同 phase 回退不清 tag,必须返回 failed 的 architect 重审)。
185
+ return step.agents.filter((agent) => getStepVerdict(item, step.id, agent.id) !== "passed").map((a) => a.id)
186
+ }
187
+
188
+ export function applyAgentVerdict(item: WorkItem, stepId: string, agentKey: string, verdict: Verdict): void {
189
+ item.tags[tagKey(stepId, agentKey)] = verdict
190
+ }
191
+
192
+ export function clearStepTags(item: WorkItem, stepId: string): void {
193
+ for (const key of Object.keys(item.tags)) {
194
+ if (key.startsWith(`${stepId}:`)) delete item.tags[key]
195
+ }
196
+ }
197
+
198
+ export function stepCanPass(item: WorkItem, step: StepConfig): boolean {
199
+ if (adjudicateStep(item, step) !== "passed") return false
200
+ const blockers = item.children.filter((child) => isBlockingSeverity(child.severity))
201
+ if (step.id === "implement") {
202
+ // implement 门禁 carve-out:review 态 blocking issue 已由 dev 提交进入待复核(等待对应 reviewer 裁定),
203
+ // 不阻塞 dev 提交推进(防 dev 死锁在 implement);其余 blocking issue 仍须终态。
204
+ return blockers.every((child) => child.phase === "review" || isTerminalPhase(child.phase))
205
+ }
206
+ const layer = REVIEW_STEP_TO_LAYER[step.id]
207
+ if (layer) {
208
+ // 层感知门禁 carve-out:verify_tool/verify_task/verify_quality 仅要求本层可裁定的 blocking issue 为终态
209
+ // (防止末位提交时本层未复核 issue 假收尾),其他层 review 态 issue 不阻塞本 step。
210
+ return blockers.every((child) => childSourcePhase(child) !== layer || isTerminalPhase(child.phase))
211
+ }
212
+ return blockers.every((child) => isTerminalPhase(child.phase))
213
+ }
214
+
215
+ export function phaseStepsAllPassed(item: WorkItem, workflow: LoadedWorkflow, phaseName: WorkItemPhase): boolean {
216
+ const phase = workflow.phases.find((p) => p.name === phaseName)
217
+ if (!phase || phase.steps.length === 0) return false
218
+ return phase.steps.every((step) => adjudicateStep(item, step) === "passed")
219
+ }
220
+
221
+ export function childReachedPhase(child: WorkItem, targetPhase: WorkItemPhase): boolean {
222
+ if (isTerminalPhase(child.phase)) return true
223
+ const targetIdx = WORK_ITEM_PHASES.indexOf(targetPhase)
224
+ const childIdx = WORK_ITEM_PHASES.indexOf(child.phase)
225
+ return childIdx >= targetIdx
226
+ }
227
+
228
+ export function forwardGatePassed(item: WorkItem, workflow: LoadedWorkflow, targetPhase: WorkItemPhase): boolean {
229
+ if (!phaseStepsAllPassed(item, workflow, item.phase)) return false
230
+ const issuesOk = item.children
231
+ .filter((child) => isBlockingSeverity(child.severity))
232
+ .every((child) => childReachedPhase(child, targetPhase))
233
+ if (!issuesOk) return false
234
+ // task children(子任务)仅当跨入 review 及之后(targetPhase index >= 2)才纳入检查:
235
+ // analyze(todo)→implement(in_progress) 跨 phase 时 task child 为 todo 态,纳入会全流程死锁。
236
+ if (phaseOrderIndex(targetPhase) >= 2) {
237
+ const tasksOk = item.children
238
+ .filter((child) => child.type === "task")
239
+ .every((child) => childReachedPhase(child, targetPhase))
240
+ if (!tasksOk) return false
241
+ }
242
+ return true
243
+ }
244
+
245
+ export function rollbackChildren(item: WorkItem): void {
246
+ for (const child of item.children) {
247
+ if (isTerminalPhase(child.phase)) continue
248
+ if (child.phase === "review") continue
249
+ child.phase = "todo"
250
+ child.currentStep = null
251
+ }
252
+ }
253
+
254
+ export function hasUnresolvedChildren(item: WorkItem): boolean {
255
+ return item.children.some((child) => {
256
+ // Info 级 issue 不阻塞任何门禁(stepCanPass/forwardGatePassed/complete 均按 blocking 过滤),
257
+ // 常驻不应触发检查点:仅 task child 与 Low 及以上级别 issue 参与判定。
258
+ if (child.type === "issue" && isInfoSeverity(child.severity)) return false
259
+ return !isTerminalPhase(child.phase)
260
+ })
261
+ }
262
+
263
+ export function checkpointTriggered(item: WorkItem, workflow: LoadedWorkflow, step: StepConfig): boolean {
264
+ // A1 窄短路:step 已全部通过(adjudicateStep 返回 passed,含 D2 维度翻盘)时跳过检查点判定,
265
+ // 避免已通过的 step 在重试边界被打断;always_run 的 step 不享受短路(口径与 recommendForItem 一致)。
266
+ if (adjudicateStep(item, step) === "passed" && !step.always_run) return false
267
+ const retryCount = readInternalCount(item, "_retryCount")
268
+ if (retryCount <= 0) return false
269
+ if (retryCount % effectiveMaxRetries(workflow, step) !== 0) return false
270
+ return hasUnresolvedChildren(item)
271
+ }
272
+
273
+ export function applyCheckpointContinue(item: WorkItem, step: StepConfig): void {
274
+ clearStepTags(item, step.id)
275
+ item.metadata["_checkpoint"] = false
276
+ // 重置内部重试计数:checkpointTriggered 按 _retryCount 实时计算,continue 后若不重置,
277
+ // 残留计数 + 未终态 children 会让检查点视图恒成立,编排无法获得下一步分派指令(死锁)。
278
+ resetInternalRetryCount(item)
279
+ }
280
+
281
+ export function applyCheckpointGiveup(item: WorkItem, step: StepConfig): void {
282
+ for (const child of item.children) {
283
+ if (!isTerminalPhase(child.phase)) child.phase = "cancelled"
284
+ }
285
+ for (const agent of step.agents) {
286
+ applyAgentVerdict(item, step.id, agent.id, "passed")
287
+ }
288
+ item.metadata["_checkpoint"] = false
289
+ // 与 continue 对称重置:跨 step 差异化 max_retries 时,防止上一 step 残留计数污染下一 step 检查点判定。
290
+ resetInternalRetryCount(item)
291
+ }
292
+
293
+ export function incrementRetry(item: WorkItem): void {
294
+ setInternalCount(item, "_retryCount", readInternalCount(item, "_retryCount") + 1)
295
+ }
296
+
297
+ /** 归零内部重试计数:checkpoint 决策与 recovery 恢复后须清空残留计数,防止检查点判定死锁或跨 step 污染。 */
298
+ export function resetInternalRetryCount(item: WorkItem): void {
299
+ setInternalCount(item, "_retryCount", 0)
300
+ }
301
+
302
+ export function suspendItem(item: WorkItem, reason: string): void {
303
+ item.suspended = true
304
+ item.metadata["suspend_reason"] = reason
305
+ }
306
+
307
+ export type TransitionDirection = "pass" | "fail"
308
+
309
+ export interface TransitionResult {
310
+ advanced: boolean
311
+ reason?: string
312
+ target?: "done" | "halt" | string
313
+ }
314
+
315
+ function resolveStepInPhase(workflow: LoadedWorkflow, stepId: string): { step: StepConfig; phaseName: WorkItemPhase } | null {
316
+ const entry = workflow.stepMap.get(stepId)
317
+ if (!entry) return null
318
+ return { step: entry.step, phaseName: entry.phase.name }
319
+ }
320
+
321
+ function phaseOrderIndex(phase: WorkItemPhase): number {
322
+ return WORK_ITEM_PHASES.indexOf(phase)
323
+ }
324
+
325
+ /**
326
+ * 沿当前 step 的 transition 推进状态机。
327
+ * - done: item 进入终态(phase=done)并触发写回
328
+ * - halt: item 置为 suspended=true 保留当前列
329
+ * - step id:
330
+ * - 目标与当前同 phase:仅切换 currentStep
331
+ * - 目标跨 phase:正向需 phase 门禁(当前 phase 全部 step passed 且 Low+ children 达到目标 phase),反向直接迁移
332
+ */
333
+ export function applyTransition(
334
+ item: WorkItem,
335
+ workflow: LoadedWorkflow,
336
+ direction: TransitionDirection,
337
+ ): TransitionResult {
338
+ const current = resolveCurrentStep(item, workflow)
339
+ if (!current) {
340
+ return { advanced: false, reason: `WorkItem phase="${item.phase}" 在 workflow 中无对应 step 容器。` }
341
+ }
342
+
343
+ const target = direction === "pass" ? current.step.transitions.on_pass : current.step.transitions.on_fail
344
+
345
+ if (target === "done") {
346
+ // done 转移不走 forwardGatePassed(短路),置 done 前须显式检查 task children 全部终态
347
+ // (未完成子任务不得收尾;issue blocking 检查已在 stepCanPass 处理)。
348
+ const unfinishedTasks = item.children.filter((child) => child.type === "task" && !isTerminalPhase(child.phase))
349
+ if (unfinishedTasks.length > 0) {
350
+ return { advanced: false, reason: "存在未完成的子任务(task children 未达终态),无法进入 done。" }
351
+ }
352
+ item.phase = "done"
353
+ item.currentStep = null
354
+ return { advanced: true, target: "done" }
355
+ }
356
+ if (target === "halt") {
357
+ suspendItem(item, "halt")
358
+ return { advanced: true, target: "halt" }
359
+ }
360
+
361
+ const resolved = resolveStepInPhase(workflow, target)
362
+ if (!resolved) {
363
+ return { advanced: false, reason: `transition 目标 "${target}" 无法解析。` }
364
+ }
365
+
366
+ if (resolved.phaseName === item.phase) {
367
+ item.currentStep = resolved.step.id
368
+ return { advanced: true, target: target }
369
+ }
370
+
371
+ if (direction === "pass") {
372
+ if (!forwardGatePassed(item, workflow, resolved.phaseName)) {
373
+ return {
374
+ advanced: false,
375
+ reason: `跨 phase 正向推进被门禁拦截:当前 phase "${item.phase}" 未全部 step passed 或存在 Low+ children 未达到目标 phase "${resolved.phaseName}"。`,
376
+ }
377
+ }
378
+ item.phase = resolved.phaseName
379
+ item.currentStep = resolved.step.id
380
+ return { advanced: true, target: target }
381
+ }
382
+
383
+ rollbackChildren(item)
384
+ incrementRetry(item)
385
+ // 回退目标 step 的裁决 tags 一并重置:残留 passed/failed 会让 recommendForItem 误判
386
+ // 该 step 已裁决而不重新分派(review failed 回退 implement 时 developer passed 残留 → blocked 死锁)。
387
+ clearStepTags(item, resolved.step.id)
388
+ item.phase = resolved.phaseName
389
+ item.currentStep = resolved.step.id
390
+ return { advanced: true, target: target }
391
+ }
392
+
393
+ export function isForwardTransition(item: WorkItem, workflow: LoadedWorkflow, direction: TransitionDirection): boolean {
394
+ const current = resolveCurrentStep(item, workflow)
395
+ if (!current) return false
396
+ const target = direction === "pass" ? current.step.transitions.on_pass : current.step.transitions.on_fail
397
+ if (target === "done" || target === "halt") return false
398
+ const resolved = resolveStepInPhase(workflow, target)
399
+ if (!resolved) return false
400
+ return phaseOrderIndex(resolved.phaseName) > phaseOrderIndex(item.phase)
401
+ }
402
+
403
+ /**
404
+ * 评估当前 step 沿 on:pass 正向推进是否会被 applyTransition 拦截(只读,不修改 item)。
405
+ * 与 applyTransition(pass) 的门禁口径一致:
406
+ * - done 目标:task children 须全部终态(否则 applyTransition 拦截);
407
+ * - 跨 phase 正向目标:forwardGatePassed 须通过(否则 applyTransition 拦截)。
408
+ * 返回拦截原因(供 blocked 视图展示),无拦截返回 undefined。
409
+ */
410
+ function forwardAdvanceBlockReason(item: WorkItem, workflow: LoadedWorkflow, step: StepConfig): string | undefined {
411
+ const target = step.transitions.on_pass
412
+ if (target === "done") {
413
+ const unfinishedTasks = item.children.filter((c) => c.type === "task" && !isTerminalPhase(c.phase))
414
+ if (unfinishedTasks.length > 0) {
415
+ return `存在 ${unfinishedTasks.length} 个未达终态的 task child(#${unfinishedTasks.map((c) => c.id).join("、")}),无法推进到 done。`
416
+ }
417
+ return undefined
418
+ }
419
+ if (target === "halt") return undefined
420
+ const resolved = resolveStepInPhase(workflow, target)
421
+ if (!resolved) return `transition 目标 "${target}" 无法解析。`
422
+ if (resolved.phaseName === item.phase) return undefined
423
+ if (!forwardGatePassed(item, workflow, resolved.phaseName)) {
424
+ const notReached = item.children.filter(
425
+ (c) => c.type === "task" && !childReachedPhase(c, resolved.phaseName)
426
+ )
427
+ if (notReached.length > 0) {
428
+ return `跨 phase 正向推进被门禁拦截:目标 phase "${resolved.phaseName}" 要求全部 task child 达 ${resolved.phaseName} 态,以下 task 未达标:#${notReached.map((c) => c.id).join("、")}。`
429
+ }
430
+ return `跨 phase 正向推进被门禁拦截:当前 phase "${item.phase}" 未全部 step passed 或存在 Low+ children 未达到目标 phase "${resolved.phaseName}"。`
431
+ }
432
+ return undefined
433
+ }
434
+
435
+ /**
436
+ * 当前 step 门禁相关阻塞 children(blocked 推导与视图诊断共用):
437
+ * - review 验证 step(verify_tool/verify_task/verify_quality):层感知口径——本层可裁定的非终态 blocking issue
438
+ * (childSourcePhase 与 REVIEW_STEP_TO_LAYER[step.id] 一致)。非本层 review 态 issue 不阻塞本 step,故不列出。
439
+ * - implement:镜像 stepCanPass 的 carve-out——review 态 blocking issue 已由 dev 提交进入待复核(等待对应
440
+ * reviewer 裁定),不阻塞 dev 提交推进,故不计入;其余非终态 blocking issue 列出。
441
+ * - 其他非 review step:全部非终态 blocking issue。
442
+ */
443
+ export function blockingStepChildren(item: WorkItem, step: StepConfig): WorkItem[] {
444
+ const layer = REVIEW_STEP_TO_LAYER[step.id]
445
+ return item.children.filter((child) => {
446
+ if (!isBlockingSeverity(child.severity)) return false
447
+ if (isTerminalPhase(child.phase)) return false
448
+ if (step.id === "implement" && child.phase === "review") return false
449
+ if (layer && childSourcePhase(child) !== layer) return false
450
+ return true
451
+ })
452
+ }
453
+
454
+ /**
455
+ * blocked step 应补交裁定的 reviewer 推导(谁提谁裁定):
456
+ * - 带 exempt_request 标记的 child → 报源 reviewer 补交 exempt_adjudications
457
+ * - review 态 child → 报源 reviewer 补交 recheck_adjudications
458
+ * - todo/in_progress 等其他态 child → 不派 reviewer(补交无法解决),不加入 agents
459
+ * 报源解析:child.metadata.source(完整 agent 名)必须属于当前 step agents,否则跳过该 child——
460
+ * 裁定逻辑(submit.resolveAdjudicatorStepForIssue)硬性要求报源合法,非法报源派发出去提交即被拒,
461
+ * 形成不闭合循环;报源缺失/非法一律不派 reviewer,落入空派由视图输出诊断清单引导 recovery。
462
+ */
463
+ export function blockedSupplementAgents(item: WorkItem, step: StepConfig): string[] {
464
+ const agents = new Set<string>()
465
+ const stepAgentSet = new Set(stepAgentIds(step))
466
+ for (const child of blockingStepChildren(item, step)) {
467
+ if (child.metadata[EXEMPT_REQUEST_KEY] === undefined && child.phase !== "review") continue
468
+ const source = readIssueSource(child)
469
+ if (!source || !stepAgentSet.has(source)) continue
470
+ agents.add(source)
471
+ }
472
+ return [...agents]
473
+ }
474
+
475
+ export interface EngineRecommendation {
476
+ status: "recommend" | "suspended" | "checkpoint" | "blocked" | "terminal"
477
+ stepId: string | null
478
+ agents: string[]
479
+ checkpoint?: { retryCount: number; effectiveMaxRetries: number }
480
+ blockedReason?: string
481
+ message?: string
482
+ }
483
+
484
+ /**
485
+ * 引擎调度推荐。只读模式:不修改 item,仅返回推荐。
486
+ * 上游(submit 工具)在调用后自行应用状态迁移。
487
+ */
488
+ export function recommendForItem(item: WorkItem, workflow: LoadedWorkflow): EngineRecommendation {
489
+ if (item.suspended) {
490
+ const reason = (item.metadata["suspend_reason"] as string) || "suspended"
491
+ return {
492
+ status: "suspended",
493
+ stepId: item.currentStep,
494
+ agents: [],
495
+ message: `WorkItem 已暂停(${reason}),跳过调度。`,
496
+ }
497
+ }
498
+
499
+ const current = resolveCurrentStep(item, workflow)
500
+ if (!current) {
501
+ return {
502
+ status: "blocked",
503
+ stepId: null,
504
+ agents: [],
505
+ blockedReason: `WorkItem phase="${item.phase}" 在 workflow 中无对应 step 容器。`,
506
+ }
507
+ }
508
+
509
+ if (checkpointTriggered(item, workflow, current.step)) {
510
+ return {
511
+ status: "checkpoint",
512
+ stepId: current.step.id,
513
+ agents: [],
514
+ checkpoint: {
515
+ retryCount: readInternalCount(item, "_retryCount"),
516
+ effectiveMaxRetries: effectiveMaxRetries(workflow, current.step),
517
+ },
518
+ }
519
+ }
520
+
521
+ const adjudication = adjudicateStep(item, current.step)
522
+ if (adjudication === "passed" && !current.step.always_run) {
523
+ if (!stepCanPass(item, current.step)) {
524
+ // 本层 blocking children 未终态(reviewer 漏带 recheck/豁免裁定导致):推导应补交裁定的报源 reviewer,
525
+ // 不再恒返回 agents=[](否则 orchestrator 收不到"下一步分派谁"的指令,形成静默死锁)。
526
+ // 仅 review step(verify_tool/verify_task/verify_quality)走 supplement 派生:implement 的
527
+ // stepCanPass carve-out 已把 review 态 child 排除在阻塞之外,其 blocked 恒为待修复/豁免申请中条目,
528
+ // 补交裁定无意义,保持 agents=[] 走诊断清单。
529
+ const blockingChildren = blockingStepChildren(item, current.step)
530
+ const recheckCount = blockingChildren.filter(
531
+ (c) => c.phase === "review" && c.metadata[EXEMPT_REQUEST_KEY] === undefined,
532
+ ).length
533
+ const exemptCount = blockingChildren.filter((c) => c.metadata[EXEMPT_REQUEST_KEY] !== undefined).length
534
+ const todoCount = blockingChildren.length - recheckCount - exemptCount
535
+ const isReviewStep = REVIEW_STEP_TO_LAYER[current.step.id] !== undefined
536
+ const agents = isReviewStep ? blockedSupplementAgents(item, current.step) : []
537
+ const blockedReason =
538
+ agents.length > 0
539
+ ? `step 全部 agent 已 passed 但存在 ${blockingChildren.length} 个本层待复核/待裁定 blocking children(待复核 ${recheckCount}、豁免申请中 ${exemptCount}),需报源 reviewer 补交复核/裁定解除阻塞。`
540
+ : todoCount > 0
541
+ ? `step 全部 agent 已 passed 但存在 ${blockingChildren.length} 个未到终态的 blocking children(待修复 ${todoCount}、豁免申请中 ${exemptCount}),需 developer 修复后重新提交审查。`
542
+ : `step 全部 agent 已 passed 但存在 ${blockingChildren.length} 个待复核/豁免申请中的 blocking children 且无法推导需补交的 reviewer,请核对 state 后按需调用 opx_orch_init(recovery=...) 恢复。`
543
+ return {
544
+ status: "blocked",
545
+ stepId: current.step.id,
546
+ agents,
547
+ blockedReason,
548
+ }
549
+ }
550
+ // step 全 passed 且 children 达标后仍可能被跨 phase 门禁或 done 的 task children 终态检查拦截
551
+ // (如重复 task child / 未完成子任务)。显式返回 blocked + 原因而非 terminal,
552
+ // 避免"step 已通过"误导造成无人再触发迁移的静默死锁。
553
+ const gateBlock = forwardAdvanceBlockReason(item, workflow, current.step)
554
+ if (gateBlock) {
555
+ return {
556
+ status: "blocked",
557
+ stepId: current.step.id,
558
+ agents: [],
559
+ blockedReason: gateBlock,
560
+ }
561
+ }
562
+ return {
563
+ status: "terminal",
564
+ stepId: current.step.id,
565
+ agents: [],
566
+ message: "step 已通过,沿 transitions.on_pass 推进(由 submit 工具执行状态迁移)。",
567
+ }
568
+ }
569
+
570
+ const agents = recommendAgents(item, current.step)
571
+ return {
572
+ status: "recommend",
573
+ stepId: current.step.id,
574
+ agents,
575
+ }
576
+ }
@@ -0,0 +1,56 @@
1
+ // types
2
+ export {
3
+ WORK_ITEM_PHASES, WORK_ITEM_TYPES, VERDICTS, SEVERITY_LEVELS, BLOCKING_SEVERITIES,
4
+ INTERNAL_METADATA_PREFIX, tagKey, isInternalMetadataKey, stepAgentIds,
5
+ } from "./types.ts"
6
+ export type {
7
+ WorkItemPhase, WorkItemType, Verdict, Severity,
8
+ WorkItem, WorkItemWriteback, StepConfig, StepAgent, PhaseConfig, StepTransitions,
9
+ WorkflowConfig, StepAdjudication,
10
+ } from "./types.ts"
11
+
12
+ // loader
13
+ export { loadWorkflow, findStepByWorkItemPhase } from "./loader.ts"
14
+ export type { LoadedWorkflow } from "./loader.ts"
15
+
16
+ // engine
17
+ export {
18
+ createInitialWorkItem, isTerminalPhase, isBlockingSeverity, isInfoSeverity,
19
+ effectiveMaxRetries, getStepVerdict, adjudicateStep, recommendAgents,
20
+ applyAgentVerdict, clearStepTags, stepCanPass, phaseStepsAllPassed,
21
+ childReachedPhase, forwardGatePassed, rollbackChildren, hasUnresolvedChildren,
22
+ checkpointTriggered, applyCheckpointContinue, applyCheckpointGiveup,
23
+ incrementRetry, suspendItem, applyTransition, isForwardTransition,
24
+ recommendForItem,
25
+ } from "./engine.ts"
26
+ export type {
27
+ EngineRecommendation, TransitionResult, TransitionDirection,
28
+ } from "./engine.ts"
29
+
30
+ // submit
31
+ export { submitForStep, adjudicateExempt, adjudicateRecheck, assertSubmitRouting, EXEMPT_REQUEST_KEY } from "./submit.ts"
32
+ export type {
33
+ SubmitInput, SubmitResult, AdjudicationResult, RecheckAdjudicationResult,
34
+ } from "./submit.ts"
35
+
36
+ // reset(review 分层重置 + new_children 去重)
37
+ export {
38
+ resetReviewTagsOnFix, dedupeNewChildren, resolveChildIssueFields,
39
+ } from "./reset.ts"
40
+ export type { ResetReviewTagsInput } from "./reset.ts"
41
+
42
+ // status(工作流动态视图)
43
+ export { renderWorkflowStatusView } from "./status.ts"
44
+ export type { WorkflowStatusViewOptions } from "./status.ts"
45
+
46
+ // writeback
47
+ export { enqueueWriteback, flushWritebacks, retryPendingWritebacks, setWritebackHandler } from "./writeback.ts"
48
+ export type { WritebackHandler } from "./writeback.ts"
49
+
50
+ // collector
51
+ export { OpenSpecCollector, AdoCollector, registerCollector, getCollectors, __resetCollectors } from "./collector.ts"
52
+ export type { CollectorAdapter, OpenSpecCollectorOptions, OpenSpecChangeRef } from "./collector.ts"
53
+
54
+ // poller
55
+ export { pollOnce, pollAdapter, startPolling, __resetPoller } from "./poller.ts"
56
+ export type { PollOnceResult, PollAdapterResult } from "./poller.ts"