@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
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@ifrankwang/openspec-agents",
3
+ "version": "0.118.0",
4
+ "type": "module",
5
+ "main": "./src/core/index.ts",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./src/core/index.ts",
9
+ "types": "./src/core/index.ts"
10
+ },
11
+ "./core": {
12
+ "import": "./src/core/index.ts",
13
+ "types": "./src/core/index.ts"
14
+ },
15
+ "./provider": {
16
+ "import": "./src/core/provider.ts",
17
+ "types": "./src/core/provider.ts"
18
+ },
19
+ "./opencode": {
20
+ "import": "./src/adapters/opencode/index.ts",
21
+ "types": "./src/adapters/opencode/index.ts"
22
+ },
23
+ "./server": {
24
+ "import": "./src/adapters/opencode/index.ts",
25
+ "types": "./src/adapters/opencode/index.ts"
26
+ },
27
+ "./claude-code": {
28
+ "import": "./src/adapters/claude-code/index.ts",
29
+ "types": "./src/adapters/claude-code/index.ts"
30
+ },
31
+ "./codex": {
32
+ "import": "./src/adapters/codex/index.ts",
33
+ "types": "./src/adapters/codex/index.ts"
34
+ },
35
+ "./zcode": {
36
+ "import": "./src/adapters/zcode/index.ts",
37
+ "types": "./src/adapters/zcode/index.ts"
38
+ },
39
+ "./mcp": {
40
+ "import": "./src/adapters/mcp-common/index.ts",
41
+ "types": "./src/adapters/mcp-common/index.ts"
42
+ },
43
+ "./mcp/cli": {
44
+ "import": "./src/adapters/mcp-common/cli.ts",
45
+ "types": "./src/adapters/mcp-common/cli.ts"
46
+ }
47
+ },
48
+ "files": [
49
+ "src",
50
+ "assets"
51
+ ],
52
+ "scripts": {
53
+ "sync": "bun scripts/sync.ts",
54
+ "build:plugins": "bun scripts/build-plugins.ts",
55
+ "test": "bun test",
56
+ "typecheck": "tsc --noEmit"
57
+ },
58
+ "peerDependencies": {
59
+ "@opencode-ai/plugin": ">=1.17.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "@opencode-ai/plugin": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "devDependencies": {
67
+ "@opencode-ai/plugin": "^1.17.0",
68
+ "@types/bun": "^1.3.0",
69
+ "@types/js-yaml": "^4.0.9",
70
+ "typescript": "^5.9.0"
71
+ },
72
+ "dependencies": {
73
+ "@modelcontextprotocol/sdk": "^1.30.0",
74
+ "js-yaml": "^4.1.0"
75
+ },
76
+ "publishConfig": {
77
+ "access": "public"
78
+ }
79
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 多适配器共享的 agent 定义解析工具(DRY):agent.md frontmatter/body 解析、项目根路径解析、
3
+ * 非主代理形态注入排除清单。opencode / claude-code / codex / zcode 适配器统一引用。
4
+ */
5
+ import { dirname, resolve as pathResolve } from "node:path"
6
+ import { fileURLToPath } from "node:url"
7
+ import * as yaml from "js-yaml"
8
+
9
+ export interface ParsedAgent {
10
+ frontmatter: Record<string, unknown>
11
+ body: string
12
+ }
13
+
14
+ export function parseAgentMd(content: string): ParsedAgent {
15
+ const result: ParsedAgent = { frontmatter: {}, body: content }
16
+ if (!content.startsWith("---")) return result
17
+ const end = content.indexOf("---", 3)
18
+ if (end === -1) return result
19
+ const fmText = content.slice(3, end).trim()
20
+ result.body = content.slice(end + 3).trim()
21
+ try {
22
+ result.frontmatter = (yaml.load(fmText) as Record<string, unknown>) ?? {}
23
+ } catch {
24
+ result.body = content
25
+ }
26
+ return result
27
+ }
28
+
29
+ /** 项目根(assets/ 等资源所在目录)。 */
30
+ export const PROJECT_ROOT = pathResolve(dirname(fileURLToPath(import.meta.url)), "..", "..")
31
+
32
+ export function resolve(...p: string[]): string {
33
+ return pathResolve(PROJECT_ROOT, ...p)
34
+ }
35
+
36
+ /** 主代理模板(opencode 用)不在 claude code / codex / zcode 注入:主代理即本体,子代理定义全部注入。 */
37
+ export const EXCLUDED_AGENTS = new Set(["openspec-main.md"])
@@ -0,0 +1,35 @@
1
+ /**
2
+ * claude code 适配器:Claude Code 官方插件包生成(Plugin 机制,code.claude.com/docs/en/plugins)。
3
+ * 复用共享生成器 src/adapters/plugin-common/,差异参数:清单目录 .claude-plugin/。
4
+ *
5
+ * 与 zcode 同构的官方插件形态:安装/启用/禁用/卸载均为 CLI 或会话内命令
6
+ * (claude plugin marketplace add + claude plugin install,或 /plugin marketplace add + /plugin install),
7
+ * 插件安装后复制到本地缓存,子代理为插件只读形态。
8
+ *
9
+ * 已知限制(接入文档注明):
10
+ * - 插件子代理为 read-only,不支持 hooks/mcpServers/permissionMode frontmatter(本项目子代理
11
+ * 本就只读,无冲突);
12
+ * - claude plugin install / 会话内 /plugin install 为人工步骤,本适配器仅保证生成结构与官方
13
+ * schema 一致。
14
+ */
15
+ import { resolve } from "../agent-md.ts"
16
+ import {
17
+ buildPluginPackage,
18
+ type PluginPackageResult,
19
+ } from "../plugin-common/index.ts"
20
+
21
+ /** 默认插件包输出目录(dist/ 已 gitignore,生成物不入库)。 */
22
+ export const CLAUDE_CODE_PLUGIN_DIR = resolve("dist", "claude-code-plugin")
23
+ const MANIFEST_DIR_NAME = ".claude-plugin"
24
+
25
+ export type ClaudeCodePluginBuildResult = PluginPackageResult
26
+
27
+ /**
28
+ * 生成 Claude Code 官方插件包到 outDir(默认 dist/claude-code-plugin/):
29
+ * .claude-plugin/plugin.json 清单、agents/*.md、skills/<名>/、assets/workflows/、.mcp.json、
30
+ * .mcp-server/cli.mjs bundle。
31
+ * 返回生成的组件清单供调用方/测试断言。
32
+ */
33
+ export function buildClaudeCodePlugin(outDir: string = CLAUDE_CODE_PLUGIN_DIR): ClaudeCodePluginBuildResult {
34
+ return buildPluginPackage({ outDir, manifestDirName: MANIFEST_DIR_NAME })
35
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * codex 适配器:官方插件包生成(.codex-plugin/plugin.json + agents/skills/.mcp.json),
3
+ * 以及兼容手动 agent 定义注入(.codex/agents/*.toml)、MCP 配置分发(config.toml,TOML 格式)、
4
+ * 默认无人值守(MCP server 启动参数 --unattended)。
5
+ *
6
+ * 已知限制(接入文档注明):
7
+ * - codex 的 request_user_input 工具仅根线程(主代理)可用,且普通模式需 feature flag
8
+ * (--experimental-auto-plan / request_user_input 相关 flag)开启,codex exec 不可用;
9
+ * - 子代理无法向用户提问 → 默认无人值守,需拍板事项由主代理依据 orchestrator skill 自行裁决。
10
+ */
11
+ import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, readdirSync } from "node:fs"
12
+ import { join, dirname } from "node:path"
13
+ import { parseAgentMd, resolve, EXCLUDED_AGENTS } from "../agent-md.ts"
14
+ import { buildPluginPackage, PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_AUTHOR, type PluginPackageResult } from "../plugin-common/index.ts"
15
+
16
+ /** codex agent 工具白名单(按职责收敛,审查/分析角色不含写文件工具)。 */
17
+ const AGENT_TOOLS: Record<string, string[]> = {
18
+ "openspec-architect": ["read", "grep", "glob", "ls", "bash", "web"],
19
+ "openspec-developer": ["read", "grep", "glob", "ls", "bash", "apply_patch", "web"],
20
+ default: ["read", "grep", "glob", "ls", "bash"],
21
+ }
22
+
23
+ function tomlEscape(s: string): string {
24
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
25
+ }
26
+
27
+ /** 生成 codex agent TOML(.codex/agents/*.toml)。 */
28
+ function toAgentToml(name: string, description: string, body: string, tools: string[]): string {
29
+ const lines: string[] = [
30
+ `description = "${tomlEscape(description)}"`,
31
+ `tools = [${tools.map((t) => `"${t}"`).join(", ")}]`,
32
+ "",
33
+ "[instructions]",
34
+ `body = """`,
35
+ body.trim(),
36
+ `"""`,
37
+ "",
38
+ ]
39
+ return lines.join("\n")
40
+ }
41
+
42
+ /** 注入子代理定义到 .codex/agents/*.toml(目标仓库根目录)。返回注入的 agent 名列表。 */
43
+ export function injectCodexAgents(repoRoot: string): string[] {
44
+ const agentsRoot = resolve("assets", "agents")
45
+ if (!existsSync(agentsRoot)) return []
46
+ const targetDir = join(repoRoot, ".codex", "agents")
47
+ mkdirSync(targetDir, { recursive: true })
48
+ const injected: string[] = []
49
+ for (const file of readdirSync(agentsRoot)) {
50
+ if (!file.endsWith(".md")) continue
51
+ if (EXCLUDED_AGENTS.has(file)) continue
52
+ const md = readFileSync(join(agentsRoot, file), "utf-8")
53
+ const { frontmatter, body } = parseAgentMd(md)
54
+ const name = (frontmatter.name as string) ?? file.replace(/\.md$/, "")
55
+ if (!name) continue
56
+ const tools = AGENT_TOOLS[name] ?? AGENT_TOOLS.default
57
+ writeFileSync(
58
+ join(targetDir, `${name}.toml`),
59
+ toAgentToml(name, (frontmatter.description as string) ?? "", body, tools),
60
+ "utf-8",
61
+ )
62
+ injected.push(name)
63
+ }
64
+ return injected
65
+ }
66
+
67
+ /**
68
+ * 分发 MCP 注册配置到 .codex/config.toml(TOML,合并既有配置)。
69
+ * serverEntry 为 MCP server 可执行入口(node 脚本路径),stdio transport + 默认无人值守。
70
+ */
71
+ export function injectCodexMcp(repoRoot: string, serverEntry: string): void {
72
+ const target = join(repoRoot, ".codex", "config.toml")
73
+ mkdirSync(dirname(target), { recursive: true })
74
+ const existing = existsSync(target) ? readFileSync(target, "utf-8") : ""
75
+ const block = [
76
+ `[mcp_servers.openspec-agents]`,
77
+ `command = "node"`,
78
+ `args = ["${tomlEscape(serverEntry)}", "--transport", "stdio", "--worktree", ".", "--unattended"]`,
79
+ ].join("\n")
80
+ // 已存在同名 server 块则整体替换,否则追加
81
+ const re = /\[mcp_servers\.openspec-agents\][\s\S]*?(?=\n\[|\n*$)/m
82
+ const next = re.test(existing) ? existing.replace(re, block) : `${existing.trimEnd()}\n\n${block}\n`
83
+ writeFileSync(target, next, "utf-8")
84
+ }
85
+
86
+ /** 清理注入产物(测试/卸载用)。 */
87
+ export function removeCodexInjection(repoRoot: string): void {
88
+ try { rmSync(join(repoRoot, ".codex", "agents"), { recursive: true, force: true }) } catch {}
89
+ try { rmSync(join(repoRoot, ".codex", "config.toml"), { recursive: true, force: true }) } catch {}
90
+ }
91
+
92
+ /** Codex 官方插件包默认输出目录(dist/ 已 gitignore,生成物不入库)。 */
93
+ export const CODEX_PLUGIN_DIR = resolve("dist", "codex-plugin")
94
+
95
+ /**
96
+ * 生成 Codex 官方插件包到 outDir(默认 dist/codex-plugin/):
97
+ * .codex-plugin/plugin.json 清单、agents/*.md、skills/<名>/、assets/workflows/、
98
+ * .mcp.json、.mcp-server/cli.mjs bundle。
99
+ *
100
+ * 与 Claude/ZCode 插件包共用 plugin-common 的组件复制与 MCP bundle 构建,
101
+ * 差异仅在清单目录名、plugin.json 字段和 .mcp.json 的路径/模板变量。
102
+ */
103
+ export function buildCodexPlugin(outDir: string = CODEX_PLUGIN_DIR): PluginPackageResult {
104
+ const result = buildPluginPackage({ outDir, manifestDirName: ".codex-plugin" })
105
+
106
+ // Codex 官方 plugin.json:可声明 skills/mcpServers,interface 用于商店展示。
107
+ writeFileSync(
108
+ join(outDir, ".codex-plugin", "plugin.json"),
109
+ JSON.stringify(
110
+ {
111
+ name: PLUGIN_NAME,
112
+ version: result.version,
113
+ description: PLUGIN_DESCRIPTION,
114
+ author: { name: PLUGIN_AUTHOR },
115
+ license: "MIT",
116
+ skills: "./skills/",
117
+ mcpServers: "./.mcp.json",
118
+ interface: {
119
+ displayName: "OpenSpec Agents",
120
+ shortDescription: "OpenSpec change orchestration for Codex",
121
+ longDescription: PLUGIN_DESCRIPTION,
122
+ developerName: "ifrankwang",
123
+ category: "Developer Tools",
124
+ capabilities: ["Read", "Write"],
125
+ defaultPrompt: ["Run the OpenSpec change orchestration workflow."],
126
+ },
127
+ },
128
+ null,
129
+ 2,
130
+ ) + "\n",
131
+ "utf-8",
132
+ )
133
+
134
+ // Codex 插件 MCP 使用相对插件根的路径;--worktree . 指向 Codex 当前工作区。
135
+ writeFileSync(
136
+ join(outDir, ".mcp.json"),
137
+ JSON.stringify(
138
+ {
139
+ mcpServers: {
140
+ [PLUGIN_NAME]: {
141
+ command: "node",
142
+ args: [
143
+ "./.mcp-server/cli.mjs",
144
+ "--transport",
145
+ "stdio",
146
+ "--worktree",
147
+ ".",
148
+ "--unattended",
149
+ ],
150
+ cwd: ".",
151
+ },
152
+ },
153
+ },
154
+ null,
155
+ 2,
156
+ ) + "\n",
157
+ "utf-8",
158
+ )
159
+
160
+ return result
161
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * MCP server 可执行入口(CLI):
3
+ * - --worktree <path> 主仓库根(默认 process.cwd())
4
+ * - --port <n> HTTP transport 端口(默认 4525)
5
+ * - --transport http|stdio(默认 http)
6
+ * - --unattended 默认无人值守(claude code / codex / zcode 适配器分发时开启)
7
+ *
8
+ * 运行:node <entry> --transport stdio --worktree . --unattended
9
+ */
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
11
+ import { buildMcpServer } from "./server.ts"
12
+ import { startMcpServer, DEFAULT_PORT } from "./index.ts"
13
+ import { fileURLToPath } from "node:url"
14
+ import { realpathSync } from "node:fs"
15
+
16
+ interface CliOptions {
17
+ worktree: string
18
+ port: number
19
+ transport: "http" | "stdio"
20
+ unattended: boolean
21
+ }
22
+
23
+ function parseArgs(argv: string[]): CliOptions {
24
+ const args = new Map<string, string>()
25
+ const flags = new Set<string>()
26
+ for (let i = 0; i < argv.length; i++) {
27
+ if (argv[i].startsWith("--")) {
28
+ const key = argv[i].slice(2)
29
+ const val = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[i + 1] : ""
30
+ if (val !== "") {
31
+ args.set(key, val)
32
+ i++
33
+ } else {
34
+ flags.add(key)
35
+ }
36
+ }
37
+ }
38
+ const transport = args.get("transport") === "stdio" ? "stdio" : "http"
39
+ return {
40
+ worktree: args.get("worktree") ?? process.cwd(),
41
+ port: args.get("port") ? Number(args.get("port")) : DEFAULT_PORT,
42
+ transport,
43
+ unattended: flags.has("unattended"),
44
+ }
45
+ }
46
+
47
+ export async function main(argv: string[]): Promise<void> {
48
+ const opts = parseArgs(argv)
49
+ if (opts.transport === "stdio") {
50
+ const mcp = buildMcpServer(opts.worktree, { unattended: opts.unattended })
51
+ await mcp.connect(new StdioServerTransport())
52
+ return
53
+ }
54
+ startMcpServer(opts)
55
+ }
56
+
57
+ // 直接执行判定:realpath 归一化(macOS /tmp 与 /private/tmp 符号链接场景)
58
+ try {
59
+ if (realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1] ?? "")) {
60
+ void main(process.argv.slice(2))
61
+ }
62
+ } catch {
63
+ // 路径不可解析时静默跳过入口(作为库导入场景)
64
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * MCP Server 启动入口:HTTP transport(node:http)承载 6 个 opx_* 工具,
3
+ * 并承载 dashboard/collector/poller 副作用(原 OpenCode 插件壳职责迁移至此)。
4
+ * 非 OpenCode agent(claude code / codex / zcode)经 MCP client 接入同一套编排状态机。
5
+ */
6
+ import { createServer, type Server } from "node:http"
7
+ import { randomUUID } from "node:crypto"
8
+ import { join } from "node:path"
9
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"
10
+ import { buildMcpServer } from "./server.ts"
11
+ import { startDashboard } from "../opencode/dashboard.ts"
12
+ import { OpenSpecCollector, AdoCollector, registerCollector, startPolling } from "../../core/workflow/index.ts"
13
+ import { readContextFromWorktree } from "../../core/state.ts"
14
+
15
+ export interface McpServerOptions {
16
+ worktree: string
17
+ port?: number
18
+ hostname?: string
19
+ /** 默认无人值守(非 OpenCode agent 默认开启,spec: unattended-default)。 */
20
+ unattended?: boolean
21
+ }
22
+
23
+ export const DEFAULT_PORT = 4525
24
+ const MAX_PORT_ATTEMPTS = 20
25
+
26
+ /** 启动 dashboard/collector/poller 副作用(原插件壳职责,单一宿主 = MCP server 进程)。 */
27
+ function startSideEffects(worktree: string): void {
28
+ try {
29
+ void startDashboard(worktree)
30
+ // 注册内置收集器(OpenSpec + ADO 占位)并启动定时拉取;worktree 为动态上下文在此注入。
31
+ registerCollector(new OpenSpecCollector({ openspecDir: join(worktree, "openspec") }))
32
+ registerCollector(new AdoCollector())
33
+ // 仅已初始化(存在 context.json 上下文指针)的 worktree 启动轮询
34
+ void readContextFromWorktree(worktree).then((ctx) => {
35
+ if (ctx?.changeId && ctx?.taskGroupId) {
36
+ startPolling(worktree)
37
+ }
38
+ })
39
+ } catch {
40
+ // dashboard/调度启动失败不影响编排功能
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 启动 MCP server(Streamable HTTP)。会话管理:mcp-session-id 关联 transport(官方推荐模式)。
46
+ * unattended=true 时默认开启无人值守(非 OpenCode agent 适配器分发)。
47
+ * 返回实际绑定端口(server.address().port,port 为 0 或端口被占用换绑时以返回值为准)。
48
+ */
49
+ export async function startMcpServer(opts: McpServerOptions): Promise<{ server: Server; port: number }> {
50
+ const mcp = buildMcpServer(opts.worktree, { unattended: opts.unattended })
51
+ startSideEffects(opts.worktree)
52
+
53
+ const port = opts.port ?? DEFAULT_PORT
54
+ const hostname = opts.hostname ?? "127.0.0.1"
55
+ const transports = new Map<string, StreamableHTTPServerTransport>()
56
+
57
+ const server = createServer(async (req, res) => {
58
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? hostname}`)
59
+ if (url.pathname === "/mcp") {
60
+ const sessionId = Array.isArray(req.headers["mcp-session-id"])
61
+ ? req.headers["mcp-session-id"][0]
62
+ : req.headers["mcp-session-id"]
63
+ let transport = typeof sessionId === "string" ? transports.get(sessionId) : undefined
64
+ if (!transport) {
65
+ transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() })
66
+ try {
67
+ await mcp.connect(transport)
68
+ } catch (err) {
69
+ res.writeHead(500, { "content-type": "text/plain;charset=utf-8" })
70
+ res.end(`MCP transport connect 失败:${(err as Error).message}`)
71
+ return
72
+ }
73
+ }
74
+ await transport.handleRequest(req, res)
75
+ // session id 在首次 handleRequest 时才生成:生成后注册映射,后续请求复用同一 transport
76
+ if (transport.sessionId && !transports.has(transport.sessionId)) {
77
+ transports.set(transport.sessionId, transport)
78
+ transport.onclose = () => {
79
+ transports.delete(transport.sessionId ?? "")
80
+ }
81
+ }
82
+ return
83
+ }
84
+ if (req.method === "GET") {
85
+ res.writeHead(200, { "content-type": "application/json;charset=utf-8" })
86
+ res.end(JSON.stringify({ name: "openspec-agents", status: "ok" }))
87
+ return
88
+ }
89
+ res.writeHead(405, { "content-type": "text/plain;charset=utf-8" })
90
+ res.end("Method Not Allowed")
91
+ })
92
+
93
+ let started = false
94
+ for (let p = port; p < port + MAX_PORT_ATTEMPTS; p++) {
95
+ try {
96
+ await new Promise<void>((resolve, reject) => {
97
+ server.once("error", (err: NodeJS.ErrnoException) => {
98
+ if (err.code === "EADDRINUSE") {
99
+ reject(err)
100
+ } else {
101
+ console.error("[mcp]", err.message)
102
+ resolve()
103
+ }
104
+ })
105
+ server.listen(p, hostname, () => resolve())
106
+ })
107
+ started = true
108
+ console.log(`[mcp] openspec-agents MCP server http://${hostname}:${p}/mcp(worktree: ${opts.worktree})`)
109
+ break
110
+ } catch {
111
+ continue
112
+ }
113
+ }
114
+ if (!started) {
115
+ throw new Error(`无法启动 MCP server:端口 ${port}-${port + MAX_PORT_ATTEMPTS - 1} 均被占用`)
116
+ }
117
+ const addr = server.address()
118
+ const actualPort = typeof addr === "object" && addr !== null ? addr.port : 0
119
+ return { server, port: actualPort }
120
+ }
121
+
122
+ /** 命令行入口:node dist/... --worktree <path> [--port <n>] */
123
+ export function runCli(argv: string[]): void {
124
+ const args = new Map<string, string>()
125
+ for (let i = 0; i < argv.length; i++) {
126
+ if (argv[i].startsWith("--")) {
127
+ const key = argv[i].slice(2)
128
+ const val = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[i + 1] : ""
129
+ args.set(key, val)
130
+ if (val !== "") i++
131
+ }
132
+ }
133
+ const worktree = args.get("worktree") ?? process.cwd()
134
+ const port = args.get("port") ? Number(args.get("port")) : DEFAULT_PORT
135
+ const unattended = args.has("unattended")
136
+ startMcpServer({ worktree, port, unattended })
137
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 纯 JSON Schema → zod 转换(MCP SDK 工具 inputSchema 校验用)。
3
+ * 与 opencode 适配层转换器职责相同但独立实现(不依赖 @opencode-ai/plugin),
4
+ * 转换语义与 P1 纯 JSON Schema 契约一致。
5
+ */
6
+ import { z } from "zod"
7
+ import type { JSONSchema } from "../../core/provider.ts"
8
+
9
+ export function jsonSchemaToZod(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 = jsonSchemaToZod(sub)
16
+ shape[key] = (schema.required ?? []).includes(key) ? converted : converted.optional()
17
+ }
18
+ if (Object.keys(shape).length === 0) return z.object({})
19
+ return z.object(shape)
20
+ }
21
+ case "array": {
22
+ const items = schema.items ?? { type: "string" }
23
+ let arr = z.array(jsonSchemaToZod(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 ? z.string() : z.enum(values as [string, ...string[]])
31
+ }
32
+ let s = z.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" ? z.number().int() : z.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 = z.boolean()
45
+ if (schema.default !== undefined) b = b.default(schema.default as boolean)
46
+ return b
47
+ }
48
+ case "null":
49
+ return z.null()
50
+ default:
51
+ return z.any()
52
+ }
53
+ }