@ifrankwang/openspec-agents 0.150.0 → 0.150.2

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.
@@ -9033,7 +9033,11 @@ function withAgentArg(parameters) {
9033
9033
 
9034
9034
  // src/adapters/deepseek-harness/plugin.ts
9035
9035
  var Config = z.object({
9036
- unattended: z.boolean().default(true)
9036
+ unattended: z.boolean().default(true),
9037
+ subagents: z.array(z.object({
9038
+ toolName: z.string(),
9039
+ agentPreset: z.string()
9040
+ })).default([])
9037
9041
  });
9038
9042
  var name = "openspec-opx-tools";
9039
9043
  var inject = ["tools"];
@@ -9051,6 +9055,50 @@ function apply(ctx, config = {}) {
9051
9055
  }
9052
9056
  }));
9053
9057
  }
9058
+ mountSubagentTools(ctx, config.subagents ?? []);
9059
+ }
9060
+ var SUBAGENT_TOOL_PACKAGE = "@deepseek-ai/dsh-tool-subagent";
9061
+ var PRESET_READY_TIMEOUT_MS = 3000;
9062
+ var PRESET_READY_POLL_MS = 50;
9063
+ async function waitForPreset(registry, presetId, timeoutMs = PRESET_READY_TIMEOUT_MS) {
9064
+ const deadline = Date.now() + timeoutMs;
9065
+ while (Date.now() < deadline) {
9066
+ try {
9067
+ await registry.readDocument(presetId);
9068
+ return true;
9069
+ } catch {
9070
+ await new Promise((resolve) => setTimeout(resolve, PRESET_READY_POLL_MS));
9071
+ }
9072
+ }
9073
+ return false;
9074
+ }
9075
+ function mountSubagentTools(ctx, specs) {
9076
+ if (specs.length === 0 || typeof ctx.inject !== "function")
9077
+ return;
9078
+ ctx.inject(["agentPresets"], async (injected) => {
9079
+ let subagentTool;
9080
+ try {
9081
+ subagentTool = await import(SUBAGENT_TOOL_PACKAGE);
9082
+ } catch (error) {
9083
+ injected.logger?.warn?.(`openspec: 无法加载 ${SUBAGENT_TOOL_PACKAGE},子代理委派工具未挂载:${String(error)}`);
9084
+ return;
9085
+ }
9086
+ for (const spec of specs) {
9087
+ if (!await waitForPreset(injected.agentPresets, spec.agentPreset)) {
9088
+ injected.logger?.warn?.(`openspec: agent preset "${spec.agentPreset}" 未在 ${PRESET_READY_TIMEOUT_MS}ms 内注册,跳过子代理工具 ${spec.toolName}`);
9089
+ continue;
9090
+ }
9091
+ try {
9092
+ await injected.plugin(subagentTool, {
9093
+ provider: "spawn",
9094
+ toolName: spec.toolName,
9095
+ agentPreset: spec.agentPreset
9096
+ });
9097
+ } catch (error) {
9098
+ injected.logger?.warn?.(`openspec: 子代理工具 ${spec.toolName} 挂载失败:${String(error)}`);
9099
+ }
9100
+ }
9101
+ });
9054
9102
  }
9055
9103
  export {
9056
9104
  Config,
@@ -30629,7 +30629,7 @@ async function ensureDefaultUnattended(args, ctx) {
30629
30629
  }
30630
30630
  } catch {}
30631
30631
  }
30632
- var PKG_VERSION = "0.150.0";
30632
+ var PKG_VERSION = "0.150.2";
30633
30633
  function buildMcpServer(worktree, opts = {}) {
30634
30634
  const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION });
