@ifrankwang/openspec-agents 0.149.1 → 0.150.1

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,
@@ -3284,6 +3284,37 @@ var require_utils = __commonJS(function(exports, module) {
3284
3284
  }
3285
3285
  return output.join("");
3286
3286
  }
3287
+ function removeDotSegmentsRelative(path) {
3288
+ if (path === "" || path[0] === "/") {
3289
+ return removeDotSegments(path);
3290
+ }
3291
+ const segments = path.split("/");
3292
+ const last = segments.length - 1;
3293
+ const output = [];
3294
+ let trailingDirectory = false;
3295
+ for (let i = 0;i <= last; i++) {
3296
+ const segment = segments[i];
3297
+ if (segment === "." || segment === "..") {
3298
+ if (segment === "..") {
3299
+ if (output.length !== 0 && output[output.length - 1] !== "..") {
3300
+ output.pop();
3301
+ } else {
3302
+ output.push("..");
3303
+ }
3304
+ }
3305
+ trailingDirectory = i === last;
3306
+ } else {
3307
+ output.push(segment);
3308
+ }
3309
+ }
3310
+ if (trailingDirectory && output[output.length - 1] !== "..") {
3311
+ output.push("");
3312
+ }
3313
+ if (output.length === 0 || output[0] === "" || output[0].includes(":")) {
3314
+ output.unshift(".");
3315
+ }
3316
+ return output.join("/");
3317
+ }
3287
3318
  var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
