@ifrankwang/openspec-agents 0.119.1 → 0.120.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ifrankwang/openspec-agents",
3
- "version": "0.119.1",
3
+ "version": "0.120.0",
4
4
  "type": "module",
5
5
  "main": "./src/core/index.ts",
6
6
  "exports": {
@@ -36,6 +36,14 @@
36
36
  "import": "./src/adapters/zcode/index.ts",
37
37
  "types": "./src/adapters/zcode/index.ts"
38
38
  },
39
+ "./deepseek-harness": {
40
+ "import": "./src/adapters/deepseek-harness/index.ts",
41
+ "types": "./src/adapters/deepseek-harness/index.ts"
42
+ },
43
+ "./dsh": {
44
+ "import": "./src/adapters/deepseek-harness/index.ts",
45
+ "types": "./src/adapters/deepseek-harness/index.ts"
46
+ },
39
47
  "./mcp": {
40
48
  "import": "./src/adapters/mcp-common/index.ts",
41
49
  "types": "./src/adapters/mcp-common/index.ts"
@@ -47,11 +55,15 @@
47
55
  },
48
56
  "files": [
49
57
  "src",
50
- "assets"
58
+ "assets",
59
+ "dsh",
60
+ ".mcp-server"
51
61
  ],
52
62
  "scripts": {
53
63
  "sync": "bun scripts/sync.ts",
54
64
  "build:plugins": "bun scripts/build-plugins.ts",
65
+ "build:dsh": "bun scripts/build-dsh-root.ts",
66
+ "prepack": "bun run build:dsh",
55
67
  "test": "bun test",
56
68
  "typecheck": "tsc --noEmit"
57
69
  },
@@ -65,6 +77,11 @@
65
77
  "js-yaml": "^4.1.0",
66
78
  "@opencode-ai/plugin": "^1.17.0"
67
79
  },
80
+ "dsh": {
81
+ "bundle": {
82
+ "patch": "./dsh/cordis.patch.yml"
83
+ }
84
+ },
68
85
  "publishConfig": {
69
86
  "access": "public"
70
87
  },
