dsh-plugin-t-expert 0.1.1 → 0.2.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.
package/lib/i18n.js CHANGED
@@ -3,9 +3,9 @@
3
3
  export const NS = "t-team";
4
4
 
5
5
  const ZH = {
6
- "error.rootMissing": "T专家 专家目录不存在或无法访问:\"{root}\"。请运行 ~/.t-team/sync.sh 生成,或配置正确的 root。",
6
+ "error.rootMissing": "T专家 专家目录不存在或无法访问:\"{root}\"。插件首次启动会自动把自带名册播种到该目录;若你手工移动过数据目录,请把 root 指到正确位置。",
7
7
  "error.rootNotDir": "T专家 专家目录 \"{root}\" 不是目录",
8
- "error.catalogEmpty": "在 root \"{root}\" 下没有发现任何专家(*.md)。请确认已运行 sync.sh。",
8
+ "error.catalogEmpty": "在 root \"{root}\" 下没有发现任何专家(*.md)。删掉这个空目录让插件下次启动重新播种,或把 root 改到正确的名册目录。",
9
9
  "error.summonRequiresAgent": "当前上下文没有可用的 parent agent,无法召唤专家。",
10
10
  "error.providerMissing": "找不到子代理 provider:{provider}",
11
11
  "error.providerNoPersona": "provider {provider} 不支持 persona,无法注入专家人格。",
@@ -32,9 +32,9 @@ const ZH = {
32
32
  };
33
33
 
34
34
  const EN = {
35
- "error.rootMissing": "T Expert expert directory is missing or unreadable: \"{root}\". Run ~/.t-team/sync.sh or configure a valid root.",
35
+ "error.rootMissing": "T Expert expert directory is missing or unreadable: \"{root}\". The plugin seeds its bundled roster there on first start; point root at the right place if you moved the data directory.",
36
36
  "error.rootNotDir": "T Expert expert root \"{root}\" is not a directory",
37
- "error.catalogEmpty": "No experts (*.md) found under root \"{root}\". Run sync.sh first.",
37
+ "error.catalogEmpty": "No experts (*.md) found under root \"{root}\". Delete this empty directory so the plugin re-seeds on next start, or point root at the real roster.",
38
38
  "error.summonRequiresAgent": "No parent agent in this context; cannot summon an expert.",
39
39
  "error.providerMissing": "Subagent provider not found: {provider}",
40
40
  "error.providerNoPersona": "Provider {provider} does not support persona injection.",
package/lib/index.js CHANGED
@@ -34,9 +34,10 @@ import { createSquadService } from "./squads.js";
34
34
  import { checkPlan } from "./plan-check.js";
35
35
  import { findTeamByCaptain } from "./teams/state.js";
36
36
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
37
- // 路线 A:上游团队引擎已 vendor 进 lib/teams/(由 scripts/brand.mjs 从 vendor/dsh-agent-teams/upstream/
38
- // 改名生成:命令 /agent-teams /t、工具 agent_teams_* → t_team_*),这里在 T专家 内部挂载它 ——
39
- // 安装 T专家 即自带团队引擎与团队面板,不再需要单独安装上游插件。
37
+ // 内置团队引擎:随插件一起发布的品牌化产物在 lib/teams/ 下(由 scripts/brand.mjs 从
38
+ // vendor/ 里的上游镜像改名生成),这里在 T专家 内部挂载它 ——
39
+ // 安装 T专家 即自带团队引擎与团队面板,不再需要单独安装引擎插件。
40
+ // 上游来源、版本与许可见 THIRD-PARTY-NOTICES 与 vendor/。
40
41
  import * as teamsEngine from "./teams/index.js";
41
42
  // 引擎自带的 /t 命令不注册(否则与 T专家 自己的 /t 撞名),但手势边界必须复用:
42
43
  // 它把 `/t --profile <小队> <目标>` 这条用户消息翻译成队长协议指令。
@@ -48,7 +49,14 @@ import { haltTeamWork } from "./teams/tools.js";
48
49
  export const name = NS;
49
50
  export const inject = ["tools", "subagents", "systemPrompt", "settings", "commands"];
50
51
 
51
- /** 插件配置;root 默认指向 T专家 的固定资产目录。 */
52
+ /**
53
+ * 插件配置。
54
+ *
55
+ * 规范要求「部署可能取不同值的参数都必须是可被 cordis.yml 覆盖的 Config 字段」,
56
+ * 所以召唤上限/并发、任务长度上限、简介截断长度,以及团队引擎的
57
+ * stateDir / memberProvider / memberModel / maxMembers 与引擎配置路径都在这里;
58
+ * 模块级常量只保留协议常量与固定清单。
59
+ */
52
60
  export const Config = schema.object({
53
61
  root: schema.string().default(join(homedir(), ".t-team", "experts")),
54
62
  zhRoot: schema.string().default(join(homedir(), ".t-team", "zh")),
@@ -56,6 +64,24 @@ export const Config = schema.object({
56
64
  /** 留空 = 自动扫描 root 下所有含 .md 的分区目录(上游新增分区不必改配置/代码)。 */
57
65
  divisions: schema.array(schema.string()).default([]),
58
66
  maxDepth: schema.natural(),
67
+ /** 一次 summon_t_experts 最多召唤几位专家。 */
68
+ maxSummonBatch: schema.natural().min(1).default(8),
69
+ /** summon_t_experts 的并发度。 */
70
+ summonConcurrency: schema.natural().min(1).default(4),
71
+ /** 单个专家任务的最大字符数。 */
72
+ summonTaskMaxChars: schema.natural().min(1).default(8000),
73
+ /** list_t_experts 里简介的截断长度。 */
74
+ descriptionLimit: schema.natural().min(1).default(120),
75
+ /** 团队引擎配置(team-profiles.py 的产物);留空则取 root 同级目录下的 t-team.config.json。 */
76
+ engineConfig: schema.string(),
77
+ /** 引擎的团队状态目录(工作区内,相对路径)。 */
78
+ stateDir: schema.string().default(".agent-teams"),
79
+ /** 队员子代理使用的 provider 名。 */
80
+ memberProvider: schema.string().default("spawn"),
81
+ /** 队员默认模型;留空 = 跟随队长。 */
82
+ memberModel: schema.string(),
83
+ /** 一支团队最多几名成员。 */
84
+ maxMembers: schema.natural().min(1).default(8),
59
85
  });
60
86
 
61
87
  /** 供 remote 服务读取名册的服务名。 */
@@ -67,10 +93,6 @@ export const TEAM_SERVICE = "tTeamTeams";
67
93
  /** 设置命名空间(= 插件名)。 */
68
94
  export const SETTINGS_NAMESPACE = NS;
69
95
 
70
- const SUMMON_EXPERTS_MAX = 8;
71
- const SUMMON_EXPERTS_CONCURRENCY = 4;
72
- const SUMMON_TASK_MAX_CHARS = 8000;
73
- const DESCRIPTION_LIMIT = 120;
74
96
 
75
97
  const settingsSchema = schema.object({
76
98
  enabled: schema.array(schema.string()).default([]),
@@ -84,10 +106,12 @@ export function apply(ctx, config) {
84
106
  // 必须在下面同步读取 t-team.config.json / teams.json 之前完成,否则首次安装会读到空配置。
85
107
  const seeded = seedData({ root: config.root, zhRoot: config.zhRoot });
86
108
  if (seeded.copied.length > 0) {
87
- console.log(`[t-team] 已把包内名册快照播种到 ${dirname(config.root)}(${seeded.copied.join(", ")})`);
109
+ ctx.logger?.info?.(`[t-team] 已把包内名册快照播种到 ${dirname(config.root)}(${seeded.copied.join(", ")})`);
88
110
  }
89
111
  if (!seeded.ok) {
90
- console.warn(`[t-team] 名册快照播种失败,将按已有数据继续:${seeded.error}`);
112
+ // 播种失败不当作致命:名册目录若因此缺失,第一次读名册会在 ensureReady() 里**响亮地**抛出
113
+ // (error.rootMissing),那才是最早可解析点。这里只保证失败一定会出现在日志里。
114
+ ctx.logger?.error?.(`[t-team] 名册快照播种失败,将按已有数据继续:${seeded.error}`);
91
115
  }
92
116
 
93
117
  // ---- 设置段(T专家 自己的命名空间,与其它插件不冲突)----
@@ -115,19 +139,28 @@ export function apply(ctx, config) {
115
139
  let loadError = null;
116
140
 
117
141
  /**
118
- * 名册指纹:source.json 与各分区目录 + 中文侧车文件的 mtime。
119
- * sync.sh / sync-zh.py 每次都会写这些文件,所以上游更新后**无需重启**即可自动重载;
120
- * 目录 mtime 同时覆盖「新增/删除专家」。
142
+ * 名册指纹:source.json、各分区目录、以及中文侧车文件的 mtime。
143
+ *
144
+ * 要点:运行期读的中文正文路径是 `zh/<分区>/<slug>.md`(不是 `manual-bodies/`,那是
145
+ * 数据层的**输入**,由同步脚本合并后写进 `zh/<分区>/`)。所以 `zhRoot` 与每个
146
+ * `zh/<分区>/` 目录都必须进指纹 —— 否则「给某位专家**新增**一篇中文正文」这条最常见的
147
+ * 补译操作不会触发重载:该专家的 personaPathZh 一直是 undefined,召唤时只能读英文。
148
+ * (已在 verify 里用临时数据目录做过行为验证。)
121
149
  */
122
150
  async function fingerprint() {
123
151
  const mtimeOf = (path) => stat(path).then((info) => info.mtimeMs).catch(() => 0);
124
152
  const parts = await Promise.all([
125
153
  mtimeOf(config.root),
154
+ mtimeOf(config.zhRoot),
155
+ mtimeOf(join(config.zhRoot, "manual-bodies")),
126
156
  mtimeOf(join(dirname(config.root), "source.json")),
127
157
  mtimeOf(join(config.zhRoot, "names.json")),
128
158
  mtimeOf(join(config.zhRoot, "descriptions.json")),
129
159
  mtimeOf(join(config.zhRoot, "divisions.json")),
160
+ mtimeOf(join(config.zhRoot, "manual.json")),
130
161
  ...effectiveDivisions.map((division) => mtimeOf(join(config.root, division))),
162
+ ...effectiveDivisions.map((division) => mtimeOf(join(config.zhRoot, division))),
163
+ ...effectiveDivisions.map((division) => mtimeOf(join(config.zhRoot, "manual-bodies", division))),
131
164
  ]);
132
165
  return parts.join("|");
133
166
  }
@@ -219,8 +252,8 @@ export function apply(ctx, config) {
219
252
  const current = locale();
220
253
  const taskText = String(task ?? "").trim();
221
254
  if (taskText === "") throw new Error(t(current, "error.taskEmpty"));
222
- if ([...taskText].length > SUMMON_TASK_MAX_CHARS) {
223
- throw new Error(t(current, "error.taskTooLong", { limit: SUMMON_TASK_MAX_CHARS }));
255
+ if ([...taskText].length > config.summonTaskMaxChars) {
256
+ throw new Error(t(current, "error.taskTooLong", { limit: config.summonTaskMaxChars }));
224
257
  }
225
258
  if (exec.agent === undefined) throw new Error(t(current, "error.summonRequiresAgent"));
226
259
 
@@ -349,7 +382,7 @@ export function apply(ctx, config) {
349
382
  slug: expert.slug,
350
383
  name,
351
384
  emoji: expert.emoji,
352
- description: truncate(description, DESCRIPTION_LIMIT),
385
+ description: truncate(description, config.descriptionLimit),
353
386
  };
354
387
  }),
355
388
  }));
@@ -386,7 +419,7 @@ export function apply(ctx, config) {
386
419
 
387
420
  ctx.tools.register(defineTool({
388
421
  name: "summon_t_experts",
389
- description: "Summon multiple T专家 experts in parallel for one mission. At most 8 experts run with concurrency 4; successful answers are returned even when some fail.",
422
+ description: `Summon multiple T专家 experts in parallel for one mission. At most ${config.maxSummonBatch} experts run with concurrency ${config.summonConcurrency}; successful answers are returned even when some fail.`,
390
423
  parameters: {
391
424
  experts: {
392
425
  type: "array",
@@ -418,15 +451,15 @@ export function apply(ctx, config) {
418
451
  const current = locale();
419
452
  const specs = Array.isArray(args.experts) ? args.experts : [];
420
453
  if (specs.length === 0) throw new Error(t(current, "error.specsEmpty"));
421
- if (specs.length > SUMMON_EXPERTS_MAX) {
422
- throw new Error(t(current, "error.specsTooMany", { limit: SUMMON_EXPERTS_MAX }));
454
+ if (specs.length > config.maxSummonBatch) {
455
+ throw new Error(t(current, "error.specsTooMany", { limit: config.maxSummonBatch }));
423
456
  }
424
457
  specs.forEach((spec, index) => {
425
458
  if (spec === null || typeof spec !== "object" || typeof spec.expert !== "string" || typeof spec.task !== "string") {
426
459
  throw new Error(t(current, "error.specInvalid", { index }));
427
460
  }
428
461
  });
429
- const results = await mapPool(specs, SUMMON_EXPERTS_CONCURRENCY, async (spec) => {
462
+ const results = await mapPool(specs, config.summonConcurrency, async (spec) => {
430
463
  try {
431
464
  const result = await runExpert(spec.expert, spec.task, exec);
432
465
  return { expert: result.expert, ok: true, answer: result.answer };
@@ -443,42 +476,81 @@ export function apply(ctx, config) {
443
476
  },
444
477
  }));
445
478
 
446
- // ---- 内置团队引擎(vendor 自上游并品牌化;配置来自 team-profiles.py 生成的 JSON)----
447
- // 配置文件名已随品牌改为 t-team.config.json,旧名 agent-teams.config.json 仍作为兜底读一次。
479
+ // ---- 内置团队引擎(vendor 自上游并品牌化;小队数据来自 team-profiles.py 生成的 JSON)----
480
+ // 配置路径与引擎标量都是 Config 字段(可从 cordis.yml 覆盖):标量以 Config 为准,
481
+ // 数据文件只提供生成物本身(profiles / memberMaxDepth)——小队目录是数据,不是可调参数。
448
482
  const engineConfigDir = dirname(config.root);
449
- const engineConfigPath = ["t-team.config.json", "agent-teams.config.json"]
450
- .map((file) => join(engineConfigDir, file))
451
- .find((candidate) => existsSync(candidate)) ?? join(engineConfigDir, "t-team.config.json");
483
+ const engineConfigPath = typeof config.engineConfig === "string" && config.engineConfig !== ""
484
+ ? config.engineConfig
485
+ : ["t-team.config.json", "agent-teams.config.json"]
486
+ .map((file) => join(engineConfigDir, file))
487
+ .find((candidate) => existsSync(candidate)) ?? join(engineConfigDir, "t-team.config.json");
452
488
  let engineConfig = {};
453
- try {
454
- const parsed = JSON.parse(readFileSync(engineConfigPath, "utf8"));
455
- if (parsed !== null && typeof parsed === "object") engineConfig = parsed;
456
- } catch {
457
- console.warn(`[t-team] 未读到团队引擎配置 ${engineConfigPath}(跑一次 team-profiles.py 生成小队后即可用 /t 拉起团队)`);
489
+ let engineConfigDetail = "";
490
+ // 配置错误要响亮:**文件存在但内容不合法**属于自带的错配,直接让插件加载失败,
491
+ // 而不是带着空配置把团队功能静默降级。文件不存在是"还没生成小队"(自定义 root 时合法),
492
+ // 这种情况明确降级,并把原因送到所有可见面(日志 + /t + 下面的 engine-status 提示段)。
493
+ if (existsSync(engineConfigPath)) {
494
+ let parsed;
495
+ try {
496
+ parsed = JSON.parse(readFileSync(engineConfigPath, "utf8"));
497
+ } catch (error) {
498
+ throw new Error(`团队引擎配置无法解析:${engineConfigPath}(${error instanceof Error ? error.message : String(error)})。它是 team-profiles.py 的产物,删掉后重跑一次即可重建。`);
499
+ }
500
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
501
+ throw new Error(`团队引擎配置必须是 JSON 对象:${engineConfigPath}`);
502
+ }
503
+ if (parsed.profiles !== undefined && (parsed.profiles === null || typeof parsed.profiles !== "object" || Array.isArray(parsed.profiles))) {
504
+ throw new Error(`团队引擎配置的 profiles 必须是对象:${engineConfigPath}`);
505
+ }
506
+ engineConfig = parsed;
507
+ } else {
508
+ engineConfigDetail = `找不到团队引擎配置 ${engineConfigPath}(跑一次 team-profiles.py 生成小队后即可用 /t 拉起团队)`;
458
509
  }
459
510
  // 引擎与名册解耦:引擎挂载失败(例如 Harness 子代理契约不匹配)时,
460
- // T专家 的专家名册、@ 召唤、/t 列表仍然可用,只是团队功能不可用;失败原因会经 /t 报出来。
461
- const engineState = { ok: false, detail: "" };
462
- try {
463
- ctx.plugin(teamsEngine, {
464
- stateDir: typeof engineConfig.stateDir === "string" ? engineConfig.stateDir : ".agent-teams",
465
- memberProvider: typeof engineConfig.memberProvider === "string" ? engineConfig.memberProvider : "spawn",
466
- ...(engineConfig.memberModel === undefined ? {} : { memberModel: engineConfig.memberModel }),
467
- ...(engineConfig.memberMaxDepth === undefined ? {} : { memberMaxDepth: engineConfig.memberMaxDepth }),
468
- ...(engineConfig.maxMembers === undefined ? {} : { maxMembers: engineConfig.maxMembers }),
469
- // 引擎自带命令必须关掉:T专家 自己注册 /t(带小队别名解析),否则两个同名命令相撞。
470
- // 手势边界另外复用(见下),它是队长协议真正被触发的地方。
471
- slashCommand: false,
472
- profiles: engineConfig.profiles ?? {},
473
- });
474
- // 复用引擎导出的手势边界:把 `/t --profile <小队> <目标>` 这条用户消息翻成队长协议指令。
475
- installTTeamGestureBoundary(ctx, () => engineConfig.profiles ?? {});
476
- engineState.ok = true;
477
- } catch (error) {
478
- engineState.detail = error instanceof Error ? error.message : String(error);
479
- console.error(`[t-team] 团队引擎挂载失败(专家名册功能不受影响):${engineState.detail}`);
511
+ // T专家 的专家名册、@ 召唤、/t 列表仍然可用,只是团队功能不可用。
512
+ // 这是**已声明的降级**,不是静默跳过:原因会写进日志、经 /t 报出,并进系统提示段。
513
+ const engineState = { ok: false, detail: engineConfigDetail };
514
+ if (engineConfigDetail === "") {
515
+ try {
516
+ ctx.plugin(teamsEngine, {
517
+ stateDir: config.stateDir,
518
+ memberProvider: config.memberProvider,
519
+ ...(config.memberModel === undefined ? {} : { memberModel: config.memberModel }),
520
+ ...(engineConfig.memberMaxDepth === undefined ? {} : { memberMaxDepth: engineConfig.memberMaxDepth }),
521
+ maxMembers: config.maxMembers,
522
+ // 引擎自带命令必须关掉:T专家 自己注册 /t(带小队别名解析),否则两个同名命令相撞。
523
+ // 手势边界另外复用(见下),它是队长协议真正被触发的地方。
524
+ slashCommand: false,
525
+ profiles: engineConfig.profiles ?? {},
526
+ });
527
+ // 复用引擎导出的手势边界:把 `/t --profile <小队> <目标>` 这条用户消息翻成队长协议指令。
528
+ installTTeamGestureBoundary(ctx, () => engineConfig.profiles ?? {});
529
+ engineState.ok = true;
530
+ } catch (error) {
531
+ engineState.detail = error instanceof Error ? error.message : String(error);
532
+ ctx.logger?.error?.(`[t-team] 团队引擎挂载失败(专家名册功能不受影响):${engineState.detail}`);
533
+ }
534
+ } else {
535
+ ctx.logger?.error?.(`[t-team] ${engineConfigDetail}`);
480
536
  }
481
537
 
538
+ // 已声明的降级也要**对模型可见**:否则"团队功能没了"这件事只留在日志里,
539
+ // 用户问起来模型只能说不知道 —— 那正是规范禁止的"静默跳过缺失的引用对象"。
540
+ ctx.systemPrompt.section({
541
+ name: "t-team:engine-status",
542
+ order: 120,
543
+ text: (context) => {
544
+ if (context.agent?.session?.header?.parentSession !== undefined) return "";
545
+ if (engineState.ok === true) return "";
546
+ return [
547
+ "## T专家 团队引擎当前不可用",
548
+ `团队功能(/t 建队、t_team_* 工具)现在不可用。原因:${engineState.detail}`,
549
+ "专家名册、@ 召唤与 summon_t_expert 不受影响。用户需要团队功能时,请原样转述上面的原因,不要假装能建队。",
550
+ ].join("\n");
551
+ },
552
+ });
553
+
482
554
  // ---- 小队定义服务(设置页「小队」标签用;写回 teams.json 后自动重跑 team-profiles.py)----
483
555
  const teamsFile = join(dirname(config.root), "teams.json");
484
556
  const squads = createSquadService({
@@ -504,6 +576,8 @@ export function apply(ctx, config) {
504
576
  teamId: team.teamId,
505
577
  name: team.name,
506
578
  workspace: team.workspace,
579
+ // 面板/设置页的「打开活动面板」要带 captainSessionId:活动面板是会话作用域的,
580
+ // 缺它就只能匹配当前会话的团队,别的会话(或历史归档)点按钮会静默无反应。
507
581
  captainSessionId: team.captainSessionId,
508
582
  phase: team.phase,
509
583
  halted: team.halted === true,
package/lib/plan-check.js CHANGED
@@ -77,7 +77,9 @@ export function checkPlan(team, tasks) {
77
77
  const sim = { ...(team ?? {}), tasks: [...existing] };
78
78
  const taken = new Set(existing.map((task) => task.id));
79
79
  const labels = new Map(); // 本批 id → 模拟 id
80
- let counter = existing.length;
80
+ // 引擎分配的是 `t${team.taskSeq + 1}` 并随后自增(不是"现有任务条数")。两者只在
81
+ // taskSeq === tasks.length 时才一致 —— 有任务被删除/取消过就会分叉,所以以 taskSeq 为准。
82
+ let counter = Number.isSafeInteger(team?.taskSeq) ? team.taskSeq : existing.length;
81
83
  const report = [];
82
84
 
83
85
  proposed.forEach((raw, index) => {
@@ -3,7 +3,7 @@ import { join } from 'node:path';
3
3
  import { readTeamSync, readRetiredMemberIdsSync } from "./state.js";
4
4
  import { MEMBER_TOOL_NAMES, TEAM_TOOL_NAMES } from "./tool-names.js";
5
5
  export const TEAM_ACTIVATION_PROMPT = 'T Team (T专家 团队) provides multi-agent team collaboration. Apply these rules when the user requests it (including /t-team) or when continuing an existing team. Mentioning, quoting, discussing, or declining T Team alone is not a request to start work.';
6
- export const TEAM_MEMBER_PROMPT = 'You are an T Team member. Follow your assigned member persona and task contract. Use t_team_claim_task, t_team_update_task, t_team_send_message and t_team_status for your own work. Include the current attempt_id in updates; report completion or failure to the captain. Do not create, approve, edit or resume a team. If your durable membership is unavailable, report that to the parent instead of creating a replacement.';
6
+ export const TEAM_MEMBER_PROMPT = 'You are a T Team member. Follow your assigned member persona and task contract. Use t_team_claim_task, t_team_update_task, t_team_send_message and t_team_status for your own work. Include the current attempt_id in updates; report completion or failure to the captain. Do not create, approve, edit or resume a team. If your durable membership is unavailable, report that to the parent instead of creating a replacement.';
7
7
  function stateRoot(agent, config) {
8
8
  return join(agent.session.header.cwd ?? process.cwd(), config.stateDir);
9
9
  }
@@ -91,7 +91,7 @@ export function usageSectionText(toolNames, profilesText = '') {
91
91
  1. Inspect current team state when needed, using t_team_status. Continue existing work without duplicating its roster/tasks. Create only when no current team exists, with the user's goal as description and approval="required"; automatic approval requires an explicit request to run immediately. Staged plans never spawn or schedule work.
92
92
  2. Add each needed role once; members inherit your model route unless another is requested/needed. A requested profile goes to create({profile}); it supplies its roster. Seed profiles also supply tasks; captain-planning profiles require your DAG. Do not duplicate either.
93
93
  3. Build the complete smallest useful DAG while staged. Every task needs a subject; dependencies represent prerequisites. Give every required contributor a task or explicit message. Present the plan and end your turn for review; never approve in that planning turn. Approve only after a later explicit user approval or the Web action.
94
- 4. Respect Web approve/return/discard control messages. On return, ask what to change before editing; after the answer, use one atomic t_team_edit_plan batch (edit downstream references before removals), summarize and await review again. Never inspect or edit .t-team state files or plugin source code to revise plans. Discard does not authorize a replacement.
94
+ 4. Respect Web approve/return/discard control messages. On return, ask what to change before editing; after the answer, use one atomic t_team_edit_plan batch (edit downstream references before removals), summarize and await review again. Never inspect or edit the T Team state directory in the workspace or the plugin source code to revise plans. Discard does not authorize a replacement.
95
95
  5. The scheduler dispatches ready tasks after approval. Delegate; do not duplicate slow work or send messages merely to start a stage. Handle reports/user work, then yield when waiting is all that remains: reports wake you automatically. Use status after a delivery or user request, never busy-poll or wait for unassigned members.
96
96
  6. Tasks carry attempt_id capabilities. Use the current attempt_id; stale means ownership changed. Pause members only on explicit request; later guidance via send_message continues that same attempt. Retry, transfer or take over through reassign_task first; it revokes the old attempt and waits for quiescence. Prefer a member. Captain implementation/review takeover requires a user request. Every takeover is one ready task at a time, finished in this turn; never yield with captain-owned work open.
97
97
  7. Quality kinds (requirements, implementation, verification, review, repair, integration) require objective + acceptance; implementation/repair also require inScope + verify. Derive paths/commands from the workspace/profile, never assume src/ or pnpm test. Review/requirements complete only with verdict=pass; needs_revision/reject fail with findings. Never approve your own implementation or ask for a deliberate failure.
@@ -119,7 +119,7 @@ function isDefaultExcluded(path) {
119
119
  const base = segments[segments.length - 1] ?? '';
120
120
  if (segments[0] === '.git' || segments[0] === '.dsh')
121
121
  return true;
122
- if (base === '.env' || base.startsWith('.env.'))
122
+ if (base === '.env' || (base.startsWith('.env.') && !['.example', '.sample', '.template', '.dist'].some(suffix => base.endsWith(suffix))))
123
123
  return true;
124
124
  if (segments.includes('secrets'))
125
125
  return true;
@@ -253,9 +253,7 @@ export function validateCreateTask(team, input) {
253
253
  continue;
254
254
  if (!OPEN_STATUSES.includes(other.status))
255
255
  continue;
256
- if (dependencies.includes(other.id) || other.dependencies.includes('pending-new'))
257
- continue;
258
- if (dependencies.includes(other.id))
256
+ if (dependencyClosureContains(team.tasks, dependencies, other.id))
259
257
  continue;
260
258
  const overlap = inScopeOverlap(input.inScope, other.inScope);
261
259
  if (overlap.length > 0) {
@@ -280,7 +278,7 @@ export function validateCreateTask(team, input) {
280
278
  }
281
279
  }
282
280
  const nextTeam = team.halted === true && input.resume === true
283
- ? { ...team, halted: false, haltedAt: undefined }
281
+ ? { ...team, halted: false, haltedAt: undefined, escalated: false }
284
282
  : team;
285
283
  return {
286
284
  ok: true,
@@ -571,6 +569,7 @@ export function resumeTeamState(team, reason) {
571
569
  ...team,
572
570
  halted: false,
573
571
  haltedAt: undefined,
572
+ escalated: false,
574
573
  },
575
574
  };
576
575
  }
@@ -710,7 +710,7 @@ export function registerTTeamTools(ctx, config) {
710
710
  }));
711
711
  ctx.tools.register(defineTool({
712
712
  name: 't_team_edit_plan',
713
- description: 'Atomically revise the current staged T Team plan without spawning members or scheduling tasks. Use this when the user continues chatting to change a plan that is waiting for approval. Submit dependent edits in order (update downstream dependencies or assignees, then remove tasks, then remove unused members). Never inspect or edit .t-team state files or plugin source code to revise a plan.',
713
+ description: 'Atomically revise the current staged T Team plan without spawning members or scheduling tasks. Use this when the user continues chatting to change a plan that is waiting for approval. Submit dependent edits in order (update downstream dependencies or assignees, then remove tasks, then remove unused members). Never inspect or edit the T Team state directory in the workspace or plugin source code to revise a plan.',
714
714
  parameters: {
715
715
  operations: {
716
716
  type: 'array',
@@ -1005,7 +1005,7 @@ export function registerTTeamTools(ctx, config) {
1005
1005
  const member = requireMember(fresh, args.name);
1006
1006
  const requeued = [];
1007
1007
  for (const task of fresh.tasks) {
1008
- if (task.assignee !== member.name || task.status === 'completed')
1008
+ if (task.assignee !== member.name || TERMINAL_TASK_STATUSES.includes(task.status))
1009
1009
  continue;
1010
1010
  invalidateTaskAttempt(task);
1011
1011
  task.reassigning = false;
@@ -1508,10 +1508,13 @@ export function registerTTeamTools(ctx, config) {
1508
1508
  throw new Error(`task ${task.id} is owned by member "${task.assignee}"; call t_team_reassign_task with assignee="captain" before takeover`);
1509
1509
  }
1510
1510
  if (identity.kind === 'member') {
1511
+ if (task.reassigning === true) {
1512
+ throw new Error(`task ${task.id} is being reassigned; stop work and wait for the fresh assignment`);
1513
+ }
1511
1514
  if (task.assignee !== identity.name) {
1512
1515
  throw new Error(`task ${task.id} is assigned to "${task.assignee ?? 'nobody'}", not you`);
1513
1516
  }
1514
- if (task.attemptId !== undefined && args.attempt_id !== task.attemptId) {
1517
+ if (task.attemptId !== args.attempt_id) {
1515
1518
  throw new Error(`stale attempt for task ${task.id}: expected the current attempt_id; stop work and request fresh assignment`);
1516
1519
  }
1517
1520
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-t-expert",
3
- "version": "0.1.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "T Expert — a 279-expert, 18-division roster with full Chinese translations, as a standalone DeepSeek Harness plugin, with a built-in multi-agent team engine (T Team).",
6
6
  "license": "MIT",
@@ -45,7 +45,10 @@
45
45
  "cordis.patch.yml",
46
46
  "README.md",
47
47
  "LICENSE",
48
- "THIRD-PARTY-NOTICES"
48
+ "THIRD-PARTY-NOTICES",
49
+ "vendor/dsh-agent-teams/LICENSE",
50
+ "vendor/dsh-agent-teams/VENDOR.md",
51
+ "vendor/third-party-licenses"
49
52
  ],
50
53
  "dsh": {
51
54
  "bundle": {
@@ -53,7 +56,6 @@
53
56
  },
54
57
  "client": {
55
58
  "inject": [
56
- "@deepseek-ai/dsh-client-runtime",
57
59
  "@deepseek-ai/dsh-client-connection",
58
60
  "@deepseek-ai/dsh-client-locale",
59
61
  "@deepseek-ai/dsh-api-remotes",
@@ -65,11 +67,11 @@
65
67
  }
66
68
  },
67
69
  "scripts": {
68
- "build": "node scripts/brand.mjs && node scripts/build-client.mjs",
69
- "verify": "node scripts/verify.mjs",
70
- "brand": "node scripts/brand.mjs",
71
- "sync-data": "node scripts/sync-data.mjs",
72
- "prepublishOnly": "npm run build && npm run verify && node scripts/sync-data.mjs --check"
70
+ "build": "node ../brand.mjs && node ../build-client.mjs",
71
+ "verify": "node ../verify.mjs",
72
+ "brand": "node ../brand.mjs",
73
+ "sync-data": "node ../sync-data.mjs",
74
+ "prepublishOnly": "npm run build && npm run verify && node ../sync-data.mjs --check"
73
75
  },
74
76
  "peerDependencies": {
75
77
  "@deepseek-ai/cordis": "^4.0.1",
@@ -85,7 +87,6 @@
85
87
  "@deepseek-ai/dsh-llm": "0.1.5-rc.1 || 0.1.5-rc.2",
86
88
  "@deepseek-ai/dsh-session": "0.1.5-rc.1 || 0.1.5-rc.2",
87
89
  "@deepseek-ai/dsh-settings": "0.1.5-rc.1 || 0.1.5-rc.2",
88
- "@deepseek-ai/dsh-skill": "0.1.5-rc.1 || 0.1.5-rc.2",
89
90
  "@deepseek-ai/dsh-subagent": "0.1.5-rc.1 || 0.1.5-rc.2",
90
91
  "@deepseek-ai/dsh-system-prompt": "0.1.5-rc.1 || 0.1.5-rc.2",
91
92
  "@deepseek-ai/dsh-tools": "0.1.5-rc.1 || 0.1.5-rc.2",
@@ -133,9 +134,6 @@
133
134
  "@deepseek-ai/dsh-settings": {
134
135
  "optional": true
135
136
  },
136
- "@deepseek-ai/dsh-skill": {
137
- "optional": true
138
- },
139
137
  "@deepseek-ai/dsh-subagent": {
140
138
  "optional": true
141
139
  },
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 程序员阿江(Relakkes)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,53 @@
1
+ # vendor: @nanmicoder/dsh-agent-teams(T专家 内置团队引擎)
2
+
3
+ 路线 A:把 AgentTeams 的团队引擎**原样**内置进 T专家,安装后不再需要单独安装 AgentTeams。
4
+
5
+ | 项 | 值 |
6
+ | --- | --- |
7
+ | 上游仓库 | https://github.com/NanmiCoder/dsh-agent-teams |
8
+ | 上游版本 | `0.1.17-rc.1` |
9
+ | 仓库 commit | `18fba6211fc3aac305fc9bb1c8a7faaf7273137a` |
10
+ | 同步来源目录 | `~/.dsh/profiles/desktop/node_modules/@nanmicoder/dsh-agent-teams` |
11
+ | 同步时间 | 2026-09-11 18:31:14 +0800 |
12
+ | 许可 | MIT(见同目录 LICENSE;本目录文件保持逐字节原样) |
13
+
14
+ ## 同步进来的文件
15
+
16
+ ```
17
+ lib/teams/capabilities.js 4943 bytes
18
+ lib/teams/command.js 8378 bytes
19
+ lib/teams/event-types.js 569 bytes
20
+ lib/teams/events.js 2827 bytes
21
+ lib/teams/harness-compat.js 8733 bytes
22
+ lib/teams/index.js 27236 bytes
23
+ lib/teams/members.js 33124 bytes
24
+ lib/teams/profiles.js 23215 bytes
25
+ lib/teams/quality-gates.js 37323 bytes
26
+ lib/teams/scheduler.js 24563 bytes
27
+ lib/teams/snapshot.js 7725 bytes
28
+ lib/teams/state.js 38289 bytes
29
+ lib/teams/tool-names.js 708 bytes
30
+ lib/teams/tools.js 127546 bytes
31
+ lib/teams/types.js 780 bytes
32
+ lib/teams/web-routes.js 3328 bytes
33
+ vendor/dsh-agent-teams/client.bundle.txt 197722 bytes
34
+ ```
35
+
36
+ ## T专家 的改动点(都在我们的代码里,vendor 文件不动)
37
+
38
+ 1. host:`lib/index.js` 里 `ctx.plugin(engineNamespace, engineConfig)` 内部挂载引擎,配置来自
39
+ `~/.t-team/t-team.config.json`(由 `team-profiles.py` 依据 `teams.json` 生成)。
40
+ 2. client:`src/client.jsx` 在 factory 里临时截获 `window.__ModuleLoader__.load`,取出上游 client bundle
41
+ 注册的 factory,用我们自己的 `require` 物化它,再调用它的 `apply(ctx)` —— 于是团队树/草案面板与
42
+ T专家 的设置页共用同一个客户端模块(同一个注册 id,DSH 只允许一个包注册一个 id)。
43
+ 3. 工具名保持上游的 `agent_teams_*`,命令保持 `/agent-teams`;T专家 另加 `/t` 短命令与专家名册工具。
44
+
45
+ ## 更新上游
46
+
47
+ ```bash
48
+ bash ~/web/t-team/tz.sh 13 # 从已安装的包同步(先 dshmarket/pnpm 升到新版)
49
+ bash ~/web/t-team/tz.sh engine-sync --check # 只看差异
50
+ bash ~/web/t-team/tz.sh engine-sync --from DIR --clone ~/.t-team/vendor/dsh-agent-teams
51
+ ```
52
+
53
+ 同步后:`bash ~/web/t-team/tz.sh 10`(自检)→ `tz.sh 12`(构建)→ `tz.sh 8`(安装)→ 重启 DSH Desktop。
@@ -0,0 +1,22 @@
1
+ # 第三方许可文本(随包分发)
2
+
3
+ 本目录只放**随包分发的第三方内容**的完整许可证文本,以便再分发时履行 MIT 的
4
+ "保留版权声明与许可声明"义务。说明与署名见包根目录的 `THIRD-PARTY-NOTICES`。
5
+
6
+ | 文件 | 覆盖的随包内容 | 来源 |
7
+ | --- | --- | --- |
8
+ | `agency-agents.LICENSE` | `data/experts/`(18 分区 / 279 位专家的逐字节镜像) | The Agency / AgentLand 名册快照,MIT |
9
+ | `agency-agents-zh.LICENSE` | `data/zh/` 中的中文名字、简介与人格正文 | `agency-agents-zh` 中文翻译与本地化资产,MIT |
10
+
11
+ 两份文本都是从实际用于生成快照的来源包内**逐字节复制**的,未经改写:
12
+
13
+ ```
14
+ <安装的 @michengai/dsh-agency-agents>/assets/agency-agents/LICENSE
15
+ <安装的 @michengai/dsh-agency-agents>/assets/agency-agents-zh/LICENSE
16
+ ```
17
+
18
+ `@michengai/dsh-agency-agents` 自身的 TypeScript 源码与构建脚本是 Apache-2.0,
19
+ 但**本插件不打包它的任何代码**,只用到了上面这两份 MIT 授权的资产,
20
+ 因此这里只需分发这两份 MIT 文本。
21
+
22
+ 内置团队引擎的许可证在 `../dsh-agent-teams/LICENSE`(同样随包分发)。
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Michael Sitarzewski (original English version)
4
+ Copyright (c) 2026 jnMetaCode (Chinese translation and localization)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 AgentLand Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.