@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,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,2 @@
1
+ // 旧入口保留向后兼容
2
+ export { OpenspecOrchestratePlugin } from "./adapters/opencode/index.ts"
@@ -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
+