30635
30635
  for (const [name, spec] of Object.entries(TOOL_SPECS)) {
@@ -45,7 +45,6 @@ capabilities: ["orchestrator"]
45
45
  - 每次子代理返回后,调用状态查询工具获取权威「下一步」指令并严格遵循;推进被拦时按工具给出的阻塞原因与修复建议处理,不自行推断其他动作。DSH 形态下「返回」指收到子代理完成通知,不是分派工具的即时返回(见「分派范式」的 DSH 异步委派)
46
46
  - 分派/推进决策一律以工具返回为准:工具已给出明确分派指令时直接分派,不得先读状态文件交叉验证;仅门禁拒绝或工具给出修复建议时才读取状态文件核对
47
47
  - 分派子代理前先查询状态确认当前阶段/层,确保不跳阶段或错层分派;工具视图呈现 worktree 未就绪时不得分派
48
- - 不过度沟通:任务组内部不停下来向用户汇报,持续执行直到阻塞或完成
49
48
  - 断点续传:子代理因步骤限制中断后重新分派即可继续,无需保存已完成子任务列表
50
49
  - 工具列出多个子代理时并排分派(单条消息中同时发送),不串行等待
51
50
  - 分派前校验分派 prompt:按「禁止事项」中禁止转述的动态内容清单逐项检查,校验通过后再分派
@@ -12,6 +12,11 @@
12
12
  name: '@ifrankwang/openspec-agents/.dsh-plugin/opx-tools.mjs'
13
13
  config:
14
14
  unattended: true
15
+ subagents:
16
+ - toolName: openspec_developer
17
+ agentPreset: openspec-developer
18
+ - toolName: openspec_reviewer
19
+ agentPreset: openspec-reviewer
15
20
  - id: openspec-skills
16
21
  name: '@deepseek-ai/dsh-skill-filesystem'
17
22
  config:
@@ -105,12 +110,6 @@
105
110
  thresholdChars: 8192
106
111
  headChars: 4096
107
112
  tailChars: 1024
108
- - id: openspec-subagent-developer
109
- name: '@deepseek-ai/dsh-tool-subagent'
110
- config:
111
- provider: spawn
112
- toolName: openspec_developer
113
- agentPreset: openspec-developer
114
113
  - id: openspec-preset-reviewer
115
114
  name: '@deepseek-ai/dsh-agent-preset'
116
115
  config:
@@ -375,10 +374,4 @@
375
374
  thresholdChars: 8192
376
375
  headChars: 4096
377
376
  tailChars: 1024
378
- - id: openspec-subagent-reviewer
379
- name: '@deepseek-ai/dsh-tool-subagent'
380
- config:
381
- provider: spawn
382
- toolName: openspec_reviewer
383
- agentPreset: openspec-reviewer
384
377
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ifrankwang/openspec-agents",
3
- "version": "0.150.0",
3
+ "version": "0.150.2",
4
4
  "description": "OpenSpec companion plugin for AI coding agents (Claude Code, Codex, ZCode, DeepSeek Harness, OpenCode): a stricter proposal standard plus multi-agent implementation and review, gated to production-grade standards",
5
5
  "author": "ifrankwang",
6
6
  "license": "MIT",
@@ -68,7 +68,14 @@ export const DSH_NATIVE_TOOLS_ROW_ID = "openspec-opx-tools"
68
68
  */
69
69
  export const DSH_SKILLS_DIR_SEGMENTS = ["assets", "skills"] as const
70
70
 
71
- /** 原生工具插件与 skill 根的基础 patch;子代理工具行由 buildDshPatchContent 动态追加。 */
71
+ /**
72
+ * 原生工具插件与 skill 根的基础 patch。
73
+ *
74
+ * 子代理委派工具**不写成静态 patch 行**:DSH 加载器对同一层的条目并行激活,静态工具行会在
75
+ * preset 声明行注册完成之前校验 `agentPreset`,报 `Unknown agent preset`。改为把
76
+ * toolName ↔ preset id 清单放进插件配置,由插件在 preset 就绪后动态挂载委派工具
77
+ * (见 plugin.ts 的 mountSubagentTools),顺序由代码保证。
78
+ */
72
79
  const DSH_BASE_PATCH_YAML = `# DeepSeek Harness (DSH) bundle patch: expose OpenSpec orchestration tools and skills.
73
80
  # The patch is applied as a bundle layer under dsh.profile.bundles.
74
81
  # baseUrl 指向 profile 根目录;bundle 安装后位于 profile 的 node_modules 下,
@@ -205,15 +212,25 @@ function dshSubagentPresetPlugins(editAllowed: boolean): Record<string, unknown>
205
212
  ]
206
213
  }
207
214
 
215
+ /** 子代理委派规格:工具名 + 引用的 preset id(写入插件配置,供动态挂载使用)。 */
216
+ export interface DshSubagentSpec {
217
+ toolName: string
218
+ agentPreset: string
219
+ }
220
+
221
+ /** 解析一个子代理的 preset 声明与委派规格。 */
222
+ interface DshSubagentDefinition {
223
+ spec: DshSubagentSpec
224
+ presetEntry: DshSubagentEntry
225
+ }
226
+
208
227
  /**
209
- * 从 assets/agents/*.md 收集 DSH 子代理:每个物理子代理生成一个 Agent Preset 声明行
210
- * 与一个 `@deepseek-ai/dsh-tool-subagent` 工具行(后者以 `agentPreset` 引用前者)。
211
- *
212
- * preset 行必须排在工具行之前:工具行在加载时校验 `agentPreset` 是否已声明。
228
+ * 从 assets/agents/*.md 收集 DSH 子代理:每个物理子代理生成一个 Agent Preset 声明行,
229
+ * 以及一份「工具名 + preset id」规格(交给插件动态挂载委派工具)。
213
230
  */
214
- function collectDshSubagentEntries(): DshSubagentEntry[] {
231
+ function collectDshSubagents(): DshSubagentDefinition[] {
215
232
  const agentsRoot = resolve("assets", "agents")
216
- const entries: DshSubagentEntry[] = []
233
+ const definitions: DshSubagentDefinition[] = []
217
234
  for (const file of readdirSync(agentsRoot)) {
218
235
  if (!file.endsWith(".md")) continue
219
236
  if (EXCLUDED_AGENTS.has(file)) continue
@@ -224,8 +241,9 @@ function collectDshSubagentEntries(): DshSubagentEntry[] {
224
241
  const persona = [DSH_SUBAGENT_TOOL_ACCESS_PREAMBLE, description, body.trim()].filter(Boolean).join("\n\n")
225
242
  const suffix = dshAgentSuffix(name)
226
243
  const presetId = `openspec-${suffix.replace(/_/g, "-")}`
227
- entries.push(
228
- {
244
+ definitions.push({
245
+ spec: { toolName: `openspec_${suffix}`, agentPreset: presetId },
246
+ presetEntry: {
229
247
  id: `openspec-preset-${suffix}`,
230
248
  name: "@deepseek-ai/dsh-agent-preset",
231
249
  config: {
@@ -238,30 +256,37 @@ function collectDshSubagentEntries(): DshSubagentEntry[] {
238
256
  ],
239
257
  },
240
258
  },
241
- {
242
- id: `openspec-subagent-${suffix}`,
243
- name: "@deepseek-ai/dsh-tool-subagent",
244
- config: {
245
- provider: "spawn",
246
- toolName: `openspec_${suffix}`,
247
- agentPreset: presetId,
248
- },
249
- },
250
- )
259
+ })
251
260
  }
252
- return entries
261
+ return definitions
262
+ }
263
+
264
+ /** 把子代理规格投影为插件配置里的 `subagents` 块(缩进对齐 openspec-opx-tools 行的 config)。 */
265
+ function subagentsConfigYaml(specs: DshSubagentSpec[]): string {
266
+ if (specs.length === 0) return " subagents: []"
267
+ const dumped = yaml.dump(specs, { lineWidth: -1 }).trimEnd().split("\n")
268
+ return [" subagents:", ...dumped.map((line) => ` ${line}`)].join("\n")
269
+ }
270
+
271
+ /** 基础 patch:原生工具插件行带上子代理委派规格,供插件在 preset 就绪后动态挂载。 */
272
+ function buildBasePatchYaml(specs: DshSubagentSpec[]): string {
273
+ return DSH_BASE_PATCH_YAML.replace(
274
+ " unattended: true",
275
+ ` unattended: true\n${subagentsConfigYaml(specs)}`,
276
+ )
253
277
  }
254
278
 
255
- /** 生成完整的 DSH cordis patch 内容:基础原生工具插件行 + skill 根 + 每个子代理的 preset 与 subagent 工具。 */
279
+ /** 生成完整的 DSH cordis patch 内容:基础原生工具插件行 + skill 根 + 每个子代理的 preset 声明。 */
256
280
  export function buildDshPatchContent(): string {
257
- const subagents = collectDshSubagentEntries()
258
- if (subagents.length === 0) return DSH_BASE_PATCH_YAML
259
- const subagentsYaml = yaml
260
- .dump(subagents, { schema: DSH_YAML_SCHEMA, lineWidth: -1 })
281
+ const definitions = collectDshSubagents()
282
+ const base = buildBasePatchYaml(definitions.map((definition) => definition.spec))
283
+ if (definitions.length === 0) return base
284
+ const presetsYaml = yaml
285
+ .dump(definitions.map((definition) => definition.presetEntry), { schema: DSH_YAML_SCHEMA, lineWidth: -1 })
261
286
  .split("\n")
262
287
  .map((line) => (line ? ` ${line}` : line))
263
288
  .join("\n")
264
- return `${DSH_BASE_PATCH_YAML.trimEnd()}\n${subagentsYaml}\n`
289
+ return `${base.trimEnd()}\n${presetsYaml}\n`
265
290
  }
266
291
 
267
292
  export type DeepSeekHarnessPluginBuildResult = PluginPackageResult
@@ -25,6 +25,19 @@ import { buildNativeToolDefinitions, type DshToolExec } from "./native-tools.ts"
25
25
  const Config = z.object({
26
26
  /** 默认无人值守:会话已初始化且未显式设置时自动开启。默认 true,与 MCP 侧 --unattended 同口径。 */
27
27
  unattended: z.boolean().default(true),
28
+ /**
29
+ * 子代理委派规格(工具名 + 引用的 preset id),由构建期写入 patch。
30
+ * 委派工具不写成静态 patch 行:DSH 加载器并行激活同层条目,静态工具行会在 preset 注册前
31
+ * 校验 `agentPreset` 报 Unknown agent preset;改由插件在 preset 就绪后动态挂载。
32
+ */
33
+ subagents: z
34
+ .array(
35
+ z.object({
36
+ toolName: z.string(),
37
+ agentPreset: z.string(),
38
+ }),
39
+ )
40
+ .default([]),
28
41
  })
29
42
 
30
43
  /** cordis 插件名。 */
@@ -39,7 +52,10 @@ const inject = ["tools"]
39
52
  * worktree 每次调用从 `exec.agent.session.header.cwd` 取会话项目目录(该字段缺失即报错,不静默回落进程 cwd);
40
53
  * `exec.signal` 在每次调用入口检查取消;调用者身份由 `_agent` 入参声明(缺省=编排主代理视角)。
41
54
  */
42
- function apply(ctx: any, config: { unattended?: boolean } = {}): void {
55
+ function apply(
56
+ ctx: any,
57
+ config: { unattended?: boolean; subagents?: Array<{ toolName: string; agentPreset: string }> } = {},
58
+ ): void {
43
59
  const unattended = config.unattended !== false
44
60
  // 进程 cwd 仅用于「无 agent 上下文的非 agent 调用」兜底;agent 调用缺会话目录时工具直接报错。
45
61
  const processCwd = process.cwd()
@@ -56,6 +72,70 @@ function apply(ctx: any, config: { unattended?: boolean } = {}): void {
56
72
  }),
57
73
  )
58
74
  }
75
+ mountSubagentTools(ctx, config.subagents ?? [])
76
+ }
77
+
78
+ /** 子代理委派工具所属的官方包(保持 external,由 DSH 安装目录在运行时解析)。 */
79
+ const SUBAGENT_TOOL_PACKAGE = "@deepseek-ai/dsh-tool-subagent"
80
+
81
+ /** 等待 preset 声明注册完成的上限:注册在启动阶段完成,正常在毫秒级。 */
82
+ const PRESET_READY_TIMEOUT_MS = 3000
83
+ const PRESET_READY_POLL_MS = 50
84
+
85
+ /**
86
+ * 轮询等待 preset 声明注册完成。
87
+ *
88
+ * `agentPresets.readDocument(id)` 直接读内存声明表:返回成功即表示 preset 行已激活,
89
+ * 此时再挂载委派工具,工具行的加载期校验必然通过。
90
+ */
91
+ async function waitForPreset(registry: any, presetId: string, timeoutMs = PRESET_READY_TIMEOUT_MS): Promise<boolean> {
92
+ const deadline = Date.now() + timeoutMs
93
+ while (Date.now() < deadline) {
94
+ try {
95
+ await registry.readDocument(presetId)
96
+ return true
97
+ } catch {
98
+ await new Promise((resolve) => setTimeout(resolve, PRESET_READY_POLL_MS))
99
+ }
100
+ }
101
+ return false
102
+ }
103
+
104
+ /**
105
+ * 在 preset 就绪后动态挂载子代理委派工具。
106
+ *
107
+ * 与静态 patch 行的差异只有挂载时机:本函数先等 `agentPresets` 服务可用、再逐个确认 preset
108
+ * 已注册,最后才挂 `@deepseek-ai/dsh-tool-subagent` 实例——消除「工具行先于 preset 行激活」
109
+ * 的加载竞态(DSH 对同层条目并行激活,desktop 多插件下稳定触发)。
110
+ */
111
+ function mountSubagentTools(ctx: any, specs: Array<{ toolName: string; agentPreset: string }>): void {
112
+ if (specs.length === 0 || typeof ctx.inject !== "function") return
113
+ ctx.inject(["agentPresets"], async (injected: any) => {
114
+ let subagentTool: any
115
+ try {
116
+ subagentTool = await import(SUBAGENT_TOOL_PACKAGE)
117
+ } catch (error) {
118
+ injected.logger?.warn?.(`openspec: 无法加载 ${SUBAGENT_TOOL_PACKAGE},子代理委派工具未挂载:${String(error)}`)
119
+ return
120
+ }
121
+ for (const spec of specs) {
122
+ if (!(await waitForPreset(injected.agentPresets, spec.agentPreset))) {
123
+ injected.logger?.warn?.(
124
+ `openspec: agent preset "${spec.agentPreset}" 未在 ${PRESET_READY_TIMEOUT_MS}ms 内注册,跳过子代理工具 ${spec.toolName}`,
125
+ )
126
+ continue
127
+ }
128
+ try {
129
+ await injected.plugin(subagentTool, {
130
+ provider: "spawn",
131
+ toolName: spec.toolName,
132
+ agentPreset: spec.agentPreset,
133
+ })
134
+ } catch (error) {
135
+ injected.logger?.warn?.(`openspec: 子代理工具 ${spec.toolName} 挂载失败:${String(error)}`)
136
+ }
137
+ }
138
+ })
59
139
  }
60
140
 
61
141
  export { Config, apply, inject, name }
@@ -154,9 +154,27 @@ declare module "@deepseek-ai/dsh-tools" {
154
154
  }
155
155
 
156
156
  declare module "@deepseek-ai/cordis" {
157
- /** 本项目插件只用到 ctx.tools;其余 cordis 服务不消费。 */
157
+ /** 本项目插件用到的 cordis 能力:工具注册表、依赖注入与子插件挂载;其余服务不消费。 */
158
158
  export interface Context {
159
159
  tools: import("@deepseek-ai/dsh-tools").ToolRegistry
160
+ /**
161
+ * 依赖就绪后执行回调。本插件用它等 `agentPresets` 服务可用,再动态挂载子代理委派工具
162
+ * (DSH 对同层 patch 条目并行激活,静态工具行会先于 preset 声明校验而失败)。
163
+ */
164
+ inject(deps: readonly string[], callback: (ctx: Context) => unknown): unknown
165
+ /** 挂载子插件。本插件用它挂载官方 `@deepseek-ai/dsh-tool-subagent` 实例。 */
166
+ plugin(plugin: unknown, config?: unknown): unknown
160
167
  [key: string]: any
161
168
  }
169
+ }
170
+
171
+ declare module "@deepseek-ai/dsh-tool-subagent" {
172
+ /**
173
+ * 官方子代理委派工具插件(cordis 插件形态:命名导出 name/inject/Config/apply)。
174
+ * 本插件在 preset 就绪后动态挂载它,故只需声明挂载所需的公开形状。
175
+ */
176
+ export const name: string
177
+ export const inject: readonly string[]
178
+ export const Config: unknown
179
+ export function apply(ctx: unknown, config?: unknown): void
162
180
  }