dsh-plugin-t-expert 0.2.7 → 0.2.9
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 +20 -9
- 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/catalog.js +134 -31
- package/lib/client.js +1 -1
- package/lib/command.js +4 -1
- package/lib/i18n.js +75 -11
- package/lib/index.js +167 -62
- package/lib/plan-check.js +14 -3
- package/lib/remote.js +82 -15
- package/lib/skill.js +102 -42
- package/lib/squads.js +51 -3
- package/lib/teams/assignee-contract.js +47 -0
- package/lib/teams/quality-gates.js +24 -1
- package/lib/teams/state.js +14 -2
- package/lib/teams/tools.js +9 -3
- package/package.json +10 -24
- 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 +4 -3
- package/skills/t-expert-manager/references/ops-reference.md +2 -2
- package/vendor/third-party-licenses/README.md +1 -1
package/data/team-profiles.py
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""把 T专家 的小队定义(teams.json
|
|
2
|
+
"""把 T专家 的小队定义(teams.json)编译成内置团队引擎的 profiles 配置。
|
|
3
3
|
|
|
4
|
-
职责分工:T专家 管「人」(名册 + 中文 +
|
|
4
|
+
职责分工:T专家 管「人」(名册 + 中文 + 人格),内置引擎 管「队」(成员生命周期、任务 DAG、
|
|
5
5
|
调度、邮箱、面板)。本脚本只做**数据转换**:读 teams.json + experts/ + zh/,把每位专家的
|
|
6
|
-
中文名与人格正文写成 profile 成员的 executionPrompt,然后写进
|
|
7
|
-
|
|
6
|
+
中文名与人格正文写成 profile 成员的 executionPrompt,然后写进 `t-team.config.json`
|
|
7
|
+
(引擎由 T专家 在 apply 时挂载并读这份配置)。
|
|
8
8
|
|
|
9
9
|
用法:
|
|
10
|
-
python3 team-profiles.py # 生成并写入
|
|
11
|
-
python3 team-profiles.py --dry-run #
|
|
10
|
+
python3 team-profiles.py # 生成并写入 t-team.config.json(+ teams.resolved.json)
|
|
11
|
+
python3 team-profiles.py --dry-run # 只打印校验与**将要写入的内容**(预览 == 产物)
|
|
12
12
|
python3 team-profiles.py --mode full # 成员人格用完整正文(默认 summary 摘录)
|
|
13
|
-
python3 team-profiles.py --no-write #
|
|
13
|
+
python3 team-profiles.py --no-write # 同上,但不动文件
|
|
14
14
|
|
|
15
15
|
注意(两个已确认的坑):
|
|
16
|
-
1)
|
|
17
|
-
|
|
16
|
+
1) **不要把 stateDir 写进 t-team.config.json**:状态目录的唯一真源是插件侧 Config.stateDir,
|
|
17
|
+
数据文件里再留一份就会出现「文件里一个值、实际生效另一个值」的双来源(审计 D-1)。
|
|
18
18
|
2) AgentTeams 的 profile key 只支持 ASCII(中文 key 不会生成命令别名),中文放 description/aliases。
|
|
19
19
|
"""
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
import argparse
|
|
23
|
+
import copy
|
|
23
24
|
import json
|
|
24
25
|
import os
|
|
25
26
|
import re
|
|
@@ -43,7 +44,10 @@ MARKER_START = "# >>> t-team teams(由 team-profiles.py 生成,勿手改;
|
|
|
43
44
|
MARKER_END = "# <<< t-team teams"
|
|
44
45
|
|
|
45
46
|
MAX_PROFILES = 48 # 引擎侧 lib/teams/profiles.js 已把 MAX_TEAM_PROFILES 调到 48(原默认 16)
|
|
46
|
-
|
|
47
|
+
# 一个 profile 的成员数上限(经验值:队长上下文压力)。
|
|
48
|
+
# 只是**默认**:插件侧 Config.maxMembers 经 --max-members 覆盖它,两边同一个真源;
|
|
49
|
+
# 直接跑本脚本(不带该参数)时行为与从前完全一致。
|
|
50
|
+
MAX_MEMBERS = 8
|
|
47
51
|
MAX_SEED_TASKS = 32 # AgentTeams: MAX_PROFILE_TASKS
|
|
48
52
|
PROFILE_KEY_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
49
53
|
NAME_FIELD = re.compile(r"^name\s*:", re.M)
|
|
@@ -107,10 +111,10 @@ def load_roster(experts_root: str, zh_root: str) -> dict[str, dict]:
|
|
|
107
111
|
|
|
108
112
|
|
|
109
113
|
def build_members(slugs: list[str], roster: dict[str, dict], mode: str, budget: int,
|
|
110
|
-
errors: list[str], where: str) -> list[dict]:
|
|
114
|
+
errors: list[str], where: str, max_members: int = MAX_MEMBERS) -> list[dict]:
|
|
111
115
|
members, used_names = [], {}
|
|
112
|
-
if len(slugs) >
|
|
113
|
-
errors.append(f"{where}: 成员 {len(slugs)} 位超过上限 {
|
|
116
|
+
if len(slugs) > max_members:
|
|
117
|
+
errors.append(f"{where}: 成员 {len(slugs)} 位超过上限 {max_members}")
|
|
114
118
|
for slug in slugs:
|
|
115
119
|
expert = roster.get(slug)
|
|
116
120
|
if expert is None:
|
|
@@ -160,48 +164,31 @@ def emit_member(member: dict, indent: str) -> str:
|
|
|
160
164
|
return "\n".join(lines)
|
|
161
165
|
|
|
162
166
|
|
|
163
|
-
def
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def write_patch(patch_path: str, block: str, dry_run: bool) -> str:
|
|
191
|
-
"""把标记块替换进 profile patch(保留其它条目),返回备份路径。"""
|
|
192
|
-
old = open(patch_path, encoding="utf-8").read() if os.path.isfile(patch_path) else "[]\n"
|
|
193
|
-
pattern = re.compile(re.escape(MARKER_START) + r".*?" + re.escape(MARKER_END) + r"\n?", re.S)
|
|
194
|
-
if pattern.search(old):
|
|
195
|
-
new = pattern.sub(block, old)
|
|
196
|
-
else:
|
|
197
|
-
base = old if old.strip() and old.strip() != "[]" else ""
|
|
198
|
-
new = (base.rstrip("\n") + "\n\n" if base else "") + block
|
|
199
|
-
backup = f"{patch_path}.bak.{datetime.now():%Y%m%d-%H%M%S}"
|
|
200
|
-
if dry_run:
|
|
201
|
-
return backup
|
|
202
|
-
shutil.copy2(patch_path, backup) if os.path.isfile(patch_path) else None
|
|
203
|
-
open(patch_path, "w", encoding="utf-8").write(new)
|
|
204
|
-
return backup
|
|
167
|
+
def build_engine_config(spec: dict, profiles: dict[str, dict]) -> dict:
|
|
168
|
+
"""构造**真正会写下去**的引擎配置(`t-team.config.json` 的那份对象)。
|
|
169
|
+
|
|
170
|
+
预览与落盘共用这一个构造点,两者不可能再漂移 —— 这是「预览必须等于产物」的实现方式。
|
|
171
|
+
|
|
172
|
+
注意:**不要**把 stateDir 写进这份 JSON。状态目录的唯一真源是插件侧 Config.stateDir
|
|
173
|
+
(lib/index.js 用它挂载引擎、找团队状态、跑 t_team_plan_check)。这里再写一份,就会出现
|
|
174
|
+
「数据文件里一个值、实际生效另一个值」的双来源:改开 Config.stateDir 的部署两边失明且无报错
|
|
175
|
+
(审计 D-1)。历史数据文件里的该字段已删除,且插件不再读它。
|
|
176
|
+
"""
|
|
177
|
+
return {
|
|
178
|
+
"memberProvider": (spec.get("engine") or {}).get("memberProvider", "spawn"),
|
|
179
|
+
"profiles": {
|
|
180
|
+
key: {
|
|
181
|
+
"description": profile["description"],
|
|
182
|
+
"taskPlanning": profile["taskPlanning"],
|
|
183
|
+
"members": [
|
|
184
|
+
{"name": m["name"], "role": m["role"], "executionPrompt": m["executionPrompt"]}
|
|
185
|
+
for m in profile["members"]
|
|
186
|
+
],
|
|
187
|
+
**({"tasks": profile["tasks"]} if profile.get("tasks") else {}),
|
|
188
|
+
}
|
|
189
|
+
for key, profile in profiles.items()
|
|
190
|
+
},
|
|
191
|
+
}
|
|
205
192
|
|
|
206
193
|
|
|
207
194
|
def remove_patch_block(patch_path: str, dry_run: bool) -> bool:
|
|
@@ -220,6 +207,16 @@ def remove_patch_block(patch_path: str, dry_run: bool) -> bool:
|
|
|
220
207
|
return True
|
|
221
208
|
|
|
222
209
|
|
|
210
|
+
def read_engine_state_dir(engine_config_path: str) -> object:
|
|
211
|
+
"""读盘上那份产物里的 stateDir 死字段(只为让预览如实说明「它会被丢弃」;读不到就返回 None)。"""
|
|
212
|
+
if not os.path.isfile(engine_config_path):
|
|
213
|
+
return None
|
|
214
|
+
try:
|
|
215
|
+
return json.load(open(engine_config_path, encoding="utf-8")).get("stateDir")
|
|
216
|
+
except (OSError, json.JSONDecodeError):
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
223
220
|
def main() -> int:
|
|
224
221
|
ap = argparse.ArgumentParser()
|
|
225
222
|
ap.add_argument("--teams", default=DEFAULT_TEAMS)
|
|
@@ -227,10 +224,15 @@ def main() -> int:
|
|
|
227
224
|
ap.add_argument("--zh", default=DEFAULT_ZH)
|
|
228
225
|
ap.add_argument("--patch", default=DEFAULT_PATCH)
|
|
229
226
|
ap.add_argument("--mode", choices=["summary", "full", "reference"], help="覆盖默认人格模式")
|
|
227
|
+
ap.add_argument("--max-members", type=int, default=MAX_MEMBERS,
|
|
228
|
+
help=f"一个 profile 的成员数上限(默认 {MAX_MEMBERS};插件侧由 Config.maxMembers 透传)")
|
|
230
229
|
ap.add_argument("--dry-run", action="store_true")
|
|
231
230
|
ap.add_argument("--no-write", action="store_true")
|
|
232
231
|
args = ap.parse_args()
|
|
233
232
|
|
|
233
|
+
if args.max_members < 1:
|
|
234
|
+
print(f"错误:--max-members 必须是正整数,收到 {args.max_members}", file=sys.stderr)
|
|
235
|
+
return 1
|
|
234
236
|
if not os.path.isfile(args.teams):
|
|
235
237
|
print(f"错误:找不到小队定义 {args.teams}", file=sys.stderr)
|
|
236
238
|
return 1
|
|
@@ -267,7 +269,7 @@ def main() -> int:
|
|
|
267
269
|
aliases[alias] = key
|
|
268
270
|
mode = args.mode or profile.get("personaMode") or defaults.get("personaMode") or "summary"
|
|
269
271
|
budget = int(profile.get("personaBudget") or defaults.get("personaBudget") or 1600)
|
|
270
|
-
members = build_members(list(profile.get("members") or []), roster, mode, budget, errors, key)
|
|
272
|
+
members = build_members(list(profile.get("members") or []), roster, mode, budget, errors, key, args.max_members)
|
|
271
273
|
if not members:
|
|
272
274
|
errors.append(f"{key}: 没有任何有效成员")
|
|
273
275
|
continue
|
|
@@ -303,32 +305,32 @@ def main() -> int:
|
|
|
303
305
|
print(" ✗", error, file=sys.stderr)
|
|
304
306
|
return 1
|
|
305
307
|
|
|
306
|
-
|
|
308
|
+
# 引擎配置:T专家 在 apply 时读它并 ctx.plugin(内置引擎, 该配置)
|
|
309
|
+
engine_config_path = os.path.join(os.path.dirname(os.path.abspath(args.teams)), "t-team.config.json")
|
|
310
|
+
engine_config = build_engine_config(spec, profiles)
|
|
311
|
+
|
|
307
312
|
if args.dry_run or args.no_write:
|
|
308
|
-
|
|
313
|
+
# 预览必须等于产物:这里打印的就是上面那个 engine_config 对象。
|
|
314
|
+
# 旧实现在这里打印一段带 `- id: agent-teams` / `stateDir: .agent-teams` 的**遗留 patch 块**,
|
|
315
|
+
# 既与实际产物不符,又与 build_engine_config 的「不要把 stateDir 写进这份 JSON」注释自相矛盾
|
|
316
|
+
# —— 预览是给人看的,看到的东西必须等于真正会写下去的东西(复核 F-1)。
|
|
317
|
+
preview = copy.deepcopy(engine_config)
|
|
318
|
+
legacy_state_dir = read_engine_state_dir(engine_config_path)
|
|
319
|
+
# 预览的键集合必须与产物一致,所以 stateDir **不作为产物键**出现;
|
|
320
|
+
# 盘上若还留着历史死字段,另起一行如实说明它会被丢弃(不塞进 JSON 里假装是产物)。
|
|
321
|
+
print("\n" + "=" * 60)
|
|
322
|
+
print(f"预览:将写入 {engine_config_path}(内容与该文件实际写入的完全一致):\n")
|
|
323
|
+
print(json.dumps(preview, ensure_ascii=False, indent=2))
|
|
324
|
+
print("\nstateDir:不是本文件的产物字段(预览与产物都不含它)。"
|
|
325
|
+
"状态目录的唯一真源是插件侧 Config.stateDir(审计 D-1);"
|
|
326
|
+
f"历史遗留字段{'会被丢弃(盘上当前值 ' + repr(legacy_state_dir) + ')' if legacy_state_dir is not None else '已不在盘上'}。")
|
|
309
327
|
if args.dry_run:
|
|
310
328
|
print("(dry-run,未写盘)")
|
|
311
329
|
return 0
|
|
312
330
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
"stateDir": (spec.get("engine") or {}).get("stateDir", ".agent-teams"),
|
|
317
|
-
"memberProvider": (spec.get("engine") or {}).get("memberProvider", "spawn"),
|
|
318
|
-
"profiles": {
|
|
319
|
-
key: {
|
|
320
|
-
"description": profile["description"],
|
|
321
|
-
"taskPlanning": profile["taskPlanning"],
|
|
322
|
-
"members": [
|
|
323
|
-
{"name": m["name"], "role": m["role"], "executionPrompt": m["executionPrompt"]}
|
|
324
|
-
for m in profile["members"]
|
|
325
|
-
],
|
|
326
|
-
**({"tasks": profile["tasks"]} if profile.get("tasks") else {}),
|
|
327
|
-
}
|
|
328
|
-
for key, profile in profiles.items()
|
|
329
|
-
},
|
|
330
|
-
}, open(engine_config_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
|
331
|
-
open(engine_config_path, "a", encoding="utf-8").write("\n")
|
|
331
|
+
with open(engine_config_path, "w", encoding="utf-8") as handle:
|
|
332
|
+
json.dump(engine_config, handle, ensure_ascii=False, indent=2)
|
|
333
|
+
handle.write("\n")
|
|
332
334
|
print(f" 引擎配置 : {engine_config_path}")
|
|
333
335
|
|
|
334
336
|
removed = remove_patch_block(args.patch, args.dry_run)
|
|
@@ -355,7 +357,7 @@ def main() -> int:
|
|
|
355
357
|
if removed:
|
|
356
358
|
print(f"\n已从 {args.patch} 移除旧的 agent-teams 配置块(团队引擎现由 T专家 内置挂载)")
|
|
357
359
|
print(f" 成员人格体积: {total} 字符({len(profiles)} 支小队)")
|
|
358
|
-
print("\n下一步:重启 DSH Desktop
|
|
360
|
+
print("\n下一步:重启 DSH Desktop 让新配置生效,然后就能用:")
|
|
359
361
|
for key, profile in profiles.items():
|
|
360
362
|
print(f" /t {profile['aliases'][0] if profile['aliases'] else key} <目标> 或 /t --profile {key} <目标>")
|
|
361
363
|
return 0
|