3288
3319
  var HOST_DELIM_RE = /[@/?#:]/g;
3289
3320
  var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
@@ -3544,6 +3575,7 @@ var require_utils = __commonJS(function(exports, module) {
3544
3575
  encodeFragment,
3545
3576
  escapePreservingEscapes,
3546
3577
  removeDotSegments,
3578
+ removeDotSegmentsRelative,
3547
3579
  isIPv4,
3548
3580
  isUUID,
3549
3581
  normalizeIPv6,
@@ -3728,16 +3760,18 @@ var require_schemes = __commonJS(function(exports, module) {
3728
3760
 
3729
3761
  // node_modules/fast-uri/index.js
3730
3762
  var require_fast_uri = __commonJS(function(exports, module) {
3731
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3763
+ var { normalizeIPv6, removeDotSegments, removeDotSegmentsRelative, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3732
3764
  var { SCHEMES, getSchemeHandler } = require_schemes();
3733
3765
  var VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
3734
3766
  var MALFORMED_SCHEME_ERROR = "URI scheme is malformed.";
3735
- function decodeValidScheme(scheme) {
3736
- const decodedScheme = unescape(String(scheme));
3737
- if (!VALID_SCHEME.test(decodedScheme)) {
3767
+ var MALFORMED_HOST_ERROR = "URI host is malformed.";
3768
+ var FORBIDDEN_DOMAIN_HOST_CHARACTER = /[\0-\x20"#/:<>?@[\\\]^`{|}\x7F]/u;
3769
+ function validateScheme(scheme) {
3770
+ const stringScheme = String(scheme);
3771
+ if (!VALID_SCHEME.test(stringScheme)) {
3738
3772
  throw new TypeError(MALFORMED_SCHEME_ERROR);
3739
3773
  }
3740
- return decodedScheme;
3774
+ return stringScheme;
3741
3775
  }
3742
3776
  function normalize(uri, options) {
3743
3777
  if (typeof uri === "string") {
@@ -3772,6 +3806,9 @@ var require_fast_uri = __commonJS(function(exports, module) {
3772
3806
  const resolvedSchemeHandler = getSchemeHandler(options && options.scheme || resolved.scheme);
3773
3807
  const resolvedHost = resolved.host;
3774
3808
  const resolvedHostIsIP = resolvedHost !== undefined && resolvedHost !== "" && (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6);
3809
+ if (hasForbiddenDomainHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)) {
3810
+ throw new Error(MALFORMED_HOST_ERROR);
3811
+ }
3775
3812
  canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP);
3776
3813
  const encodedASCIIHost = resolvedHost && resolvedHost.indexOf("%") !== -1 && !/\P{ASCII}/u.test(resolvedHost);
3777
3814
  if (resolved.error && !encodedASCIIHost) {
@@ -3813,14 +3850,15 @@ var require_fast_uri = __commonJS(function(exports, module) {
3813
3850
  if (relative.path[0] === "/") {
3814
3851
  target.path = removeDotSegments(relative.path);
3815
3852
  } else {
3816
- if ((base.userinfo !== undefined || base.host !== undefined || base.port !== undefined) && !base.path) {
3853
+ const baseHasAuthority = base.userinfo !== undefined || base.host !== undefined || base.port !== undefined;
3854
+ if (baseHasAuthority && !base.path) {
3817
3855
  target.path = "/" + relative.path;
3818
3856
  } else if (!base.path) {
3819
3857
  target.path = relative.path;
3820
3858
  } else {
3821
3859
  target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
3822
3860
  }
3823
- target.path = removeDotSegments(target.path);
3861
+ target.path = base.scheme === undefined && !baseHasAuthority && target.path[0] !== "/" ? removeDotSegmentsRelative(target.path) : removeDotSegments(target.path);
3824
3862
  }
3825
3863
  target.query = relative.query;
3826
3864
  }
@@ -3858,13 +3896,14 @@ var require_fast_uri = __commonJS(function(exports, module) {
3858
3896
  const options = Object.assign({}, opts);
3859
3897
  const uriTokens = [];
3860
3898
  if (component.scheme) {
3861
- component.scheme = decodeValidScheme(component.scheme);
3899
+ component.scheme = validateScheme(component.scheme);
3862
3900
  }
3863
3901
  const schemeHandler = getSchemeHandler(options.scheme || component.scheme);
3864
3902
  if (schemeHandler && schemeHandler.serialize)
3865
3903
  schemeHandler.serialize(component, options);
3866
3904
  const hasAuthority = component.userinfo !== undefined || component.host !== undefined || component.port !== undefined;
3867
- const pathNoScheme = !options.skipEscape && component.scheme === undefined && !hasAuthority;
3905
+ const schemelessReference = (component.scheme === undefined || options.reference === "suffix") && !hasAuthority;
3906
+ const pathNoScheme = !options.skipEscape && schemelessReference;
3868
3907
  if (component.path !== undefined) {
3869
3908
  if (!options.skipEscape) {
3870
3909
  component.path = serializePathEncoding(component.path, pathNoScheme);
@@ -3873,7 +3912,7 @@ var require_fast_uri = __commonJS(function(exports, module) {
3873
3912
  }
3874
3913
  }
3875
3914
  if (options.reference !== "suffix" && component.scheme) {
3876
- component.scheme = decodeValidScheme(component.scheme);
3915
+ component.scheme = validateScheme(component.scheme);
3877
3916
  uriTokens.push(component.scheme, ":");
3878
3917
  }
3879
3918
  const authority = recomposeAuthority(component);
@@ -3889,9 +3928,9 @@ var require_fast_uri = __commonJS(function(exports, module) {
3889
3928
  if (component.path !== undefined) {
3890
3929
  let s = component.path;
3891
3930
  if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) {
3892
- s = removeDotSegments(s);
3931
+ s = schemelessReference && s[0] !== "/" ? removeDotSegmentsRelative(s) : removeDotSegments(s);
3893
3932
  }
3894
- if (pathNoScheme) {
3933
+ if (schemelessReference) {
3895
3934
  s = serializePathEncoding(s, true);
3896
3935
  }
3897
3936
  if (authority === undefined && s[0] === "/" && s[1] === "/") {
@@ -3939,16 +3978,32 @@ var require_fast_uri = __commonJS(function(exports, module) {
3939
3978
  return hasMalformedPercentEncoding(matches[3]) || host !== undefined && !isIPLiteral(host) && hasMalformedPercentEncoding(host) || hasMalformedPercentEncoding(matches[6]) || hasMalformedPercentEncoding(matches[7]) || hasMalformedPercentEncoding(matches[8]);
3940
3979
  }
3941
3980
  function canonicalizeHost(parsed, options, schemeHandler, isIP) {
3981
+ if (hasForbiddenDomainHost(parsed, options, schemeHandler, isIP)) {
3982
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
3983
+ return true;
3984
+ }
3942
3985
  if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
3986
+ let url;
3943
3987
  try {
3944
- parsed.host = new URL("http://" + parsed.host).hostname;
3988
+ url = new URL("http://" + parsed.host);
3945
3989
  } catch (e) {
3946
3990
  parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
3947
3991
  return true;
3948
3992
  }
3993
+ if (!isHostOnlyURL(url)) {
3994
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
3995
+ return true;
3996
+ }
3997
+ parsed.host = url.hostname;
3949
3998
  }
3950
3999
  return false;
3951
4000
  }
4001
+ function hasForbiddenDomainHost(parsed, options, schemeHandler, isIP) {
4002
+ return Boolean(parsed.host && isIP === false && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && FORBIDDEN_DOMAIN_HOST_CHARACTER.test(parsed.host));
4003
+ }
4004
+ function isHostOnlyURL(url) {
4005
+ return url.username === "" && url.password === "" && url.port === "" && url.pathname === "/" && url.search === "" && url.hash === "" && url.hostname !== "";
4006
+ }
3952
4007
  function parseWithStatus(uri, opts) {
3953
4008
  const options = Object.assign({}, opts);
3954
4009
  const parsed = {
@@ -4003,9 +4058,8 @@ var require_fast_uri = __commonJS(function(exports, module) {
4003
4058
  parsed.query = matches[7];
4004
4059
  parsed.fragment = matches[8];
4005
4060
  if (parsed.scheme !== undefined) {
4006
- const decodedScheme = unescape(parsed.scheme);
4007
- if (VALID_SCHEME.test(decodedScheme)) {
4008
- parsed.scheme = decodedScheme.toLowerCase();
4061
+ if (VALID_SCHEME.test(parsed.scheme)) {
4062
+ parsed.scheme = parsed.scheme.toLowerCase();
4009
4063
  } else {
4010
4064
  parsed.error = parsed.error || MALFORMED_SCHEME_ERROR;
4011
4065
  malformedScheme = true;
@@ -4033,7 +4087,7 @@ var require_fast_uri = __commonJS(function(exports, module) {
4033
4087
  malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true);
4034
4088
  parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
4035
4089
  if (malformedIPLiteral) {
4036
- parsed.error = parsed.error || "URI host is malformed.";
4090
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
4037
4091
  malformedAuthorityOrPort = true;
4038
4092
  }
4039
4093
  } else {
@@ -4061,7 +4115,7 @@ var require_fast_uri = __commonJS(function(exports, module) {
4061
4115
  if (!isIP) {
4062
4116
  host = normalizePercentEncoding(host.toLowerCase());
4063
4117
  }
4064
- parsed.host = reescapeHostDelimiters(host, isIP);
4118
+ parsed.host = isIP || options.scheme !== "null" ? reescapeHostDelimiters(host, isIP) : host;
4065
4119
  }
4066
4120
  if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4067
4121
  if (parsed.path) {
@@ -30575,7 +30629,7 @@ async function ensureDefaultUnattended(args, ctx) {
30575
30629
  }
30576
30630
  } catch {}
30577
30631
  }
30578
- var PKG_VERSION = "0.149.1";
30632
+ var PKG_VERSION = "0.150.1";
30579
30633
  function buildMcpServer(worktree, opts = {}) {
30580
30634
  const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION });
30581
30635
  for (const [name, spec] of Object.entries(TOOL_SPECS)) {
package/README.md CHANGED
@@ -84,6 +84,8 @@ dsh plugin --profile web add @ifrankwang/openspec-agents
84
84
 
85
85
  `web` 是 profile 名,换成你自己的(`~/.dsh/profiles/` 下的目录名)。装完重启该 profile;作用项目随当前会话所属项目自动确定,同一个 profile 下切换项目不必重装或改配置。编排工具需要在一个项目会话里使用。
86
86
 
87
+ 要求 DSH 0.2.1 及以上。子代理的角色提示词与工具集由 Agent Preset 承载,profile 需包含 web 应用 bundle(`desktop` / `web` 模板都满足)。子代理委派为异步:分派后等完成通知,再继续下一步。
88
+
87
89
  #### OpenCode
88
90
 
89
91
  要求 OpenCode 2.x(1.x 的插件接口不兼容)。
@@ -42,7 +42,7 @@ capabilities: ["orchestrator"]
42
42
 
43
43
  ## 行为准则
44
44
 
45
- - 每次子代理返回后,调用状态查询工具获取权威「下一步」指令并严格遵循;推进被拦时按工具给出的阻塞原因与修复建议处理,不自行推断其他动作
45
+ - 每次子代理返回后,调用状态查询工具获取权威「下一步」指令并严格遵循;推进被拦时按工具给出的阻塞原因与修复建议处理,不自行推断其他动作。DSH 形态下「返回」指收到子代理完成通知,不是分派工具的即时返回(见「分派范式」的 DSH 异步委派)
46
46
  - 分派/推进决策一律以工具返回为准:工具已给出明确分派指令时直接分派,不得先读状态文件交叉验证;仅门禁拒绝或工具给出修复建议时才读取状态文件核对
47
47
  - 分派子代理前先查询状态确认当前阶段/层,确保不跳阶段或错层分派;工具视图呈现 worktree 未就绪时不得分派
48
48
  - 不过度沟通:任务组内部不停下来向用户汇报,持续执行直到阻塞或完成
@@ -95,6 +95,14 @@ full 模式 verify_quality 以 5 个逻辑身份并行审查:同一物理审
95
95
 
96
96
  不包含任何 task/issue 明细、文件清单、执行边界具体值等动态内容——一切交给状态查询工具。
97
97
 
98
+ ### DSH 异步委派
99
+
100
+ DSH 0.2.1 起子代理委派为异步:`openspec_developer` / `openspec_reviewer` 调用后只返回子任务 id,子代理结束时会收到完成通知。
101
+
102
+ - 分派后不要立即查状态、不要重复分派同一角色;收到该子代理的完成通知后再查状态推进
103
+ - 并排分派的多个子代理各自完成、各自通知,全部到齐后再进入下一步
104
+ - 通知里的最终回答是子代理自述,权威状态仍以状态查询工具为准
105
+
98
106
  ## 无人值守行为约定
99
107
 
100
108
  无人值守模式下,所有需拍板事项由你依据工具返回与已加载 skill 的规范自行裁决,**不向用户提问**: