@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,331 @@
|
|
|
1
|
+
import type { WorkItem, WorkItemPhase, StepConfig, StepAdjudication } from "./types.ts"
|
|
2
|
+
import { stepAgentIds, EXEMPT_REQUEST_KEY } from "./types.ts"
|
|
3
|
+
import type { LoadedWorkflow } from "./loader.ts"
|
|
4
|
+
import { applyAgentVerdict, adjudicateStep, stepCanPass, applyTransition, getStepVerdict, isTerminalPhase, isInfoSeverity } from "./engine.ts"
|
|
5
|
+
import { issueChildrenOf, taskChildrenOf } from "../task-children.ts"
|
|
6
|
+
import { readIssueSource } from "../constants.ts"
|
|
7
|
+
|
|
8
|
+
/** 豁免申请标记键单一事实源在 types.ts(engine 与 submit 共用免循环引用);本模块 re-export 兼容既有消费方。 */
|
|
9
|
+
export { EXEMPT_REQUEST_KEY } from "./types.ts"
|
|
10
|
+
|
|
11
|
+
export interface SubmitInput {
|
|
12
|
+
stepId: string
|
|
13
|
+
agentKey: string
|
|
14
|
+
verdict: "passed" | "failed"
|
|
15
|
+
fixedIds?: string[]
|
|
16
|
+
exemptIds?: string[]
|
|
17
|
+
newChildren?: WorkItem[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SubmitResult {
|
|
21
|
+
stepId: string
|
|
22
|
+
agentKey: string
|
|
23
|
+
verdict: "passed" | "failed"
|
|
24
|
+
stepAdjudication: StepAdjudication
|
|
25
|
+
advanced: boolean
|
|
26
|
+
transitionTarget?: string
|
|
27
|
+
childrenUpdated: string[]
|
|
28
|
+
/** 推进失败(applyTransition 拦截)的原因;推进成功时不返回。 */
|
|
29
|
+
reason?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AdjudicationResult {
|
|
33
|
+
issueId: string
|
|
34
|
+
action: "dismissed" | "rejected"
|
|
35
|
+
childPhase: WorkItemPhase
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 报源 agent 属于哪个 review step 的 agents 就由谁裁定(固定逻辑,非可配置),未命中返回 null */
|
|
39
|
+
function resolveReviewStepForSource(workflow: LoadedWorkflow, source: string): StepConfig | null {
|
|
40
|
+
for (const phase of workflow.phases) {
|
|
41
|
+
if (phase.name !== "review") continue
|
|
42
|
+
for (const step of phase.steps) {
|
|
43
|
+
if (stepAgentIds(step).includes(source)) return step
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 校验 issue 裁定者权限(谁提谁裁定):裁定者恒为报 issue 的 agent(child.metadata.source),
|
|
51
|
+
* 供豁免裁定(adjudicateExempt)与复核(adjudicateRecheck)复用:
|
|
52
|
+
* - 报源缺失或不属于任何 review step 的 agents(非合法 review agent)→ 抛错(畸形数据拒绝,零副作用);
|
|
53
|
+
* - 裁定者 ≠ 报源 → 抛错(tool/task/quality 一律按报源收敛,无 step 白名单);
|
|
54
|
+
* 校验在一切状态写入之前执行,越权抛错不产生状态变更。返回报源对应 review step 供调用方使用。
|
|
55
|
+
*/
|
|
56
|
+
function resolveAdjudicatorStepForIssue(
|
|
57
|
+
workflow: LoadedWorkflow,
|
|
58
|
+
child: WorkItem,
|
|
59
|
+
agentKey: string,
|
|
60
|
+
role: "裁定" | "复核",
|
|
61
|
+
): StepConfig {
|
|
62
|
+
const prefix = role === "裁定" ? "豁免裁定失败" : "复核失败"
|
|
63
|
+
const source = readIssueSource(child)
|
|
64
|
+
const step = source ? resolveReviewStepForSource(workflow, source) : null
|
|
65
|
+
if (!step) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`${prefix}:issue "${child.id}" 的报源 "${source ?? "缺失"}" 不是合法 review agent(不属于任何 review step 的 agents),无法确定裁定者。`
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
if (agentKey !== source) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`${prefix}:issue "${child.id}" 由报源 "${source}" 裁定(谁提谁裁定),${role}者必须为 "${source}",拒绝 "${agentKey}" ${role}。`
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
return step
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 提交路由与归属校验(submitForStep 与 agentSubmitExecute 共用):
|
|
80
|
+
* step 声明存在、agent 属于 step.agents、提交 step 与当前 step 一致、step 归属 phase 与 item.phase 一致,
|
|
81
|
+
* 任一不满足即抛错且零状态变更。
|
|
82
|
+
*/
|
|
83
|
+
export function assertSubmitRouting(workflow: LoadedWorkflow, item: WorkItem, stepId: string, agentKey: string): StepConfig {
|
|
84
|
+
const entry = workflow.stepMap.get(stepId)
|
|
85
|
+
if (!entry) {
|
|
86
|
+
throw new Error(`submit 路由失败:step "${stepId}" 未在 workflow "${workflow.id}" 中声明。`)
|
|
87
|
+
}
|
|
88
|
+
const step = entry.step
|
|
89
|
+
if (!stepAgentIds(step).includes(agentKey)) {
|
|
90
|
+
throw new Error(`submit 越权:agent "${agentKey}" 不属于 step "${stepId}" 的 agents,拒绝提交且不产生任何状态变更。`)
|
|
91
|
+
}
|
|
92
|
+
if (item.currentStep !== null && item.currentStep !== stepId) {
|
|
93
|
+
throw new Error(`submit 校验失败:item "${item.id}" 当前 step 为 "${item.currentStep}",与提交的 step "${stepId}" 不一致,拒绝提交且不产生任何状态变更。`)
|
|
94
|
+
}
|
|
95
|
+
// phase↔step 归属校验:currentStep 为 null(todo 初始态 / 终态)显式跳过,错位态拒绝提交零状态变更。
|
|
96
|
+
if (item.currentStep !== null && entry.phase.name !== item.phase) {
|
|
97
|
+
throw new Error(`submit 校验失败:item "${item.id}" 的 phase "${item.phase}" 与 step "${stepId}" 归属阶段 "${entry.phase.name}" 不一致(phase ↔ step 错位),拒绝提交且不产生任何状态变更。`)
|
|
98
|
+
}
|
|
99
|
+
return step
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 链式推进:submitForStep 的 applyTransition(pass) 成功后,若新 step 已全 passed 且
|
|
104
|
+
* stepCanPass 满足,继续沿 on_pass 穿越(等价 main 的 task 自动跳过语义),
|
|
105
|
+
* 一次提交可带过多个已通过的 step。循环在每一步重算 adjudicateStep 与 stepCanPass,
|
|
106
|
+
* 停在需要 agent 动作的 step(pending/failed 裁决)或终态(done/halt)。
|
|
107
|
+
* 返回最终落点 target("done"/"halt"/step id),无推进则返回 initialTarget。
|
|
108
|
+
* 有限性:每轮推进都改变 currentStep(终态置 null)且 on_pass 构成无环 DAG,
|
|
109
|
+
* 另加 stepMap 规模上限防护,不可能无限循环。
|
|
110
|
+
*/
|
|
111
|
+
export function chainPassAdvance(item: WorkItem, workflow: LoadedWorkflow, initialTarget: string | undefined): string | undefined {
|
|
112
|
+
let target = initialTarget
|
|
113
|
+
const maxIterations = workflow.stepMap.size * 2 + 1
|
|
114
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
115
|
+
if (item.suspended || isTerminalPhase(item.phase) || item.currentStep === null) break
|
|
116
|
+
const entry = workflow.stepMap.get(item.currentStep)
|
|
117
|
+
if (!entry) break
|
|
118
|
+
const step = entry.step
|
|
119
|
+
if (step.always_run) break
|
|
120
|
+
if (adjudicateStep(item, step) !== "passed") break
|
|
121
|
+
if (!stepCanPass(item, step)) break
|
|
122
|
+
const r = applyTransition(item, workflow, "pass")
|
|
123
|
+
if (!r.advanced) break
|
|
124
|
+
if (r.target !== undefined) target = r.target
|
|
125
|
+
}
|
|
126
|
+
return target
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 通用提交入口:写 tag → children 更新 → 裁决与推进。
|
|
131
|
+
* 路由/归属校验在一切状态变更之前完成,越权提交不产生任何副作用。
|
|
132
|
+
*/
|
|
133
|
+
export function submitForStep(item: WorkItem, workflow: LoadedWorkflow, input: SubmitInput): SubmitResult {
|
|
134
|
+
const step = assertSubmitRouting(workflow, item, input.stepId, input.agentKey)
|
|
135
|
+
|
|
136
|
+
const childById = new Map<string, WorkItem>()
|
|
137
|
+
// issue children 优先入 map:fixed/exempt 解析须优先命中 issue child。
|
|
138
|
+
// task child 短数字 id("1")可能与 issue 的 externalId 撞车,且数组序不保证 issue 在前(reopen/recovery 重排)。
|
|
139
|
+
for (const c of issueChildrenOf(item)) {
|
|
140
|
+
childById.set(c.id, c)
|
|
141
|
+
// 兼容外部以原始 issue id 引用 child(child.id 可能带 issue: 前缀,externalId 为原始 id)
|
|
142
|
+
if (c.externalId) childById.set(c.externalId, c)
|
|
143
|
+
}
|
|
144
|
+
// task children 后入且撞车跳过:仅登记未被 issue child 占用的键,保证 issue 优先语义不被打乱
|
|
145
|
+
for (const c of taskChildrenOf(item)) {
|
|
146
|
+
if (!childById.has(c.id)) childById.set(c.id, c)
|
|
147
|
+
if (c.externalId && !childById.has(c.externalId)) childById.set(c.externalId, c)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 终态 issue 豁免守卫:done/cancelled 已终态的 issue 不允许重新申请豁免(防止已 cancelled 的 issue
|
|
151
|
+
// 被重复申请豁免、rejected 复活已豁免 issue)。校验在一切状态写入(applyAgentVerdict)之前,抛错零副作用。
|
|
152
|
+
// Info 级 issue 豁免守卫(纵深防御,与 agentSubmitExecute 入口校验同语义):Info 级不阻塞任何流程,
|
|
153
|
+
// 豁免无意义。本分支仅处理「新申请」(写入 exempt_request 标记),不触及存量带 exempt_request 标记的
|
|
154
|
+
// Info issue 裁定路径(adjudicateExempt 不受影响)。
|
|
155
|
+
for (const id of input.exemptIds ?? []) {
|
|
156
|
+
const child = childById.get(id)
|
|
157
|
+
if (child && isTerminalPhase(child.phase)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`豁免申请失败:issue "${child.id}" 当前 phase 为 "${child.phase}"(终态),不允许对已终态的 issue 重新申请豁免。`
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
if (child && isInfoSeverity(child.severity)) {
|
|
163
|
+
throw new Error(`豁免申请失败:issue "${child.id}" 为 Info 级 issue,不阻塞提交,无需申请豁免。`)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
applyAgentVerdict(item, input.stepId, input.agentKey, input.verdict)
|
|
168
|
+
|
|
169
|
+
const updated = new Set<string>()
|
|
170
|
+
|
|
171
|
+
for (const id of input.fixedIds ?? []) {
|
|
172
|
+
const child = childById.get(id)
|
|
173
|
+
if (child) {
|
|
174
|
+
// issue child 修复后进入 review(待对应 reviewer 复核),终态由复核裁定;
|
|
175
|
+
// task child 维持 done 语义(子任务完成态由 verify_task 验证路径处理,不进入复核)。
|
|
176
|
+
child.phase = child.type === "task" ? "done" : "review"
|
|
177
|
+
// 记录 canonical child.id(task child 短数字 id 可能与 issue externalId 撞车,
|
|
178
|
+
// 原始引用 id 渲染时歧义,统一用 child.id 回指)
|
|
179
|
+
updated.add(child.id)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const id of input.exemptIds ?? []) {
|
|
184
|
+
const child = childById.get(id)
|
|
185
|
+
if (child) {
|
|
186
|
+
child.metadata[EXEMPT_REQUEST_KEY] = { requestedBy: input.agentKey }
|
|
187
|
+
// 豁免申请进入 review(待裁定),与 fixed 的 fixed→review 对称:exempt 子项不再以 todo 态
|
|
188
|
+
// 滞留(todo 态被 implement 门禁计为阻塞,dev 无法推进、reviewer 又因 currentStep 停在
|
|
189
|
+
// implement 无法补交,形成死锁)。进入 review 后 implement 门禁 carve-out(review 态
|
|
190
|
+
// blocking 不阻塞)与 forwardGatePassed 天然放行,由报源 reviewer 在 review 轮次或补交中
|
|
191
|
+
// 裁定(dismissed→cancelled / rejected→todo 回退收敛)。task child 无豁免语义,保持原行为。
|
|
192
|
+
if (child.type === "issue") child.phase = "review"
|
|
193
|
+
updated.add(child.id)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const nc of input.newChildren ?? []) {
|
|
198
|
+
if (!childById.has(nc.id)) {
|
|
199
|
+
item.children.push(nc)
|
|
200
|
+
childById.set(nc.id, nc)
|
|
201
|
+
updated.add(nc.id)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const adjudication = adjudicateStep(item, step)
|
|
206
|
+
// 多 agent step(verify_quality 5 维并行)聚合判定:须全部 agent 非 pending 才允许触发迁移。
|
|
207
|
+
// passed 天然意味着全提交;failed 须等最后一维提交后才回退,避免单维失败过早回退阻断其余维度提交。
|
|
208
|
+
const allAdjudicated = step.agents.every((a) => getStepVerdict(item, step.id, a.id) !== "pending")
|
|
209
|
+
let advanced = false
|
|
210
|
+
let transitionTarget: string | undefined
|
|
211
|
+
let advanceBlockReason: string | undefined
|
|
212
|
+
if (adjudication === "passed" && stepCanPass(item, step)) {
|
|
213
|
+
const r = applyTransition(item, workflow, "pass")
|
|
214
|
+
advanced = r.advanced
|
|
215
|
+
if (r.advanced) {
|
|
216
|
+
// 成功推进后清理历史阻塞原因,避免状态视图展示过期信息
|
|
217
|
+
delete item.metadata["_advance_block_reason"]
|
|
218
|
+
// 链式推进:迁移后若新 step 已全 passed 且 stepCanPass 满足,继续沿 on_pass 穿越
|
|
219
|
+
// (等价 main 的 task 自动跳过语义),一次提交可带过多个已通过的 step,
|
|
220
|
+
// 停在需要 agent 动作的 step 或终态(done/halt)。
|
|
221
|
+
transitionTarget = chainPassAdvance(item, workflow, r.target)
|
|
222
|
+
} else {
|
|
223
|
+
// 推进被跨 phase 门禁/done 终态检查拦截:把原因写入 item 状态供 opx_status 展示,并随结果返回
|
|
224
|
+
advanceBlockReason = r.reason
|
|
225
|
+
item.metadata["_advance_block_reason"] = r.reason
|
|
226
|
+
}
|
|
227
|
+
} else if (adjudication === "failed" && allAdjudicated) {
|
|
228
|
+
const r = applyTransition(item, workflow, "fail")
|
|
229
|
+
advanced = r.advanced
|
|
230
|
+
if (r.advanced) {
|
|
231
|
+
delete item.metadata["_advance_block_reason"]
|
|
232
|
+
if (r.target !== undefined) transitionTarget = r.target
|
|
233
|
+
} else {
|
|
234
|
+
advanceBlockReason = r.reason
|
|
235
|
+
item.metadata["_advance_block_reason"] = r.reason
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
stepId: input.stepId,
|
|
241
|
+
agentKey: input.agentKey,
|
|
242
|
+
verdict: input.verdict,
|
|
243
|
+
stepAdjudication: adjudication,
|
|
244
|
+
advanced,
|
|
245
|
+
transitionTarget,
|
|
246
|
+
childrenUpdated: [...updated],
|
|
247
|
+
reason: advanceBlockReason,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 裁定豁免申请:dismissed → cancelled;rejected → todo;两种结果均清除 exempt_request 标记。
|
|
253
|
+
* 仅带 exempt_request 标记(已申请豁免)的 issue 可被裁定;裁定者必须属于对应 review step 的 agents。
|
|
254
|
+
*/
|
|
255
|
+
export function adjudicateExempt(
|
|
256
|
+
item: WorkItem,
|
|
257
|
+
workflow: LoadedWorkflow,
|
|
258
|
+
input: { issueId: string; agentKey: string; action: "dismissed" | "rejected" },
|
|
259
|
+
): AdjudicationResult {
|
|
260
|
+
const child =
|
|
261
|
+
issueChildrenOf(item).find((c) => c.id === input.issueId) ??
|
|
262
|
+
taskChildrenOf(item).find((c) => c.id === input.issueId)
|
|
263
|
+
if (!child) {
|
|
264
|
+
throw new Error(`豁免裁定失败:issue "${input.issueId}" 不存在于 item "${item.id}" 的 children 中。`)
|
|
265
|
+
}
|
|
266
|
+
if (child.metadata[EXEMPT_REQUEST_KEY] === undefined) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`豁免裁定失败:issue "${input.issueId}" 未申请豁免(无 exempt_request 标记),仅可裁定带豁免申请标记的 issue。`
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
// 谁提谁裁定:解析报源 review step 并校验裁定者权限(与复核同源,复用公共裁定权校验)。
|
|
272
|
+
resolveAdjudicatorStepForIssue(workflow, child, input.agentKey, "裁定")
|
|
273
|
+
|
|
274
|
+
if (input.action === "dismissed") {
|
|
275
|
+
child.phase = "cancelled"
|
|
276
|
+
} else {
|
|
277
|
+
child.phase = "todo"
|
|
278
|
+
}
|
|
279
|
+
delete child.metadata[EXEMPT_REQUEST_KEY]
|
|
280
|
+
return { issueId: input.issueId, action: input.action, childPhase: child.phase }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface RecheckAdjudicationResult {
|
|
284
|
+
issueId: string
|
|
285
|
+
verdict: "passed" | "rejected"
|
|
286
|
+
childPhase: WorkItemPhase
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 复核已修复(review 态)issue:verdict=passed → done;verdict=rejected → 回 todo 并
|
|
291
|
+
* 递增 metadata.refix_count、写入 metadata.reject_reason(给 dev 驳回原因)。
|
|
292
|
+
* 仅 review 态 issue 可复核;裁定者必须为报 issue 的对应 review step(谁提谁裁定,与豁免裁定同源归因)。
|
|
293
|
+
* 越权/非法状态抛错,不产生任何状态变更。
|
|
294
|
+
*/
|
|
295
|
+
export function adjudicateRecheck(
|
|
296
|
+
item: WorkItem,
|
|
297
|
+
workflow: LoadedWorkflow,
|
|
298
|
+
input: { issueId: string; agentKey: string; verdict: "passed" | "rejected"; rejectReason?: string },
|
|
299
|
+
): RecheckAdjudicationResult {
|
|
300
|
+
const child = issueChildrenOf(item).find((c) => c.id === input.issueId)
|
|
301
|
+
if (!child) {
|
|
302
|
+
throw new Error(`复核失败:issue "${input.issueId}" 不存在于 item "${item.id}" 的 issue children 中。`)
|
|
303
|
+
}
|
|
304
|
+
if (child.phase !== "review") {
|
|
305
|
+
throw new Error(
|
|
306
|
+
`复核失败:issue "${input.issueId}" 当前 phase 为 "${child.phase}",仅 review 态(已修复待复核)issue 可复核。`
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
// 带 exempt_request 标记的 issue 处于豁免待裁定态,须由报源 reviewer 经 exempt_adjudications 裁定,
|
|
310
|
+
// recheck 复核会绕过豁免裁定语义(passed 直接置 done 且残留标记);校验在一切状态写入之前,抛错零变更。
|
|
311
|
+
if (child.metadata[EXEMPT_REQUEST_KEY] !== undefined) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`复核失败:issue "${input.issueId}" 已申请豁免(exempt_request 标记),待裁定豁免申请应经 exempt_adjudications 裁定,不接受 recheck_adjudications 复核。`
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
// 谁提谁裁定:解析报源 review step 并校验复核者权限(与豁免裁定同源,复用公共裁定权校验)。
|
|
317
|
+
resolveAdjudicatorStepForIssue(workflow, child, input.agentKey, "复核")
|
|
318
|
+
|
|
319
|
+
if (input.verdict === "passed") {
|
|
320
|
+
child.phase = "done"
|
|
321
|
+
delete child.metadata["reject_reason"]
|
|
322
|
+
} else {
|
|
323
|
+
if (!input.rejectReason) {
|
|
324
|
+
throw new Error(`复核失败:issue "${input.issueId}" 驳回(verdict=rejected)必须提供 reject_reason。`)
|
|
325
|
+
}
|
|
326
|
+
child.phase = "todo"
|
|
327
|
+
child.metadata["refix_count"] = (typeof child.metadata["refix_count"] === "number" ? child.metadata["refix_count"] : 0) + 1
|
|
328
|
+
child.metadata["reject_reason"] = input.rejectReason
|
|
329
|
+
}
|
|
330
|
+
return { issueId: input.issueId, verdict: input.verdict, childPhase: child.phase }
|
|
331
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { SEVERITY_LEVELS, BLOCKING_SEVERITIES } from "../constants.ts"
|
|
2
|
+
export { SEVERITY_LEVELS, BLOCKING_SEVERITIES }
|
|
3
|
+
|
|
4
|
+
export const WORK_ITEM_PHASES = ["todo", "in_progress", "review", "done", "cancelled"] as const
|
|
5
|
+
export type WorkItemPhase = typeof WORK_ITEM_PHASES[number]
|
|
6
|
+
|
|
7
|
+
export const WORK_ITEM_TYPES = ["task", "issue"] as const
|
|
8
|
+
export type WorkItemType = typeof WORK_ITEM_TYPES[number]
|
|
9
|
+
|
|
10
|
+
export const VERDICTS = ["pending", "passed", "failed"] as const
|
|
11
|
+
export type Verdict = typeof VERDICTS[number]
|
|
12
|
+
|
|
13
|
+
export type Severity = typeof SEVERITY_LEVELS[number]
|
|
14
|
+
|
|
15
|
+
export interface WorkItemWriteback {
|
|
16
|
+
lastAttempt?: string
|
|
17
|
+
lastSuccess?: string
|
|
18
|
+
error?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WorkItem {
|
|
22
|
+
id: string
|
|
23
|
+
source: string
|
|
24
|
+
externalId?: string
|
|
25
|
+
type: WorkItemType
|
|
26
|
+
title: string
|
|
27
|
+
description: string
|
|
28
|
+
phase: WorkItemPhase
|
|
29
|
+
suspended: boolean
|
|
30
|
+
currentStep: string | null
|
|
31
|
+
tags: Record<string, Verdict>
|
|
32
|
+
metadata: Record<string, unknown>
|
|
33
|
+
children: WorkItem[]
|
|
34
|
+
labels: string[]
|
|
35
|
+
severity?: Severity
|
|
36
|
+
writeback?: WorkItemWriteback
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const INTERNAL_METADATA_PREFIX = "_"
|
|
40
|
+
|
|
41
|
+
/** 豁免申请标记键(非内部下划线前缀,属于业务语义字段)。定义在本模块使 engine 与 submit 均可
|
|
42
|
+
* 免循环引用(submit 依赖 engine,engine 依赖本模块,不产生新环)。 */
|
|
43
|
+
export const EXEMPT_REQUEST_KEY = "exempt_request"
|
|
44
|
+
|
|
45
|
+
export function isInternalMetadataKey(key: string): boolean {
|
|
46
|
+
return key.startsWith(INTERNAL_METADATA_PREFIX)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function tagKey(stepId: string, agentKey: string): string {
|
|
50
|
+
return `${stepId}:${agentKey}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface StepTransitions {
|
|
54
|
+
on_pass: string | "done" | "halt"
|
|
55
|
+
on_fail: string | "done" | "halt"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface StepAgent {
|
|
59
|
+
id: string
|
|
60
|
+
capability_tags: string[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface StepConfig {
|
|
64
|
+
id: string
|
|
65
|
+
agents: StepAgent[]
|
|
66
|
+
always_run?: boolean
|
|
67
|
+
max_retries?: number
|
|
68
|
+
instructions?: string[]
|
|
69
|
+
constraints?: string[]
|
|
70
|
+
transitions: StepTransitions
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** step.agents 的 id 投影:状态机/视图消费点统一从对象数组归一化为 id 字符串数组。 */
|
|
74
|
+
export function stepAgentIds(step: { agents: StepAgent[] }): string[] {
|
|
75
|
+
return step.agents.map((a) => a.id)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PhaseConfig {
|
|
79
|
+
name: WorkItemPhase
|
|
80
|
+
steps: StepConfig[]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 跨所有 step 共享的通用指引/约束(step 自动继承,渲染时合并进对应视图区块),缺省 undefined。 */
|
|
84
|
+
export interface WorkflowCommon {
|
|
85
|
+
/** 通用操作指引(所有 step 的操作指引均前置追加)。 */
|
|
86
|
+
instructions?: string[]
|
|
87
|
+
/** 通用约束(所有 step 的约束区块均前置追加)。 */
|
|
88
|
+
constraints?: string[]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface WorkflowConfig {
|
|
92
|
+
id: string
|
|
93
|
+
name?: string
|
|
94
|
+
max_retries: number
|
|
95
|
+
phases: PhaseConfig[]
|
|
96
|
+
/** 跨 step 共享的通用语义(step 自动继承),缺省 undefined。 */
|
|
97
|
+
common?: WorkflowCommon
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type StepAdjudication = "passed" | "failed" | "pending"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { WorkItem } from "./types.ts"
|
|
2
|
+
|
|
3
|
+
export type WritebackHandler = (item: WorkItem, action: string, payload: unknown) => Promise<void> | void
|
|
4
|
+
|
|
5
|
+
interface WritebackTask {
|
|
6
|
+
item: WorkItem
|
|
7
|
+
action: string
|
|
8
|
+
payload: unknown
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let handler: WritebackHandler = async () => {}
|
|
12
|
+
let queue: WritebackTask[] = []
|
|
13
|
+
|
|
14
|
+
/** 注册写回处理器(模拟外部源,默认直接成功) */
|
|
15
|
+
export function setWritebackHandler(next: WritebackHandler): void {
|
|
16
|
+
handler = next
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 非阻塞入队:仅推进队列并记录 lastAttempt,不等待外部 I/O */
|
|
20
|
+
export function enqueueWriteback(item: WorkItem, action: string, payload: unknown): void {
|
|
21
|
+
queue.push({ item, action, payload })
|
|
22
|
+
item.writeback = { ...item.writeback, lastAttempt: new Date().toISOString() }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 排空队列执行写回:成功记录 lastSuccess,失败记录 error(保留 lastAttempt) */
|
|
26
|
+
export async function flushWritebacks(): Promise<{ succeeded: string[]; failed: string[] }> {
|
|
27
|
+
const succeeded: string[] = []
|
|
28
|
+
const failed: string[] = []
|
|
29
|
+
const pending = queue
|
|
30
|
+
queue = []
|
|
31
|
+
for (const task of pending) {
|
|
32
|
+
try {
|
|
33
|
+
await handler(task.item, task.action, task.payload)
|
|
34
|
+
const { error: _staleError, ...writeback } = task.item.writeback ?? {}
|
|
35
|
+
task.item.writeback = { ...writeback, lastSuccess: new Date().toISOString() }
|
|
36
|
+
succeeded.push(task.item.id)
|
|
37
|
+
} catch (err) {
|
|
38
|
+
task.item.writeback = { ...task.item.writeback, error: (err as Error).message }
|
|
39
|
+
failed.push(task.item.id)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { succeeded, failed }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 上次写回失败则返回 true,提示下次状态变更应重试(重试动作由后续 flush 承担) */
|
|
46
|
+
export function retryPendingWritebacks(item: WorkItem): boolean {
|
|
47
|
+
return item.writeback?.error !== undefined
|
|
48
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url"
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
let _html: string | null = null
|
|
6
|
+
|
|
7
|
+
/** 页面资源定位:bundle 形态(ZCode 插件包)资源随 bundle 同目录放置;源码/包形态位于 ../../assets/dashboard/。 */
|
|
8
|
+
function resolveDashboardHtml(): string {
|
|
9
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url))
|
|
10
|
+
const bundled = join(__dirname, "dashboard", "index.html")
|
|
11
|
+
if (existsSync(bundled)) return bundled
|
|
12
|
+
return join(__dirname, "../../assets/dashboard/index.html")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getDashboardPage(): string {
|
|
16
|
+
if (_html) return _html
|
|
17
|
+
_html = readFileSync(resolveDashboardHtml(), "utf-8")
|
|
18
|
+
return _html
|
|
19
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs"
|
|
2
|
+
import { resolve } from "node:path"
|
|
3
|
+
import { SKILL_SCAN_ROOTS } from "./scan.ts"
|
|
4
|
+
|
|
5
|
+
export interface SkillTagIndex {
|
|
6
|
+
tagMap: Map<string, string[]>
|
|
7
|
+
skillTags: Map<string, string[]>
|
|
8
|
+
/** skill 名 → frontmatter 声明的机器可读必做清单(must_do),未声明则无键。 */
|
|
9
|
+
skillMustDo: Map<string, string[]>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let cacheKey: string | null = null
|
|
13
|
+
let cacheIndex: SkillTagIndex | null = null
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 扫描 skill 目录的 capabilities frontmatter,建立 tag → skill 名 与 skill 名 → tags 双向索引。
|
|
17
|
+
* 进程内缓存:roots 不变时返回同一实例,roots 变化时重建。
|
|
18
|
+
*/
|
|
19
|
+
export function scanSkillTags(roots: string[] = SKILL_SCAN_ROOTS): SkillTagIndex {
|
|
20
|
+
const key = roots.join("\u0000")
|
|
21
|
+
if (cacheKey === key && cacheIndex) return cacheIndex
|
|
22
|
+
const tagMap = new Map<string, string[]>()
|
|
23
|
+
const skillTags = new Map<string, string[]>()
|
|
24
|
+
const skillMustDo = new Map<string, string[]>()
|
|
25
|
+
const seen = new Set<string>()
|
|
26
|
+
for (const root of roots) {
|
|
27
|
+
if (!existsSync(root)) continue
|
|
28
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
29
|
+
if (!entry.isDirectory()) continue
|
|
30
|
+
if (seen.has(entry.name)) continue
|
|
31
|
+
const mdPath = resolve(root, entry.name, "SKILL.md")
|
|
32
|
+
if (!existsSync(mdPath)) continue
|
|
33
|
+
try {
|
|
34
|
+
const raw = readFileSync(mdPath, "utf-8")
|
|
35
|
+
const m = raw.match(/capabilities:\s*\[([^\]]*)\]/)
|
|
36
|
+
if (!m) continue
|
|
37
|
+
const tags = m[1].split(",").map((s: string) => s.trim().replace(/["']/g, "")).filter(Boolean)
|
|
38
|
+
skillTags.set(entry.name, tags)
|
|
39
|
+
for (const tag of tags) {
|
|
40
|
+
const arr = tagMap.get(tag) || []
|
|
41
|
+
arr.push(entry.name)
|
|
42
|
+
tagMap.set(tag, arr)
|
|
43
|
+
}
|
|
44
|
+
// 可选约定:must_do 必做清单(机器可读)。单行数组形态,与 capabilities 解析同机制;
|
|
45
|
+
// 未声明的 skill 不参与必做清单覆盖度门禁(优雅降级,缺失不报错)。
|
|
46
|
+
const mdo = raw.match(/must_do:\s*\[([^\]]*)\]/)
|
|
47
|
+
if (mdo) {
|
|
48
|
+
const items = mdo[1].split(",").map((s: string) => s.trim().replace(/["']/g, "")).filter(Boolean)
|
|
49
|
+
if (items.length > 0) skillMustDo.set(entry.name, items)
|
|
50
|
+
}
|
|
51
|
+
seen.add(entry.name)
|
|
52
|
+
} catch { /* skip unreadable */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
cacheKey = key
|
|
56
|
+
cacheIndex = { tagMap, skillTags, skillMustDo }
|
|
57
|
+
return cacheIndex
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 按 capability tag 语义匹配 skill:skill 含 tech-stack- 前缀 tag 归 techStackOnly,否则归 generic。
|
|
62
|
+
* caps 为空/undefined 返回空。
|
|
63
|
+
*/
|
|
64
|
+
export function resolveSkillsForCapabilities(
|
|
65
|
+
caps: string[] | undefined,
|
|
66
|
+
index?: SkillTagIndex,
|
|
67
|
+
): { skillNames: string[]; techStackOnly: string[]; generic: string[] } {
|
|
68
|
+
if (!caps || caps.length === 0) return { skillNames: [], techStackOnly: [], generic: [] }
|
|
69
|
+
const idx = index ?? scanSkillTags()
|
|
70
|
+
const { tagMap, skillTags } = idx
|
|
71
|
+
const matched = new Set<string>()
|
|
72
|
+
for (const cap of caps) {
|
|
73
|
+
for (const n of tagMap.get(cap) || []) matched.add(n)
|
|
74
|
+
}
|
|
75
|
+
const skillNames: string[] = []
|
|
76
|
+
const generic: string[] = []
|
|
77
|
+
const techStackOnly: string[] = []
|
|
78
|
+
for (const name of matched) {
|
|
79
|
+
skillNames.push(name)
|
|
80
|
+
const tags = skillTags.get(name) || []
|
|
81
|
+
if (tags.some((t) => t.startsWith("tech-stack-"))) techStackOnly.push(name)
|
|
82
|
+
else generic.push(name)
|
|
83
|
+
}
|
|
84
|
+
return { skillNames, techStackOnly, generic }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** efficiency tag 命中的效率类 skill 名 */
|
|
88
|
+
export function getEfficiencySkills(index: SkillTagIndex = scanSkillTags()): string[] {
|
|
89
|
+
return index.tagMap.get("efficiency") || []
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 取 skill 声明的机器可读必做清单(must_do);未声明返回 null(该 skill 不参与覆盖度门禁)。 */
|
|
93
|
+
export function getSkillMustDo(name: string, index: SkillTagIndex = scanSkillTags()): string[] | null {
|
|
94
|
+
return index.skillMustDo.get(name) ?? null
|
|
95
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { resolve, dirname, join } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import { homedir } from "node:os"
|
|
5
|
+
|
|
6
|
+
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..")
|
|
7
|
+
|
|
8
|
+
export const SKILL_SCAN_ROOTS: string[] = [
|
|
9
|
+
resolve(projectRoot, "assets", "skills"),
|
|
10
|
+
resolve(projectRoot, ".agents", "skills"),
|
|
11
|
+
resolve(projectRoot, ".opencode", "skills"),
|
|
12
|
+
join(homedir(), ".agents", "skills"),
|
|
13
|
+
join(homedir(), ".config", "opencode", "skills"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
// 仅随插件分发注入 opencode 的 skill 根目录。.agents/skills 靠 opencode 标准发现对本仓库自用生效,非插件注入职责。
|
|
17
|
+
export const DISTRIBUTED_SKILL_ROOTS: string[] = [
|
|
18
|
+
resolve(projectRoot, "assets", "skills"),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
export function findSkillPath(name: string): string | null {
|
|
22
|
+
for (const root of SKILL_SCAN_ROOTS) {
|
|
23
|
+
const p = resolve(root, name, "SKILL.md")
|
|
24
|
+
if (existsSync(p)) return p
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|