@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,157 @@
1
+ /**
2
+ * 通用 MCP Server 承载层:6 个 opx_* 工具由 agent 无关的 MCP Server(HTTP transport)承载,
3
+ * 参数采用 P1 纯 JSON Schema。任意支持 MCP client 的 agent(opencode / claude code / codex / zcode)
4
+ * 均可发现与调用同一套工具。
5
+ *
6
+ * 身份约定(适配层职责,不污染 P1 契约):
7
+ * - 每个工具在 P1 schema 基础上附加可选 `_agent` 参数:子代理调用时传自身角色名
8
+ * (如 openspec-reviewer-tool),主代理调用缺省即编排视角(orchestrator: true);
9
+ * - `_agent` 缺省/为空视为编排主代理视角。
10
+ */
11
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
12
+ import { readFileSync } from "node:fs"
13
+ import { fileURLToPath } from "node:url"
14
+ import type { JSONSchema } from "../../core/provider.ts"
15
+ import type { ToolContext } from "../../core/tools/types.ts"
16
+ import {
17
+ initExecute, setWorktreeExecute, statusExecute,
18
+ completeTaskGroupExecute, setUnattendedExecute,
19
+ } from "../../core/tools/lifecycle.ts"
20
+ import { agentSubmitExecute } from "../../core/tools/submit.ts"
21
+ import {
22
+ orchInitSchema, setWorktreeSchema, statusSchema,
23
+ completeTaskGroupSchema, setUnattendedSchema, agentSubmitSchema,
24
+ } from "../../core/tools/schemas.ts"
25
+ import { readStateByWorktree, writeState } from "../../core/state.ts"
26
+ import { jsonSchemaToZod } from "./json-schema.ts"
27
+
28
+ /** 私有身份参数:附加到每个工具的 MCP 暴露 schema(下划线前缀表示非业务参数)。 */
29
+ const AGENT_ARG = "_agent"
30
+
31
+ interface ToolSpec {
32
+ description: string
33
+ schema: JSONSchema
34
+ execute(args: Record<string, unknown>, ctx: ToolContext): Promise<string>
35
+ }
36
+
37
+ const TOOL_SPECS: Record<string, ToolSpec> = {
38
+ opx_orch_init: {
39
+ description:
40
+ "初始化编排会话。传入变更 ID 和任务组 ID,工具自动解析 tasks.md 提取全部任务组并解析目标组子任务。可通过 recovery 参数恢复到指定阶段。无 recovery 重复初始化当前任务组时保留其阶段和进度;切换到其它任务组时初始化该组。",
41
+ schema: orchInitSchema,
42
+ execute: (args, ctx) => initExecute(args as any, ctx),
43
+ },
44
+ opx_orch_set_worktree: {
45
+ description:
46
+ "确保目标组的 git worktree 就绪。若已存在则复用,否则按规范自动创建(分支 task-group/{changeId}/{taskGroupId},路径 .worktree/{changeId}/task-group-{taskGroupId})。只补齐资源,不改变阶段。",
47
+ schema: setWorktreeSchema,
48
+ execute: (args, ctx) => setWorktreeExecute(args as any, ctx),
49
+ },
50
+ opx_status: {
51
+ description:
52
+ "统一状态/上下文查询(只读为主)。按调用者角色路由:编排视角→统计+worktree;architect→spec/blocker;developer→worktree/boundary/task/issue;reviewer-tool→tool 层控件 issue;reviewer-task→task 验证状态;quality reviewer→自维度既有 issue。",
53
+ schema: statusSchema,
54
+ execute: (args, ctx) => statusExecute({ change_id: (args as any).change_id }, ctx),
55
+ },
56
+ opx_orch_complete_task_group: {
57
+ description:
58
+ "完成任务组收尾:合并 task-group 分支到 baseBranch → 清理 worktree 与分支。须在收尾验证(verify_cleanup)通过后调用。合并冲突时中止并返回 blocked(保留 worktree/分支)。",
59
+ schema: completeTaskGroupSchema,
60
+ execute: (args, ctx) => completeTaskGroupExecute(args as any, ctx),
61
+ },
62
+ opx_orch_set_unattended: {
63
+ description:
64
+ "开启/关闭无人值守模式。开启后编排流程不再向用户提问:analyze 确认模式由架构师自行裁决(不确认用户);重试检查点、状态异常、blocker 处理等需拍板事项由主代理按编排行为准则自行决策并提交——检查点决策是 opx_agent_submit 工具调用,主代理可自行执行,不因无人值守而抑制。",
65
+ schema: setUnattendedSchema,
66
+ execute: (args, ctx) => setUnattendedExecute(args as any, ctx),
67
+ },
68
+ opx_agent_submit: {
69
+ description:
70
+ "通用 step 提交,按 step_id 路由到 workflow 对应 step。校验调用者属于该 step 的 agents(越权直接拒绝),提交后推进 workflow 状态机并写回编排状态。可通过 exempt_adjudications 对已申请豁免的 issue 进行裁定(dismissed→cancelled、rejected→回 todo);可通过 recheck_adjudications 复核已修复待复核(review 态)的 issue(passed→done、rejected→回 todo 并记 refix_count 与 reject_reason,谁提谁裁定)。",
71
+ schema: agentSubmitSchema,
72
+ execute: (args, ctx) => agentSubmitExecute(args as any, ctx),
73
+ },
74
+ }
75
+
76
+ /** 附加 _agent 身份参数到 P1 schema(仅 MCP 暴露层;OpenCode 直载形态无此参数)。 */
77
+ function withAgentArg(schema: JSONSchema): JSONSchema {
78
+ return {
79
+ ...schema,
80
+ properties: {
81
+ ...(schema.properties ?? {}),
82
+ [AGENT_ARG]: {
83
+ type: "string",
84
+ description: "调用者 agent 标识(子代理调用时传自身角色名,如 openspec-reviewer-tool;缺省为编排主代理视角)",
85
+ },
86
+ },
87
+ required: schema.required ?? [],
88
+ }
89
+ }
90
+
91
+ /** 解析调用上下文:_agent 缺省/空 → 编排主代理视角(orchestrator: true)。 */
92
+ function resolveContext(args: Record<string, unknown>, worktree: string): ToolContext {
93
+ const declared = typeof args[AGENT_ARG] === "string" && args[AGENT_ARG] !== ""
94
+ const agent = declared ? (args[AGENT_ARG] as string) : "primary"
95
+ return { worktree, agent, orchestrator: agent === "primary", identityDeclared: declared }
96
+ }
97
+
98
+ /**
99
+ * 默认无人值守:server 启动时声明(claude code / codex / zcode 适配器分发),
100
+ * 会话已初始化(有 changeId 与状态)且 state.unattended 未设置时自动置 true。
101
+ * 幂等:已设置过则跳过,不覆盖用户显式关闭的会话。
102
+ */
103
+ async function ensureDefaultUnattended(
104
+ args: Record<string, unknown>,
105
+ ctx: ToolContext,
106
+ ): Promise<void> {
107
+ const changeId = typeof (args as any).change_id === "string" ? (args as any).change_id : undefined
108
+ if (!changeId) return
109
+ try {
110
+ const state = await readStateByWorktree(ctx.worktree, changeId)
111
+ if (state && state.unattended === undefined) {
112
+ state.unattended = true
113
+ await writeState(ctx.worktree, state)
114
+ }
115
+ } catch {
116
+ // 状态不可读/不可写时静默跳过(不阻断工具主流程)
117
+ }
118
+ }
119
+
120
+ declare const __OPX_PKG_VERSION__: string | undefined
121
+ /** 版本号单一来源:package.json(发布时仅需改一处);bundle 形态(ZCode 插件包)由构建期 --define 注入常量,避免运行时读文件。 */
122
+ const PKG_VERSION =
123
+ typeof __OPX_PKG_VERSION__ === "string"
124
+ ? __OPX_PKG_VERSION__
125
+ : (JSON.parse(
126
+ readFileSync(fileURLToPath(new URL("../../../package.json", import.meta.url)), "utf-8"),
127
+ ) as { version: string }).version
128
+
129
+ /** 构建承载 6 个 opx_* 工具的 MCP server。unattended=true 时启用默认无人值守(spec: unattended-default)。 */
130
+ export function buildMcpServer(worktree: string, opts: { unattended?: boolean } = {}): McpServer {
131
+ const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION })
132
+ for (const [name, spec] of Object.entries(TOOL_SPECS)) {
133
+ mcp.registerTool<any, any>(
134
+ name,
135
+ {
136
+ title: name,
137
+ description: spec.description,
138
+ inputSchema: jsonSchemaToZod(withAgentArg(spec.schema)),
139
+ },
140
+ async (args: any, _extra: any) => {
141
+ const cleanArgs = { ...(args as Record<string, unknown>) }
142
+ delete cleanArgs[AGENT_ARG]
143
+ const ctx = resolveContext(args as Record<string, unknown>, worktree)
144
+ if (opts.unattended) {
145
+ // 执行前(存量会话)+ 执行后(opx_orch_init 新建会话)各补一次:幂等,已设置则跳过
146
+ await ensureDefaultUnattended(cleanArgs, ctx)
147
+ }
148
+ const result = await spec.execute(cleanArgs, ctx)
149
+ if (opts.unattended) {
150
+ await ensureDefaultUnattended(cleanArgs, ctx)
151
+ }
152
+ return { content: [{ type: "text" as const, text: result }] }
153
+ },
154
+ )
155
+ }
156
+ return mcp
157
+ }
@@ -0,0 +1,80 @@
1
+ import { readFileSync, existsSync, readdirSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import type { AgentConfig } from "@opencode-ai/sdk"
4
+ import { parseAgentMd, resolve } from "../agent-md.ts"
5
+
6
+ function mapPermission(
7
+ fm: Record<string, unknown>
8
+ ): AgentConfig["permission"] | undefined {
9
+ const raw = fm.permission as Record<string, unknown> | undefined
10
+ if (!raw) return undefined
11
+ const permission: AgentConfig["permission"] = {}
12
+ for (const [key, val] of Object.entries(raw)) {
13
+ if (typeof val === "string" && ["ask", "allow", "deny"].includes(val)) {
14
+ ;(permission as any)[key] = val
15
+ } else if (typeof val === "object" && val !== null) {
16
+ ;(permission as any)[key] = val
17
+ }
18
+ }
19
+ return permission
20
+ }
21
+
22
+ const AGENTS_ROOT = resolve("assets", "agents")
23
+
24
+ /** 已知主代理名集合:注入的 openspec-main + 用户既有 primary agent + opencode 默认主代理 "primary"。 */
25
+ const primaryAgents = new Set<string>(["primary"])
26
+
27
+ /** 编排视角角色判定:调用者是否为主代理(承担编排者职责),替代 agent 名硬编码。 */
28
+ export function isPrimaryAgent(agent: string): boolean {
29
+ return primaryAgents.has(agent)
30
+ }
31
+
32
+ export function injectAgents(config: Record<string, unknown>): void {
33
+ // 探测主代理名:config.agent 中 mode=primary 的条目(用户自定义 + 注入的主代理模板)
34
+ const existingAgents = (config.agent as Record<string, unknown>) ?? {}
35
+ for (const [name, cfg] of Object.entries(existingAgents)) {
36
+ if (typeof cfg === "object" && cfg !== null && (cfg as Record<string, unknown>).mode === "primary") {
37
+ primaryAgents.add(name)
38
+ }
39
+ }
40
+ if (existsSync(AGENTS_ROOT)) {
41
+ const files = readdirSync(AGENTS_ROOT).filter((f) => f.endsWith(".md"))
42
+ for (const file of files) {
43
+ const md = readFileSync(join(AGENTS_ROOT, file), "utf-8")
44
+ const { frontmatter, body } = parseAgentMd(md)
45
+ const name = pathToName(file) ?? (frontmatter.name as string)
46
+ if (!name) continue
47
+
48
+ const agentConfig: Record<string, unknown> = {
49
+ description: frontmatter.description ?? "",
50
+ mode: frontmatter.mode ?? "subagent",
51
+ prompt: body,
52
+ }
53
+ if (frontmatter.hidden !== undefined) {
54
+ agentConfig.hidden = frontmatter.hidden
55
+ }
56
+ const maxSteps = (frontmatter.maxSteps ?? frontmatter.steps) as number | undefined
57
+ if (maxSteps !== undefined) {
58
+ agentConfig.maxSteps = maxSteps
59
+ }
60
+ const perm = mapPermission(frontmatter)
61
+ if (perm) agentConfig.permission = perm
62
+
63
+ if (frontmatter.mode === "primary") {
64
+ primaryAgents.add(name)
65
+ }
66
+
67
+ const existingAgentsMap = (config.agent as Record<string, unknown>) ?? {}
68
+ const existingAgent = existingAgentsMap[name] as Record<string, unknown> | undefined
69
+ config.agent = {
70
+ ...existingAgentsMap,
71
+ [name]: { ...agentConfig, ...existingAgent },
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ function pathToName(filename: string): string | null {
78
+ const base = filename.replace(/\.md$/i, "")
79
+ return base || null
80
+ }
@@ -0,0 +1,64 @@
1
+ import { createServer } from "node:http"
2
+ import type { Server } from "node:http"
3
+ import { readDashboardState } from "../../core/dashboard.ts"
4
+ import { getDashboardPage } from "../../dashboard/page.ts"
5
+
6
+ const BASE_PORT = 4519
7
+ const MAX_ATTEMPTS = 20
8
+
9
+ const servers = new Map<string, Server>()
10
+
11
+ export async function startDashboard(worktree: string): Promise<void> {
12
+ if (servers.has(worktree)) return
13
+
14
+ const pageHtml = getDashboardPage()
15
+
16
+ // 端口占用自动递增:EADDRINUSE 走 server error 事件(异步),经 once("error") + await listen
17
+ // 判定重试;其余启动期错误仅记录不中断。修复前 try/catch 同步捕获永不触发,换端口失效。
18
+ for (let port = BASE_PORT; port < BASE_PORT + MAX_ATTEMPTS; port++) {
19
+ try {
20
+ const server = createServer(async (req, res) => {
21
+ try {
22
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`)
23
+ if (url.pathname === "/api/state") {
24
+ try {
25
+ const data = await readDashboardState(worktree)
26
+ res.writeHead(200, { "content-type": "application/json;charset=utf-8" })
27
+ res.end(JSON.stringify(data ?? { active: false }))
28
+ } catch (err) {
29
+ res.writeHead(500, { "content-type": "application/json;charset=utf-8" })
30
+ res.end(JSON.stringify({ active: false, error: String(err) }))
31
+ }
32
+ return
33
+ }
34
+ res.writeHead(200, { "content-type": "text/html;charset=utf-8" })
35
+ res.end(pageHtml)
36
+ } catch (err) {
37
+ console.error("[dashboard]", (err as Error).message)
38
+ res.writeHead(500, { "content-type": "text/plain;charset=utf-8" })
39
+ res.end("internal error")
40
+ }
41
+ })
42
+ await new Promise<void>((resolve, reject) => {
43
+ server.once("error", (err: NodeJS.ErrnoException) => {
44
+ if (err.code === "EADDRINUSE") {
45
+ reject(err)
46
+ } else {
47
+ console.error("[dashboard]", err.message)
48
+ resolve()
49
+ }
50
+ })
51
+ server.listen(port, "127.0.0.1", () => resolve())
52
+ })
53
+ servers.set(worktree, server)
54
+ console.log(`[dashboard] 编排进度看板 http://127.0.0.1:${port}`)
55
+ return
56
+ } catch {
57
+ continue
58
+ }
59
+ }
60
+
61
+ console.error(
62
+ `[dashboard] 无法启动编排进度看板:端口 ${BASE_PORT}-${BASE_PORT + MAX_ATTEMPTS - 1} 均被占用`
63
+ )
64
+ }
@@ -0,0 +1,34 @@
1
+ import { type Plugin } from "@opencode-ai/plugin"
2
+ import { injectSkills } from "./skills.ts"
3
+ import { injectAgents } from "./agents.ts"
4
+
5
+ import {
6
+ init,
7
+ set_worktree,
8
+ status,
9
+ complete_task_group,
10
+ set_unattended,
11
+ agent_submit,
12
+ } from "./tools.ts"
13
+
14
+ /**
15
+ * OpenCode 插件壳(兼容过渡入口):配置注入(agent/skill)+ 工具直载。
16
+ * dashboard/collector/poller 副作用已迁移至 MCP server 进程(src/adapters/mcp-common/),
17
+ * OpenCode 建议切换为 MCP client 形态接入;插件壳保留兼容直到双轨验证完成。
18
+ */
19
+ export const OpenspecOrchestratePlugin: Plugin = async (input) => {
20
+ return {
21
+ config: async (config) => {
22
+ injectAgents(config)
23
+ injectSkills(config)
24
+ },
25
+ tool: {
26
+ opx_orch_init: init,
27
+ opx_orch_set_worktree: set_worktree,
28
+ opx_status: status,
29
+ opx_orch_complete_task_group: complete_task_group,
30
+ opx_orch_set_unattended: set_unattended,
31
+ opx_agent_submit: agent_submit,
32
+ },
33
+ }
34
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 纯 JSON Schema → zod(OpenCode 链式 schema)转换器。
3
+ * 适配层专用:工具参数定义以 P1 纯 JSON Schema 为单一事实源(src/core/tools/schemas.ts),
4
+ * OpenCode 链式 API 仅作为转换目标,工具定义本身不依赖任何 agent 专有 API。
5
+ */
6
+ import { tool } from "@opencode-ai/plugin"
7
+ import type { JSONSchema } from "../../core/provider.ts"
8
+
9
+ function convert(schema: JSONSchema): any {
10
+ switch (schema.type) {
11
+ case "object": {
12
+ const shape: Record<string, any> = {}
13
+ const properties = schema.properties ?? {}
14
+ for (const [key, sub] of Object.entries(properties)) {
15
+ const converted = convert(sub)
16
+ shape[key] = (schema.required ?? []).includes(key) ? converted : tool.schema.optional(converted)
17
+ }
18
+ if (Object.keys(shape).length === 0) return tool.schema.object({})
19
+ return tool.schema.object(shape)
20
+ }
21
+ case "array": {
22
+ const items = schema.items ?? { type: "string" }
23
+ let arr = tool.schema.array(convert(items))
24
+ if (typeof schema.minItems === "number" && schema.minItems > 0) arr = arr.min(schema.minItems)
25
+ return arr
26
+ }
27
+ case "string": {
28
+ if (Array.isArray(schema.enum)) {
29
+ const values = schema.enum as string[]
30
+ return values.length === 0 ? tool.schema.string() : tool.schema.enum(values)
31
+ }
32
+ let s = tool.schema.string()
33
+ if (typeof schema.minLength === "number" && schema.minLength > 0) s = s.min(schema.minLength)
34
+ return s
35
+ }
36
+ case "number":
37
+ case "integer": {
38
+ let n = schema.type === "integer" ? tool.schema.number().int() : tool.schema.number()
39
+ if (typeof schema.minimum === "number") n = n.min(schema.minimum)
40
+ if (typeof schema.maximum === "number") n = n.max(schema.maximum)
41
+ return n
42
+ }
43
+ case "boolean": {
44
+ let b: any = tool.schema.boolean()
45
+ if (schema.default !== undefined) b = b.default(schema.default as boolean)
46
+ return b
47
+ }
48
+ case "null":
49
+ return tool.schema.null()
50
+ default:
51
+ return tool.schema.any()
52
+ }
53
+ }
54
+
55
+ /** JSON Schema → zod schema(description 注入)。 */
56
+ export function jsonSchemaToZod(schema: JSONSchema): any {
57
+ const base = convert(schema)
58
+ if (schema.description) base.describe(schema.description)
59
+ return base
60
+ }
@@ -0,0 +1,19 @@
1
+ import { existsSync } from "node:fs"
2
+ import { DISTRIBUTED_SKILL_ROOTS } from "../../skills/scan.ts"
3
+
4
+ export function injectSkills(config: Record<string, unknown>): void {
5
+ const skillsCfg = (config.skills ?? {}) as Record<string, unknown>
6
+ const paths = (skillsCfg.paths ?? []) as string[]
7
+
8
+ for (const root of DISTRIBUTED_SKILL_ROOTS) {
9
+ if (!existsSync(root)) continue
10
+ if (!paths.includes(root)) {
11
+ paths.push(root)
12
+ }
13
+ }
14
+
15
+ if (paths.length > 0) {
16
+ skillsCfg.paths = paths
17
+ config.skills = skillsCfg
18
+ }
19
+ }
@@ -0,0 +1,78 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import { BUILD_PHASE_TARGETS, REVIEW_LAYERS, REVIEW_VERIFY_STEPS } from "../../core/types.ts"
3
+ import {
4
+ initExecute, setWorktreeExecute, statusExecute,
5
+ completeTaskGroupExecute, setUnattendedExecute,
6
+ } from "../../core/tools/lifecycle.ts"
7
+ import { agentSubmitExecute } from "../../core/tools/submit.ts"
8
+ import {
9
+ orchInitSchema, setWorktreeSchema, statusSchema,
10
+ completeTaskGroupSchema, setUnattendedSchema, agentSubmitSchema,
11
+ } from "../../core/tools/schemas.ts"
12
+ import { jsonSchemaToZod } from "./json-schema.ts"
13
+ import { isPrimaryAgent } from "./agents.ts"
14
+ import type { ToolContext } from "../../core/tools/types.ts"
15
+
16
+ /** 直载形态调用上下文:agent 身份由会话运行时推导(恒显式声明),编排视角按主代理名判定。 */
17
+ function makeCtx(context: { worktree: string; agent: string }): ToolContext {
18
+ return {
19
+ worktree: context.worktree,
20
+ agent: context.agent,
21
+ orchestrator: isPrimaryAgent(context.agent),
22
+ identityDeclared: true,
23
+ }
24
+ }
25
+
26
+ export const init = tool({
27
+ description:
28
+ "初始化编排会话。传入变更 ID 和任务组 ID,工具自动解析 tasks.md 提取全部任务组并解析目标组子任务。可通过 recovery 参数恢复到指定阶段。无 recovery 重复初始化当前任务组时保留其阶段和进度;切换到其它任务组时初始化该组。",
29
+ args: jsonSchemaToZod(orchInitSchema).shape as any,
30
+ async execute(args, context) {
31
+ return initExecute(args as any, makeCtx(context))
32
+ },
33
+ })
34
+
35
+ export const set_worktree = tool({
36
+ description:
37
+ "确保目标组的 git worktree 就绪。若已存在则复用,否则按规范自动创建(分支 task-group/{changeId}/{taskGroupId},路径 .worktree/{changeId}/task-group-{taskGroupId})。只补齐资源,不改变阶段。",
38
+ args: jsonSchemaToZod(setWorktreeSchema).shape as any,
39
+ async execute(args, context) {
40
+ return setWorktreeExecute(args as any, makeCtx(context))
41
+ },
42
+ })
43
+
44
+ export const status = tool({
45
+ description:
46
+ "统一状态/上下文查询(只读为主)。按调用者角色路由:编排视角→统计+worktree;architect→spec/blocker;developer→worktree/boundary/task/issue;reviewer-tool→tool 层控件 issue;reviewer-task→task 验证状态;quality reviewer→自维度既有 issue。",
47
+ args: jsonSchemaToZod(statusSchema).shape as any,
48
+ async execute(args, context) {
49
+ return statusExecute({ change_id: args.change_id as string }, makeCtx(context))
50
+ },
51
+ })
52
+
53
+ export const complete_task_group = tool({
54
+ description:
55
+ "完成任务组收尾:合并 task-group 分支到 baseBranch → 清理 worktree 与分支。须在收尾验证(verify_cleanup)通过后调用。合并冲突时中止并返回 blocked(保留 worktree/分支)。",
56
+ args: jsonSchemaToZod(completeTaskGroupSchema).shape as any,
57
+ async execute(args, context) {
58
+ return completeTaskGroupExecute({ change_id: args.change_id as string }, makeCtx(context))
59
+ },
60
+ })
61
+
62
+ export const set_unattended = tool({
63
+ description:
64
+ "开启/关闭无人值守模式。开启后编排流程不再向用户提问:analyze 确认模式由架构师自行裁决(不确认用户);重试检查点、状态异常、blocker 处理等需拍板事项由主代理按编排行为准则自行决策并提交——检查点决策是 opx_agent_submit 工具调用,主代理可自行执行,不因无人值守而抑制。",
65
+ args: jsonSchemaToZod(setUnattendedSchema).shape as any,
66
+ async execute(args, context) {
67
+ return setUnattendedExecute(args as any, makeCtx(context))
68
+ },
69
+ })
70
+
71
+ export const agent_submit = tool({
72
+ description:
73
+ "通用 step 提交,按 step_id 路由到 workflow 对应 step。校验调用者属于该 step 的 agents(越权直接拒绝),提交后推进 workflow 状态机并写回编排状态。可通过 exempt_adjudications 对已申请豁免的 issue 进行裁定(dismissed→cancelled、rejected→回 todo);可通过 recheck_adjudications 复核已修复待复核(review 态)的 issue(passed→done、rejected→回 todo 并记 refix_count 与 reject_reason,谁提谁裁定)。",
74
+ args: jsonSchemaToZod(agentSubmitSchema).shape as any,
75
+ async execute(args, context) {
76
+ return agentSubmitExecute(args as any, makeCtx(context))
77
+ },
78
+ })
@@ -0,0 +1,173 @@
1
+ /**
2
+ * 多 agent 插件包共享生成器(DRY):zcode 与 claude code 的官方插件机制格式同构——
3
+ * 插件目录 + 清单目录内 plugin.json(组件目录 agents/skills/.mcp.json 等与清单目录同级,
4
+ * 仅 plugin.json 放清单目录内)、MCP 模板变量 ${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PROJECT_DIR}。
5
+ * 差异仅为清单目录名,由各适配器以参数声明(manifestDirName)。
6
+ *
7
+ * 生成产物为一个完整插件目录:plugin.json 清单 + agents/*.md(从 assets/agents 转换、
8
+ * 排除主代理模板、frontmatter 仅保留 name/description)+ skills/<名>/(含 orchestrator 与
9
+ * reference/ 附属文件)+ assets/workflows/(task.yaml workflow 定义,bundle 内按部署深度
10
+ * 逐级上溯探测读取)+ .mcp.json(stdio + 自包含 bundle 入口 + 当前项目 worktree + 默认
11
+ * 无人值守)+ .mcp-server/cli.mjs 自包含 bundle(node 直接执行,不依赖 node_modules)。
12
+ */
13
+ import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, readdirSync, cpSync } from "node:fs"
14
+ import { join } from "node:path"
15
+ import { spawnSync } from "node:child_process"
16
+ import * as yaml from "js-yaml"
17
+ import { parseAgentMd, resolve, EXCLUDED_AGENTS } from "../agent-md.ts"
18
+
19
+ export const PLUGIN_NAME = "openspec-agents"
20
+ export const PLUGIN_DESCRIPTION =
21
+ "OpenSpec change 编排:opx_* 编排工具(MCP)+ 编排子代理 + orchestrator skill,非 OpenCode agent 默认无人值守"
22
+ /** plugin.json author(package.json 无 author 字段,取固定值)。 */
23
+ export const PLUGIN_AUTHOR = "openspec-agents maintainers"
24
+
25
+ function readPkgVersion(): string {
26
+ return (JSON.parse(readFileSync(resolve("package.json"), "utf-8")) as { version: string }).version
27
+ }
28
+
29
+ /**
30
+ * 构建 MCP server 自包含 bundle(node cli.ts 直跑依赖 node_modules,插件安装后无此环境,
31
+ * 故 bundle 单文件至插件包内,node 直接执行)。打包依赖本机 bun CLI(spawnSync("bun"))。
32
+ */
33
+ function bundleMcpServer(pluginDir: string): void {
34
+ const out = join(pluginDir, ".mcp-server", "cli.mjs")
35
+ mkdirSync(join(pluginDir, ".mcp-server"), { recursive: true })
36
+ // dashboard 页面资源随 bundle 同目录放置(page.ts 按此布局探测)
37
+ cpSync(resolve("assets", "dashboard"), join(pluginDir, ".mcp-server", "dashboard"), { recursive: true })
38
+ const result = spawnSync(
39
+ "bun",
40
+ [
41
+ "build",
42
+ resolve("src", "adapters", "mcp-common", "cli.ts"),
43
+ "--outfile",
44
+ out,
45
+ "--target",
46
+ "node",
47
+ "--define",
48
+ `__OPX_PKG_VERSION__="${readPkgVersion()}"`,
49
+ ],
50
+ { encoding: "utf-8", stdio: ["ignore", "ignore", "pipe"] },
51
+ )
52
+ if (result.status !== 0) {
53
+ throw new Error(`MCP server bundle 失败:${result.stderr ?? result.error?.message ?? "未知错误"}`)
54
+ }
55
+ }
56
+
57
+ /** 转换 agents:从 assets/agents/*.md 取子代理(排除主代理模板),frontmatter 仅保留 name/description。 */
58
+ function buildAgents(targetDir: string): string[] {
59
+ const agentsRoot = resolve("assets", "agents")
60
+ const injected: string[] = []
61
+ for (const file of readdirSync(agentsRoot)) {
62
+ if (!file.endsWith(".md")) continue
63
+ if (EXCLUDED_AGENTS.has(file)) continue
64
+ const { frontmatter, body } = parseAgentMd(readFileSync(join(agentsRoot, file), "utf-8"))
65
+ const name = (frontmatter.name as string) ?? file.replace(/\.md$/, "")
66
+ if (!name) continue
67
+ const pluginFrontmatter: Record<string, unknown> = { name, description: frontmatter.description ?? "" }
68
+ // plugin.json 省略 agents/skills/mcpServers 字段:官方按约定位置自动发现组件
69
+ // (插件根 agents/、skills/、.mcp.json),省略后 `claude plugin validate` 通过且
70
+ // `claude plugin details` 能发现全部组件;显式声明字符串字段反而报 Invalid input
71
+ // (`./` 前缀写法实测对 agents 同样报错),故不声明。
72
+ writeFileSync(
73
+ join(targetDir, `${name}.md`),
74
+ `---\n${yaml.dump(pluginFrontmatter)}---\n\n${body.trim()}\n`,
75
+ "utf-8",
76
+ )
77
+ injected.push(name)
78
+ }
79
+ return injected
80
+ }
81
+
82
+ /** 复制 skills:整个 skill 目录(SKILL.md + reference/ 等附属文件),含 orchestrator skill。 */
83
+ function buildSkills(targetDir: string): string[] {
84
+ const skillsRoot = resolve("assets", "skills")
85
+ const injected: string[] = []
86
+ for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
87
+ if (!entry.isDirectory()) continue
88
+ const src = join(skillsRoot, entry.name)
89
+ if (!existsSync(join(src, "SKILL.md"))) continue
90
+ cpSync(src, join(targetDir, entry.name), { recursive: true })
91
+ injected.push(entry.name)
92
+ }
93
+ return injected
94
+ }
95
+
96
+ export interface PluginPackageParams {
97
+ /** 输出目录(如 dist/zcode-plugin/) */
98
+ outDir: string
99
+ /** 清单目录名(".zcode-plugin" | ".claude-plugin"),组件目录与清单目录同级 */
100
+ manifestDirName: string
101
+ }
102
+
103
+ export interface PluginPackageResult {
104
+ pluginDir: string
105
+ version: string
106
+ agents: string[]
107
+ skills: string[]
108
+ }
109
+
110
+ /**
111
+ * 生成官方插件包到 outDir:<manifestDirName>/plugin.json 清单、agents/*.md、skills/<名>/、
112
+ * assets/workflows/、.mcp.json、.mcp-server/cli.mjs bundle。返回生成的组件清单供调用方/测试断言。
113
+ */
114
+ export function buildPluginPackage({ outDir, manifestDirName }: PluginPackageParams): PluginPackageResult {
115
+ rmSync(outDir, { recursive: true, force: true })
116
+ mkdirSync(join(outDir, manifestDirName), { recursive: true })
117
+ mkdirSync(join(outDir, "agents"), { recursive: true })
118
+ mkdirSync(join(outDir, "skills"), { recursive: true })
119
+
120
+ const version = readPkgVersion()
121
+ const agents = buildAgents(join(outDir, "agents"))
122
+ const skills = buildSkills(join(outDir, "skills"))
123
+ bundleMcpServer(outDir)
124
+ // workflow 定义随包分发:bundle 内 TASK_WORKFLOW_PATH 按部署深度逐级上溯探测
125
+ // assets/workflows/task.yaml,插件根(dist/cache 形态)上溯 1 级即命中
126
+ cpSync(resolve("assets", "workflows"), join(outDir, "assets", "workflows"), { recursive: true })
127
+
128
+ writeFileSync(
129
+ join(outDir, manifestDirName, "plugin.json"),
130
+ JSON.stringify(
131
+ {
132
+ name: PLUGIN_NAME,
133
+ version,
134
+ description: PLUGIN_DESCRIPTION,
135
+ license: "MIT",
136
+ author: { name: PLUGIN_AUTHOR },
137
+ },
138
+ null,
139
+ 2,
140
+ ) + "\n",
141
+ "utf-8",
142
+ )
143
+
144
+ // MCP server 声明:stdio + 默认无人值守;--worktree 指向当前打开项目根(官方模板变量
145
+ // ${CLAUDE_PROJECT_DIR},插件用户级安装后跨项目生效,不依赖进程 cwd 约定);
146
+ // server 入口为插件内自包含 bundle(${CLAUDE_PLUGIN_ROOT} 解析到安装后插件根)。
147
+ writeFileSync(
148
+ join(outDir, ".mcp.json"),
149
+ JSON.stringify(
150
+ {
151
+ mcpServers: {
152
+ [PLUGIN_NAME]: {
153
+ type: "stdio",
154
+ command: "node",
155
+ args: [
156
+ "${CLAUDE_PLUGIN_ROOT}/.mcp-server/cli.mjs",
157
+ "--transport",
158
+ "stdio",
159
+ "--worktree",
160
+ "${CLAUDE_PROJECT_DIR}",
161
+ "--unattended",
162
+ ],
163
+ },
164
+ },
165
+ },
166
+ null,
167
+ 2,
168
+ ) + "\n",
169
+ "utf-8",
170
+ )
171
+
172
+ return { pluginDir: outDir, version, agents, skills }
173
+ }