dsh-plugin-t-expert 0.2.7 → 0.2.10
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/README.md +67 -15
- package/THIRD-PARTY-NOTICES +16 -8
- package/data/experts/engineering/engineering-deepseek-harness-project-expert.md +256 -0
- package/data/source.json +2 -2
- package/data/t-team.config.json +16 -1
- package/data/team-profiles.py +80 -78
- package/data/teams.json +103 -47
- package/data/teams.resolved.json +17 -1
- package/data/zh/COVERAGE.json +15 -14
- package/data/zh/descriptions.json +2 -1
- package/data/zh/names.json +2 -1
- package/lib/bootstrap.js +1 -0
- package/lib/catalog.js +155 -32
- package/lib/client.js +132 -128
- package/lib/command.js +22 -6
- package/lib/i18n.js +78 -11
- package/lib/index.js +492 -115
- package/lib/plan-check.js +50 -57
- package/lib/remote-schemas.js +156 -0
- package/lib/remote.js +111 -148
- package/lib/skill.js +107 -44
- package/lib/squads.js +195 -28
- package/lib/teams/assignee-contract.js +47 -0
- package/lib/teams/harness-compat.js +36 -0
- package/lib/teams/index.js +11 -3
- package/lib/teams/members.js +6 -3
- package/lib/teams/quality-gates.js +24 -1
- package/lib/teams/state.js +14 -2
- package/lib/teams/tools.js +44 -21
- package/package.json +18 -30
- package/skills/dsh-harness-languages/SKILL.md +175 -0
- package/skills/dsh-harness-project/SKILL.md +209 -0
- package/skills/t-expert-manager/SKILL.md +15 -5
- package/skills/t-expert-manager/references/ops-reference.md +4 -4
- package/vendor/third-party-licenses/README.md +1 -1
package/lib/command.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
/**
|
|
2
3
|
* `/t` —— T专家 的小队短命令。
|
|
3
4
|
*
|
|
@@ -161,22 +162,37 @@ export function renderTeamList(profiles) {
|
|
|
161
162
|
|
|
162
163
|
/**
|
|
163
164
|
* 注册 `/t`。
|
|
164
|
-
* @param ctx
|
|
165
|
-
* @param options
|
|
166
|
-
* @param options.
|
|
165
|
+
* @param ctx 宿主上下文(需要 `effect` / `logger` 等完整能力)
|
|
166
|
+
* @param {object} options 注册选项
|
|
167
|
+
* @param {string} options.teamsFile teams.json 路径(通常与名册同目录)
|
|
168
|
+
* @param {() => boolean} options.hasEngine 探测内置团队引擎是否在位的函数
|
|
169
|
+
* @param {() => string} [options.engineError] 引擎不可用时的原因(报给用户)
|
|
170
|
+
* @param {object} [options.commands] 已解析好的 commands 服务(可选服务的时序修复,见下)
|
|
167
171
|
*/
|
|
168
172
|
export function registerTeamCommand(ctx, options) {
|
|
169
173
|
const teamsFile = options.teamsFile;
|
|
170
174
|
const hasEngine = options.hasEngine;
|
|
171
|
-
|
|
175
|
+
// ⚠️ 2026-09-13:`commands` 是**可选服务**,真实宿主里它**晚于本插件 apply** 才就绪;
|
|
176
|
+
// 而 `ctx.get("commands")` 在那一刻是 undefined,且 cordis 的 `provide` **不回溯通知**已跑过的代码
|
|
177
|
+
// —— 于是 `/t` 永远不会注册。调用方已用 `ctx.inject(["commands"], …)` 等到就绪后把服务传进来;
|
|
178
|
+
// 这里再兜底懒查一次(直连调用/测试),两条路都不做属性访问。
|
|
179
|
+
const commands = options.commands ?? ctx.get?.("commands");
|
|
180
|
+
if (typeof commands?.register !== "function") {
|
|
181
|
+
ctx.logger?.warn?.("[t-team] 没有可用的 commands 服务:/t 命令不注册。");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
ctx.effect(() => commands.register({
|
|
172
185
|
name: COMMAND_NAME,
|
|
173
186
|
description: "用 T专家 小队执行一个目标(底层走 T专家 内置的多智能体团队引擎)",
|
|
174
187
|
input: { hint: "<小队名|别名> <目标>" },
|
|
175
188
|
handler(invocation) {
|
|
176
189
|
if (hasEngine() !== true) {
|
|
177
190
|
const detail = typeof options.engineError === "function" ? options.engineError() : "";
|
|
191
|
+
// 中性表述:不指路日志。旧文案让用户去看 `[t-team]` error 行,但「引擎已调度、工具却没注册」
|
|
192
|
+
// 这条路径下那句话**永远不会打印**(lib/index.js 的 engineReady 探到假只会补 warn),
|
|
193
|
+
// 于是指引恒定失效(N-5)。
|
|
178
194
|
const text = detail === ""
|
|
179
|
-
? "团队引擎未就绪:T专家
|
|
195
|
+
? "团队引擎未就绪:T专家 内置的团队引擎没有注册成功(它的工具没有出现在工具表里)。"
|
|
180
196
|
: `团队引擎挂载失败:${detail}`;
|
|
181
197
|
// 命令结果行在部分 DSH 版本不渲染,所以连错误也交给队长转述一次。
|
|
182
198
|
invocation.agent.followup(createUserMessage({
|
|
@@ -225,4 +241,4 @@ export function registerTeamCommand(ctx, options) {
|
|
|
225
241
|
};
|
|
226
242
|
},
|
|
227
243
|
}), "t-team: /t command");
|
|
228
|
-
}
|
|
244
|
+
}
|
package/lib/i18n.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
/** T专家 host 侧文案与列表/结果渲染。 */
|
|
2
3
|
|
|
3
4
|
export const NS = "t-team";
|
|
@@ -20,6 +21,8 @@ const ZH = {
|
|
|
20
21
|
"error.specsTooMany": "一次最多召唤 {limit} 位专家",
|
|
21
22
|
"error.specInvalid": "experts[{index}] 需要 expert 与 task 两个字段",
|
|
22
23
|
"error.settingsMissing": "T专家 设置段尚未注册",
|
|
24
|
+
"error.settingsServiceMissing": "宿主没有 settings 服务:T专家 的「专家启停」不可用(名册与召唤相关能力需要它记录已启用专家)。这是宿主装配问题,不是配置错误。",
|
|
25
|
+
"error.customUnknownSlug": "未知专家 slug:{slugs}(这些 slug 不在当前名册里;先用 list_t_experts 取准确的 slug)",
|
|
23
26
|
"error.customSlugInvalid": "slug 只能用 a-z、0-9 与连字符,且以字母或数字开头:\"{slug}\"",
|
|
24
27
|
"error.customSlugTaken": "slug「{slug}」已被内置或自建专家占用",
|
|
25
28
|
"error.customDivisionInvalid": "分类目录名只能用 a-z、0-9、连字符与点,且以字母或数字开头(中文请填在「显示名」里):\"{division}\"",
|
|
@@ -65,6 +68,8 @@ const EN = {
|
|
|
65
68
|
"error.specsTooMany": "At most {limit} experts per call",
|
|
66
69
|
"error.specInvalid": "experts[{index}] requires both expert and task",
|
|
67
70
|
"error.settingsMissing": "T Expert settings section is not registered yet",
|
|
71
|
+
"error.settingsServiceMissing": "The host has no settings service: T Expert's enable/disable state is unavailable (the roster and summoning need it to remember which experts are enabled). This is a host wiring problem, not a configuration error.",
|
|
72
|
+
"error.customUnknownSlug": "Unknown expert slug(s): {slugs} (not in the current roster; call list_t_experts for exact slugs)",
|
|
68
73
|
"error.customSlugInvalid": "slug may only use a-z, 0-9 and hyphens, starting with a letter or digit: \"{slug}\"",
|
|
69
74
|
"error.categoryOfficial": "\"{division}\" is an official division: its name and existence follow the built-in roster sync, so only custom divisions can be changed",
|
|
70
75
|
"error.categoryExists": "Division \"{division}\" already exists",
|
|
@@ -128,8 +133,49 @@ export function localized(expert, locale) {
|
|
|
128
133
|
};
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
136
|
+
/**
|
|
137
|
+
* 把执行期名册分组投影成渲染期就绪的形状。
|
|
138
|
+
*
|
|
139
|
+
* 为什么在这里做:`output.render(args, value)` 必须是 `(args, value)` 的纯函数
|
|
140
|
+
* ——同一个 canonical value 在会话日志里回放时必须渲染出同一段文本。而「用哪种语言」
|
|
141
|
+
* 是**执行期**才知道的事实(读宿主 locale 设置),所以它必须和分组一起进 canonical value,
|
|
142
|
+
* 不能在 render 里再读一次宿主。
|
|
143
|
+
*
|
|
144
|
+
* @param groups - `groupByDivision()` 的分组(带 label/labelEn 与 expert 条目)。
|
|
145
|
+
* @param locale - 执行期语言("zh" | "en")。
|
|
146
|
+
* @returns 只含渲染所需字段的分组数组。
|
|
147
|
+
*/
|
|
148
|
+
export function toRenderableGroups(groups, locale) {
|
|
149
|
+
return (Array.isArray(groups) ? groups : []).map((group) => ({
|
|
150
|
+
division: group.division,
|
|
151
|
+
label: locale === "en" ? (group.labelEn ?? group.label ?? group.division) : (group.label ?? group.division),
|
|
152
|
+
count: group.count ?? (group.experts ?? []).length,
|
|
153
|
+
experts: (group.experts ?? []).map((expert) => {
|
|
154
|
+
const { name, description } = localized(expert, locale);
|
|
155
|
+
return {
|
|
156
|
+
slug: expert.slug,
|
|
157
|
+
emoji: expert.emoji ?? "",
|
|
158
|
+
// 渲染期不再读侧车译文:名字与简介在执行期就按 locale 定好。
|
|
159
|
+
name,
|
|
160
|
+
description,
|
|
161
|
+
};
|
|
162
|
+
}),
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* list_t_experts 的文本渲染(纯函数)。
|
|
168
|
+
*
|
|
169
|
+
* 语言只从 `value.locale` 取,不读宿主设置:同一个 `value` 永远渲染出同一段文本。
|
|
170
|
+
* @param args - 工具入参(只用来取 division 过滤词)。
|
|
171
|
+
* @param value - `list_t_experts.execute` 返回的 canonical value。
|
|
172
|
+
*/
|
|
173
|
+
export function renderList(_args, value) {
|
|
174
|
+
const query = typeof value.query === "string" ? value.query : "";
|
|
175
|
+
const locale = value.locale === "en" ? "en" : "zh";
|
|
176
|
+
const groups = Array.isArray(value.divisions) ? value.divisions : [];
|
|
177
|
+
const total = typeof value.total === "number" ? value.total : 0;
|
|
178
|
+
const allDivisions = Array.isArray(value.allDivisions) ? value.allDivisions : [];
|
|
133
179
|
if (query !== "" && groups.length === 0) {
|
|
134
180
|
return t(locale, "list.unknownDivision", { query, divisions: allDivisions.join(", ") });
|
|
135
181
|
}
|
|
@@ -146,23 +192,44 @@ export function renderList(locale, { query, groups, total, allDivisions }) {
|
|
|
146
192
|
} else {
|
|
147
193
|
const group = groups[0];
|
|
148
194
|
lines.push(t(locale, "list.headerOne", { division: `${group.label} (${group.division})`, count: group.count }));
|
|
149
|
-
for (const expert of group.experts) {
|
|
150
|
-
const { name, description } = localized(expert, locale);
|
|
195
|
+
for (const expert of group.experts ?? []) {
|
|
151
196
|
// 带上 slug:模型可以直接拿它当召唤参数,避免只靠名字解析
|
|
152
|
-
lines.push(`- ${expert.emoji} ${name} (${expert.slug}) — ${description}`);
|
|
197
|
+
lines.push(`- ${expert.emoji} ${expert.name} (${expert.slug}) — ${expert.description}`);
|
|
153
198
|
}
|
|
154
199
|
}
|
|
155
200
|
return lines.join("\n");
|
|
156
201
|
}
|
|
157
202
|
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
203
|
+
/**
|
|
204
|
+
* 把一批召唤结果投影成渲染期就绪的形状。
|
|
205
|
+
* @param results - `summon_t_experts` 的结果项;`expert` 已是**执行期按 locale 定好的显示名**
|
|
206
|
+
* (`summon_t_experts.execute` 负责解析,render 不再碰名册)。
|
|
207
|
+
* @param locale - 执行期语言(写进 value,供 render 取文案)。
|
|
208
|
+
*/
|
|
209
|
+
export function toRenderableSummonResults(results, locale) {
|
|
210
|
+
return (Array.isArray(results) ? results : []).map((item) => ({
|
|
211
|
+
expert: typeof item?.expert === "string" ? item.expert : "",
|
|
212
|
+
ok: item?.ok === true,
|
|
213
|
+
answer: item?.answer ?? "",
|
|
214
|
+
error: item?.error ?? "",
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* summon_t_experts 的文本渲染(纯函数)。
|
|
220
|
+
* 语言只从 `value.locale` 取,专家名来自 `value.results[].expert`(执行期已本地化)。
|
|
221
|
+
* @param args - 工具入参(未使用)。
|
|
222
|
+
* @param value - `summon_t_experts.execute` 返回的 canonical value。
|
|
223
|
+
*/
|
|
224
|
+
export function renderSummonResults(_args, value) {
|
|
225
|
+
const locale = value.locale === "en" ? "en" : "zh";
|
|
226
|
+
const results = Array.isArray(value.results) ? value.results : [];
|
|
227
|
+
const ok = results.filter((item) => item.ok === true).length;
|
|
161
228
|
const lines = [t(locale, "summon.batchHeader", { total: results.length, ok, failed: results.length - ok })];
|
|
162
229
|
for (const item of results) {
|
|
163
|
-
lines.push(item.ok
|
|
164
|
-
? t(locale, "summon.itemOk", { expert: item.expert, answer: item.answer })
|
|
165
|
-
: t(locale, "summon.itemFail", { expert: item.expert, error: item.error ?? "" }));
|
|
230
|
+
lines.push(item.ok === true
|
|
231
|
+
? t(locale, "summon.itemOk", { expert: item.expert ?? "", answer: item.answer ?? "" })
|
|
232
|
+
: t(locale, "summon.itemFail", { expert: item.expert ?? "", error: item.error ?? "" }));
|
|
166
233
|
}
|
|
167
234
|
return lines.join("\n\n");
|
|
168
235
|
}
|