@@ -73,12 +73,12 @@ export function injectCodexMcp(repoRoot: string, serverEntry: string): void {
73
73
  mkdirSync(dirname(target), { recursive: true })
74
74
  const existing = existsSync(target) ? readFileSync(target, "utf-8") : ""
75
75
  const block = [
76
- `[mcp_servers.openspec-agents]`,
76
+ `[mcp_servers.opx]`,
77
77
  `command = "node"`,
78
- `args = ["${tomlEscape(serverEntry)}", "--transport", "stdio", "--worktree", ".", "--unattended"]`,
78
+ `args = ["${tomlEscape(serverEntry)}", "--transport", "stdio", "--worktree", ".", "--unattended", "--strip-opx-prefix"]`,
79
79
  ].join("\n")
80
80
  // 已存在同名 server 块则整体替换,否则追加
81
- const re = /\[mcp_servers\.openspec-agents\][\s\S]*?(?=\n\[|\n*$)/m
81
+ const re = /\[mcp_servers\.opx\][\s\S]*?(?=\n\[|\n*$)/m
82
82
  const next = re.test(existing) ? existing.replace(re, block) : `${existing.trimEnd()}\n\n${block}\n`
83
83
  writeFileSync(target, next, "utf-8")
84
84
  }
@@ -137,7 +137,7 @@ export function buildCodexPlugin(outDir: string = CODEX_PLUGIN_DIR): PluginPacka
137
137
  JSON.stringify(
138
138
  {
139
139
  mcpServers: {
140
- [PLUGIN_NAME]: {
140
+ opx: {
141
141
  command: "node",
142
142
  args: [
143
143
  "./.mcp-server/cli.mjs",
@@ -146,6 +146,7 @@ export function buildCodexPlugin(outDir: string = CODEX_PLUGIN_DIR): PluginPacka
146
146
  "--worktree",
147
147
  ".",
148
148
  "--unattended",
149
+ "--strip-opx-prefix",
149
150
  ],
150
151
  cwd: ".",
151
152
  },
@@ -0,0 +1,205 @@
1
+ /**
2
+ * DeepSeek Harness (DSH) 适配器:生成 DSH bundle 插件包。
3
+ *
4
+ * DSH 的插件形态不是 Claude/Codex/ZCode 的 plugin.json,而是 npm 包 +
5
+ * package.json 中 `dsh.bundle.patch` 指向的 cordis patch 文件。本适配器复用
6
+ * plugin-common 的 MCP server bundle / skills / workflows 资产构建,生成一个
7
+ * 可直接用 `dsh plugin --profile <name> add <dir>` 安装的 bundle 包。
8
+ *
9
+ * 生成的 patch 不引入自研 Cordis 插件,而是直接插入 DSH 官方能力:
10
+ * - `@deepseek-ai/dsh-mcp-client`:把本项目 MCP server(opx_* 工具)桥接为
11
+ * DSH 原生工具(mcp__opx__*)。
12
+ * - `@deepseek-ai/dsh-skill-filesystem`:把 assets/skills 注册为额外 skill 根,
13
+ * 使 orchestrator 与各质量维度 skill 可被 DSH agent 发现/加载。
14
+ * - `@deepseek-ai/dsh-tool-subagent`:为每个 assets/agents 子代理生成一个
15
+ * DSH 原生 subagent 工具(如 openspec_architect / openspec_developer),
16
+ * 子代理 persona 取自对应 agent.md,真正接入 DSH 子代理体系。
17
+ */
18
+ import { writeFileSync, mkdirSync, rmSync, readFileSync, readdirSync } from "node:fs"
19
+ import { join } from "node:path"
20
+ import * as yaml from "js-yaml"
21
+ import { parseAgentMd, resolve, EXCLUDED_AGENTS } from "../agent-md.ts"
22
+ import {
23
+ PLUGIN_DESCRIPTION,
24
+ bundleMcpServer,
25
+ buildAgents,
26
+ buildSkills,
27
+ copyWorkflows,
28
+ readPkgVersion,
29
+ type PluginPackageResult,
30
+ } from "../plugin-common/index.ts"
31
+
32
+ /** DSH bundle 的 npm 包名(与项目发布包一致,便于 `dsh plugin add` 与 sync 识别)。 */
33
+ export const DSH_PLUGIN_NAME = "@ifrankwang/openspec-agents"
34
+
35
+ /** 默认插件包输出目录(dist/ 已 gitignore,生成物不入库)。 */
36
+ export const DEEP_SEEK_HARNESS_PLUGIN_DIR = resolve("dist", "deepseek-harness-plugin")
37
+ /** 简写别名,便于与 `dsh` 缩写保持一致。 */
38
+ export const DSH_PLUGIN_DIR = DEEP_SEEK_HARNESS_PLUGIN_DIR
39
+
40
+ const MANIFEST_PATCH_FILENAME = "cordis.patch.yml"
41
+
42
+ /** MCP client 与 skill 根的基础 patch;子代理工具行由 buildDshPatchContent 动态追加。 */
43
+ const DSH_BASE_PATCH_YAML = `# DeepSeek Harness (DSH) bundle patch: expose OpenSpec MCP tools and skills.
44
+ # The patch is applied as a bundle layer under dsh.profile.bundles.
45
+ # baseUrl 指向 profile 根目录;bundle 安装后位于 profile 的 node_modules 下,
46
+ # 因此从这里引用 node_modules/@ifrankwang/openspec-agents/... 是稳定的。
47
+ - insert:
48
+ - id: openspec-mcp
49
+ name: '@deepseek-ai/dsh-mcp-client'
50
+ config:
51
+ serverName: opx
52
+ transport: stdio
53
+ command: node
54
+ args:
55
+ - !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('./node_modules/@ifrankwang/openspec-agents/.mcp-server/cli.mjs', baseUrl))"
56
+ - '--transport'
57
+ - 'stdio'
58
+ - '--worktree'
59
+ - !!js "process.cwd()"
60
+ - '--unattended'
61
+ - '--strip-opx-prefix'
62
+ cwd: !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('./node_modules/@ifrankwang/openspec-agents', baseUrl))"
63
+ - id: openspec-skills
64
+ name: '@deepseek-ai/dsh-skill-filesystem'
65
+ config:
66
+ providerName: openspec-filesystem
67
+ includeDefaultRoots: false
68
+ customSkillDirs:
69
+ - !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('./node_modules/@ifrankwang/openspec-agents/assets/skills', baseUrl))"
70
+ `
71
+
72
+ interface DshSubagentEntry {
73
+ id: string
74
+ name: string
75
+ config: Record<string, unknown>
76
+ }
77
+
78
+ /** 将 openspec-architect 转换为 openspec_architect 这类 DSH 工具名后缀。 */
79
+ function dshAgentSuffix(name: string): string {
80
+ return name.replace(/^openspec-/, "").replace(/-/g, "_")
81
+ }
82
+
83
+ /** 根据 agent.md frontmatter 判断是否应禁止 DSH 子代理的写文件类工具。 */
84
+ function denyEditTools(frontmatter: Record<string, unknown>): boolean {
85
+ const permission = frontmatter.permission as Record<string, unknown> | undefined
86
+ const edit = permission?.edit
87
+ if (edit === "deny") return true
88
+ if (edit && typeof edit === "object") {
89
+ const rule = edit as Record<string, unknown>
90
+ // architect 允许编辑 md,不能整体禁止写工具
91
+ return rule["*"] === "deny" && rule["*.md"] !== "allow"
92
+ }
93
+ return false
94
+ }
95
+
96
+ /** DSH 子代理 persona 前置的工具接入说明,避免子代理在首次 opx_status 前用 shell 全盘搜索工具。 */
97
+ const DSH_SUBAGENT_TOOL_ACCESS_PREAMBLE = `## DSH 工具接入(必读)
98
+
99
+ - 本流程的 opx_* 工具在 DSH 中以 \`mcp__opx__*\` 形式出现,子代理通常只需要:
100
+ - \`mcp__opx__status\`
101
+ - \`mcp__opx__agent_submit\`
102
+ - 如果当前工具列表中没有这些工具,请立即调用 \`dev_tool_search\` 搜索 \`opx\`,或一次性解锁:
103
+ \`\`\`json
104
+ {"toolNames": ["mcp__opx__status", "mcp__opx__agent_submit"]}
105
+ \`\`\`
106
+ 如果计划使用 \`todo_write\` 跟踪任务,也请在同一轮一并解锁。
107
+ - 严禁使用 \`find\` / \`which\` / \`grep\` 等 shell 命令搜索 \`opx\`、\`openspec\`、MCP 配置或安装路径;工具未出现是因为尚未解锁,不是未安装。
108
+ - 解锁后第一件事是调用 \`mcp__opx__status\` 获取上下文,不要先探查仓库或读取状态文件。
109
+ `
110
+
111
+ /** 从 assets/agents/*.md 收集 DSH 子代理工具(跳过主代理模板)。 */
112
+ function collectDshSubagentEntries(): DshSubagentEntry[] {
113
+ const agentsRoot = resolve("assets", "agents")
114
+ const entries: DshSubagentEntry[] = []
115
+ for (const file of readdirSync(agentsRoot)) {
116
+ if (!file.endsWith(".md")) continue
117
+ if (EXCLUDED_AGENTS.has(file)) continue
118
+ const { frontmatter, body } = parseAgentMd(readFileSync(join(agentsRoot, file), "utf-8"))
119
+ const name = (frontmatter.name as string) ?? file.replace(/\.md$/, "")
120
+ if (!name) continue
121
+ const description = (frontmatter.description as string) ?? ""
122
+ const persona = [DSH_SUBAGENT_TOOL_ACCESS_PREAMBLE, description, body.trim()].filter(Boolean).join("\n\n")
123
+ const suffix = dshAgentSuffix(name)
124
+ entries.push({
125
+ id: `openspec-subagent-${suffix}`,
126
+ name: "@deepseek-ai/dsh-tool-subagent",
127
+ config: {
128
+ provider: "spawn",
129
+ toolName: `openspec_${suffix}`,
130
+ persona,
131
+ enableRunInBackground: false,
132
+ backgroundMode: "one-shot",
133
+ ...(denyEditTools(frontmatter)
134
+ ? { toolFilter: { deny: ["write", "edit", "apply_patch", "str_replace_editor"] } }
135
+ : {}),
136
+ },
137
+ })
138
+ }
139
+ return entries
140
+ }
141
+
142
+ /** 生成完整的 DSH cordis patch 内容:基础 MCP/skills + 每个子代理一个 DSH 原生 subagent 工具。 */
143
+ export function buildDshPatchContent(): string {
144
+ const subagents = collectDshSubagentEntries()
145
+ if (subagents.length === 0) return DSH_BASE_PATCH_YAML
146
+ const subagentsYaml = yaml
147
+ .dump(subagents, { lineWidth: -1 })
148
+ .split("\n")
149
+ .map((line) => (line ? ` ${line}` : line))
150
+ .join("\n")
151
+ return `${DSH_BASE_PATCH_YAML.trimEnd()}\n${subagentsYaml}\n`
152
+ }
153
+
154
+ export type DeepSeekHarnessPluginBuildResult = PluginPackageResult
155
+
156
+ /**
157
+ * 生成 DeepSeek Harness bundle 插件包到 outDir(默认 dist/deepseek-harness-plugin/):
158
+ * package.json(含 dsh.bundle.patch)、cordis.patch.yml、.mcp-server/cli.mjs bundle、
159
+ * assets/skills/、assets/workflows/。
160
+ *
161
+ * 返回生成的组件清单供调用方/测试断言;DSH 原生不消费 Markdown agent 文件,
162
+ * 但为保持与其它插件包一致仍会复制 agents/ 目录。
163
+ */
164
+ export function buildDeepSeekHarnessPlugin(outDir: string = DEEP_SEEK_HARNESS_PLUGIN_DIR): DeepSeekHarnessPluginBuildResult {
165
+ rmSync(outDir, { recursive: true, force: true })
166
+ mkdirSync(join(outDir, "agents"), { recursive: true })
167
+ mkdirSync(join(outDir, "skills"), { recursive: true })
168
+ mkdirSync(join(outDir, "assets"), { recursive: true })
169
+ mkdirSync(join(outDir, ".mcp-server"), { recursive: true })
170
+
171
+ const version = readPkgVersion()
172
+ // DSH 通过上方 subagent 工具消费 agent 定义;agents/ 目录仍保留原始 markdown 供参考/审计。
173
+ const agents = buildAgents(join(outDir, "agents"))
174
+ const skills = buildSkills(join(outDir, "skills"))
175
+ bundleMcpServer(outDir)
176
+ copyWorkflows(outDir)
177
+
178
+ writeFileSync(
179
+ join(outDir, "package.json"),
180
+ JSON.stringify(
181
+ {
182
+ name: DSH_PLUGIN_NAME,
183
+ version,
184
+ description: PLUGIN_DESCRIPTION,
185
+ type: "module",
186
+ private: true,
187
+ dsh: {
188
+ bundle: {
189
+ patch: `./${MANIFEST_PATCH_FILENAME}`,
190
+ },
191
+ },
192
+ },
193
+ null,
194
+ 2,
195
+ ) + "\n",
196
+ "utf-8",
197
+ )
198
+
199
+ writeFileSync(join(outDir, MANIFEST_PATCH_FILENAME), buildDshPatchContent(), "utf-8")
200
+
201
+ return { pluginDir: outDir, version, agents, skills }
202
+ }
203
+
204
+ /** `dsh` 缩写的构建函数别名。 */
205
+ export const buildDshPlugin = buildDeepSeekHarnessPlugin
@@ -4,6 +4,7 @@
4
4
  * - --port <n> HTTP transport 端口(默认 4525)
5
5
  * - --transport http|stdio(默认 http)
6
6
  * - --unattended 默认无人值守(claude code / codex / zcode 适配器分发时开启)
7
+ * - --strip-opx-prefix 去掉 MCP 工具名的 opx_ 前缀(DSH 使用,得到 mcp__opx__status 这类短名)
7
8
  *
8
9
  * 运行:node <entry> --transport stdio --worktree . --unattended
9
10
  */
@@ -18,6 +19,7 @@ interface CliOptions {
18
19
  port: number
19
20
  transport: "http" | "stdio"
20
21
  unattended: boolean
22
+ stripOpxPrefix: boolean
21
23
  }
22
24
 
23
25
  function parseArgs(argv: string[]): CliOptions {
@@ -41,13 +43,14 @@ function parseArgs(argv: string[]): CliOptions {
41
43
  port: args.get("port") ? Number(args.get("port")) : DEFAULT_PORT,
42
44
  transport,
43
45
  unattended: flags.has("unattended"),
46
+ stripOpxPrefix: flags.has("strip-opx-prefix"),
44
47
  }
45
48
  }
46
49
 
47
50
  export async function main(argv: string[]): Promise<void> {
48
51
  const opts = parseArgs(argv)
49
52
  if (opts.transport === "stdio") {
50
- const mcp = buildMcpServer(opts.worktree, { unattended: opts.unattended })
53
+ const mcp = buildMcpServer(opts.worktree, { unattended: opts.unattended, stripOpxPrefix: opts.stripOpxPrefix })
51
54
  await mcp.connect(new StdioServerTransport())
52
55
  return
53
56
  }
@@ -127,13 +127,14 @@ const PKG_VERSION =
127
127
  ) as { version: string }).version
128
128
 
129
129
  /** 构建承载 6 个 opx_* 工具的 MCP server。unattended=true 时启用默认无人值守(spec: unattended-default)。 */
130
- export function buildMcpServer(worktree: string, opts: { unattended?: boolean } = {}): McpServer {
130
+ export function buildMcpServer(worktree: string, opts: { unattended?: boolean; stripOpxPrefix?: boolean } = {}): McpServer {
131
131
  const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION })
132
132
  for (const [name, spec] of Object.entries(TOOL_SPECS)) {
133
+ const toolName = opts.stripOpxPrefix ? name.replace(/^opx_/, "") : name
133
134
  mcp.registerTool<any, any>(
134
- name,
135
+ toolName,
135
136
  {
136
- title: name,
137
+ title: toolName,
137
138
  description: spec.description,
138
139
  inputSchema: jsonSchemaToZod(withAgentArg(spec.schema)),
139
140
  },
@@ -22,7 +22,7 @@ export const PLUGIN_DESCRIPTION =
22
22
  /** plugin.json author(package.json 无 author 字段,取固定值)。 */
23
23
  export const PLUGIN_AUTHOR = "openspec-agents maintainers"
24
24
 
25
- function readPkgVersion(): string {
25
+ export function readPkgVersion(): string {
26
26
  return (JSON.parse(readFileSync(resolve("package.json"), "utf-8")) as { version: string }).version
27
27
  }
28
28
 
@@ -30,7 +30,7 @@ function readPkgVersion(): string {
30
30
  * 构建 MCP server 自包含 bundle(node cli.ts 直跑依赖 node_modules,插件安装后无此环境,
31
31
  * 故 bundle 单文件至插件包内,node 直接执行)。打包依赖本机 bun CLI(spawnSync("bun"))。
32
32
  */
33
- function bundleMcpServer(pluginDir: string): void {
33
+ export function bundleMcpServer(pluginDir: string): void {
34
34
  const out = join(pluginDir, ".mcp-server", "cli.mjs")
35
35
  mkdirSync(join(pluginDir, ".mcp-server"), { recursive: true })
36
36
  // dashboard 页面资源随 bundle 同目录放置(page.ts 按此布局探测)
@@ -55,7 +55,7 @@ function bundleMcpServer(pluginDir: string): void {
55
55
  }
56
56
 
57
57
  /** 转换 agents:从 assets/agents/*.md 取子代理(排除主代理模板),frontmatter 仅保留 name/description。 */
58
- function buildAgents(targetDir: string): string[] {
58
+ export function buildAgents(targetDir: string): string[] {
59
59
  const agentsRoot = resolve("assets", "agents")
60
60
  const injected: string[] = []
61
61
  for (const file of readdirSync(agentsRoot)) {
@@ -80,7 +80,7 @@ function buildAgents(targetDir: string): string[] {
80
80
  }
81
81
 
82
82
  /** 复制 skills:整个 skill 目录(SKILL.md + reference/ 等附属文件),含 orchestrator skill。 */
83
- function buildSkills(targetDir: string): string[] {
83
+ export function buildSkills(targetDir: string): string[] {
84
84
  const skillsRoot = resolve("assets", "skills")
85
85
  const injected: string[] = []
86
86
  for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
@@ -93,6 +93,11 @@ function buildSkills(targetDir: string): string[] {
93
93
  return injected
94
94
  }
95
95
 
96
+ /** 复制 workflow 定义到插件包(bundle 内按部署深度逐级上溯探测读取)。 */
97
+ export function copyWorkflows(targetDir: string): void {
98
+ cpSync(resolve("assets", "workflows"), join(targetDir, "assets", "workflows"), { recursive: true })
99
+ }
100
+
96
101
  export interface PluginPackageParams {
97
102
  /** 输出目录(如 dist/zcode-plugin/) */
98
103
  outDir: string
@@ -123,7 +128,7 @@ export function buildPluginPackage({ outDir, manifestDirName }: PluginPackagePar
123
128
  bundleMcpServer(outDir)
124
129
  // workflow 定义随包分发:bundle 内 TASK_WORKFLOW_PATH 按部署深度逐级上溯探测
125
130
  // assets/workflows/task.yaml,插件根(dist/cache 形态)上溯 1 级即命中
126
- cpSync(resolve("assets", "workflows"), join(outDir, "assets", "workflows"), { recursive: true })
131
+ copyWorkflows(outDir)
127
132
 
128
133
  writeFileSync(
129
134
  join(outDir, manifestDirName, "plugin.json"),
@@ -149,7 +154,7 @@ export function buildPluginPackage({ outDir, manifestDirName }: PluginPackagePar
149
154
  JSON.stringify(
150
155
  {
151
156
  mcpServers: {
152
- [PLUGIN_NAME]: {
157
+ opx: {
153
158
  type: "stdio",
154
159
  command: "node",
155
160
  args: [
@@ -159,6 +164,7 @@ export function buildPluginPackage({ outDir, manifestDirName }: PluginPackagePar
159
164
  "--worktree",
160
165
  "${CLAUDE_PROJECT_DIR}",
161
166
  "--unattended",
167
+ "--strip-opx-prefix",
162
168
  ],
163
169
  },
164
170
  },