@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.
- package/.dsh-plugin/opx-tools.mjs +49 -1
- package/.mcp-server/cli.mjs +73 -19
- package/README.md +2 -0
- package/assets/skills/orchestrator/SKILL.md +9 -1
- package/dsh/cordis.patch.yml +317 -224
- package/package.json +1 -1
- package/src/adapters/deepseek-harness/index.ts +150 -33
- package/src/adapters/deepseek-harness/native-tools.ts +3 -3
- package/src/adapters/deepseek-harness/plugin.ts +81 -1
- package/src/types/dsh-runtime.d.ts +20 -2
|
@@ -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,
|
package/.mcp-server/cli.mjs
CHANGED
|
@@ -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
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
4007
|
-
|
|
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 ||
|
|
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.
|
|
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 的规范自行裁决,**不向用户提问**:
|