dsh-plugin-t-expert 0.3.72 → 0.3.88

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/command.js CHANGED
@@ -133,6 +133,13 @@ function relayRouteDirective(input, list) {
133
133
  `【T专家 · 未指定小队】用户敲的是:\`/t ${input}\`,其中没有能匹配到小队名的词。`,
134
134
  `请把「${input}」当作目标,从下面的小队里挑最合适的一个,然后**直接调用 t_team_create 并带上 profile="<key>"** 建队(approval=\"required\" 先 staged,等用户在面板 review);`,
135
135
  "不要反问用户该用哪个小队,也不要自己手搭成员——profile 已带好成员与人格。",
136
+ // profile 描述的是「这类目标通常涉及哪些角色」,不是「这次必须全员上场」。不写清这一条,
137
+ // 队长会把它读成「roster 不可动」,于是为了不让人闲着而给边缘角色硬造任务:实测一份单文件
138
+ // HTML 简历被派给了 5 个人 —— 后端架构师去写「信息架构」、数据库性能工程师去查「A4 分页」。
139
+ // 那次的队长思考里两处原话正是「有点牵强」「这有点为凑人头造工作」,但它仍然照派了。
140
+ "但 profile 是「编制清单」,不等于这次全员都得上场:挑定小队后先逐人自问「这次真的需要这个角色的独有产出吗」,用不上的在 staged 阶段用 t_team_edit_plan 的 remove_member 裁掉。",
141
+ "顺序必须是**先裁人、后建任务**——任务一旦引用了成员,再删人就得先改依赖引用。拿不准的一律保留,且至少留 2 人(一个产出者 + 一个独立验证者,否则独立验证无从谈起)。",
142
+ "无论裁没裁,都要在给用户的计划说明里写明留了谁、裁了谁、为什么。",
136
143
  "若确实没有合适的小队,就用中文说明原因并列出可用小队。",
137
144
  "",
138
145
  list,
package/lib/index.js CHANGED
@@ -1991,11 +1991,9 @@ export function apply(ctx, config) {
1991
1991
  },
1992
1992
  });
1993
1993
 
1994
- // ---- 随包 skill(运维入口 + DeepSeek Harness 项目知识)----
1995
- // 都只对特定任务有用,所以不占常驻提示段,改成按需加载的 skill:
1994
+ // ---- 随包 skill(运维入口)----
1995
+ // 它只对特定任务有用,所以不占常驻提示段,改成按需加载的 skill:
1996
1996
  // · t-expert-manager 维护这个插件的人:名册 / 小队 / 装机 / 发布
1997
- // · dsh-harness-project 在 deepseek-harness 检出里读写代码:架构、启动模型、扩展点、门禁
1998
- // · dsh-harness-languages 同一个仓库里的各语言面规则与工具链
1999
1997
  // 可选依赖:宿主没有 skill 注册表时静默跳过,不影响上面任何功能。
2000
1998
  installBundledSkills(ctx, config);
2001
1999
  }
package/lib/remote.js CHANGED
@@ -58,7 +58,7 @@ function descriptor(method, parameters, typeSymbol, schema) {
58
58
  method,
59
59
  invocation: { kind: "direct" },
60
60
  parameters,
61
- result: { mode: "strict", typeSymbol, schema },
61
+ result: { mode: "strict", typeSymbol, schema, create: () => schema },
62
62
  };
63
63
  }
64
64
 
@@ -72,7 +72,7 @@ const DESCRIPTORS = [
72
72
  name: "patch",
73
73
  wire: "patch",
74
74
  source: "json",
75
- codec: { mode: "strict", typeSymbol: "TTeamUiPrefsPatch", schema: uiPrefsPatchSchema },
75
+ codec: { mode: "strict", typeSymbol: "TTeamUiPrefsPatch", schema: uiPrefsPatchSchema, create: () => uiPrefsPatchSchema },
76
76
  },
77
77
  ], "TTeamUiPrefs", uiPrefsSchema),
78
78
  // 定时事项:全部写操作都回传刷新后的整份快照,面板拿到就能直接重绘(与名册/小队同一口径)。
@@ -82,7 +82,7 @@ const DESCRIPTORS = [
82
82
  name: "input",
83
83
  wire: "input",
84
84
  source: "json",
85
- codec: { mode: "strict", typeSymbol: "TTeamScheduleInput", schema: scheduleInputSchema },
85
+ codec: { mode: "strict", typeSymbol: "TTeamScheduleInput", schema: scheduleInputSchema, create: () => scheduleInputSchema },
86
86
  },
87
87
  ], "TTeamSchedule", scheduleSnapshotSchema),
88
88
  descriptor("updateScheduleItem", [
@@ -90,13 +90,13 @@ const DESCRIPTORS = [
90
90
  name: "id",
91
91
  wire: "id",
92
92
  source: "json",
93
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
93
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128), create: () => z.string().min(1).max(128) },
94
94
  },
95
95
  {
96
96
  name: "patch",
97
97
  wire: "patch",
98
98
  source: "json",
99
- codec: { mode: "strict", typeSymbol: "TTeamSchedulePatch", schema: schedulePatchSchema },
99
+ codec: { mode: "strict", typeSymbol: "TTeamSchedulePatch", schema: schedulePatchSchema, create: () => schedulePatchSchema },
100
100
  },
101
101
  ], "TTeamSchedule", scheduleSnapshotSchema),
102
102
  descriptor("deleteScheduleItem", [
@@ -104,7 +104,7 @@ const DESCRIPTORS = [
104
104
  name: "id",
105
105
  wire: "id",
106
106
  source: "json",
107
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
107
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128), create: () => z.string().min(1).max(128) },
108
108
  },
109
109
  ], "TTeamSchedule", scheduleSnapshotSchema),
110
110
  descriptor("runScheduleItem", [
@@ -112,7 +112,7 @@ const DESCRIPTORS = [
112
112
  name: "id",
113
113
  wire: "id",
114
114
  source: "json",
115
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
115
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128), create: () => z.string().min(1).max(128) },
116
116
  },
117
117
  ], "TTeamSchedule", scheduleSnapshotSchema),
118
118
  descriptor("previewSchedule", [
@@ -120,7 +120,7 @@ const DESCRIPTORS = [
120
120
  name: "cron",
121
121
  wire: "cron",
122
122
  source: "json",
123
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(120) },
123
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(120), create: () => z.string().min(1).max(120) },
124
124
  },
125
125
  ], "TTeamSchedulePreview", schedulePreviewSchema),
126
126
  descriptor("ensureScheduleWorkspace", [
@@ -128,7 +128,7 @@ const DESCRIPTORS = [
128
128
  name: "path",
129
129
  wire: "path",
130
130
  source: "json",
131
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(1024) },
131
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(1024), create: () => z.string().max(1024) },
132
132
  },
133
133
  ], "TTeamScheduleWorkspace", scheduleWorkspaceSchema),
134
134
  descriptor("setEnabled", [
@@ -136,13 +136,13 @@ const DESCRIPTORS = [
136
136
  name: "enabled",
137
137
  wire: "enabled",
138
138
  source: "json",
139
- codec: { mode: "strict", typeSymbol: "string[]", schema: z.array(z.string()) },
139
+ codec: { mode: "strict", typeSymbol: "string[]", schema: z.array(z.string()), create: () => z.array(z.string()) },
140
140
  },
141
141
  {
142
142
  name: "expectedRevision",
143
143
  wire: "expectedRevision",
144
144
  source: "json",
145
- codec: { mode: "strict", typeSymbol: "number", schema: z.number().int().min(0) },
145
+ codec: { mode: "strict", typeSymbol: "number", schema: z.number().int().min(0), create: () => z.number().int().min(0) },
146
146
  },
147
147
  ], "TTeamEnabledState", enabledStateSchema),
148
148
  descriptor("getPrompt", [
@@ -150,13 +150,13 @@ const DESCRIPTORS = [
150
150
  name: "slug",
151
151
  wire: "slug",
152
152
  source: "json",
153
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
153
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128), create: () => z.string().min(1).max(128) },
154
154
  },
155
155
  {
156
156
  name: "division",
157
157
  wire: "division",
158
158
  source: "json",
159
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(64) },
159
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(64), create: () => z.string().min(1).max(64) },
160
160
  },
161
161
  ], "TTeamPrompt", promptSchema),
162
162
  descriptor("getSquads", [], "TTeamSquads", squadsSnapshotSchema),
@@ -165,13 +165,13 @@ const DESCRIPTORS = [
165
165
  name: "squads",
166
166
  wire: "squads",
167
167
  source: "json",
168
- codec: { mode: "strict", typeSymbol: "TTeamSquadInput[]", schema: z.array(squadInputSchema).min(1).max(48) },
168
+ codec: { mode: "strict", typeSymbol: "TTeamSquadInput[]", schema: z.array(squadInputSchema).min(1).max(48), create: () => z.array(squadInputSchema).min(1).max(48) },
169
169
  },
170
170
  {
171
171
  name: "expectedRevision",
172
172
  wire: "expectedRevision",
173
173
  source: "json",
174
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(64) },
174
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(64), create: () => z.string().max(64) },
175
175
  },
176
176
  ], "TTeamSquads", squadsSnapshotSchema),
177
177
  descriptor("startSquad", [
@@ -179,13 +179,13 @@ const DESCRIPTORS = [
179
179
  name: "profileKey",
180
180
  wire: "profileKey",
181
181
  source: "json",
182
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(64) },
182
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(64), create: () => z.string().min(1).max(64) },
183
183
  },
184
184
  {
185
185
  name: "sessionId",
186
186
  wire: "sessionId",
187
187
  source: "json",
188
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(128) },
188
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(128), create: () => z.string().max(128) },
189
189
  },
190
190
  ], "TTeamStartResult", startResultSchema),
191
191
  descriptor("getTeams", [
@@ -193,7 +193,7 @@ const DESCRIPTORS = [
193
193
  name: "archived",
194
194
  wire: "archived",
195
195
  source: "json",
196
- codec: { mode: "strict", typeSymbol: "boolean", schema: z.boolean() },
196
+ codec: { mode: "strict", typeSymbol: "boolean", schema: z.boolean(), create: () => z.boolean() },
197
197
  },
198
198
  ], "TTeamTeams", teamsSnapshotSchema),
199
199
  // teamId 是**团队名 slug 化**的产物,跨工作区会重名;workspacePath 让宿主精确定位到
@@ -207,14 +207,14 @@ const DESCRIPTORS = [
207
207
  name: "teamId",
208
208
  wire: "teamId",
209
209
  source: "json",
210
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
210
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128), create: () => z.string().min(1).max(128) },
211
211
  },
212
212
  {
213
213
  name: "workspacePath",
214
214
  wire: "workspacePath",
215
215
  source: "json",
216
216
  acceptsUndefined: true,
217
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(1024) },
217
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(1024), create: () => z.string().max(1024) },
218
218
  },
219
219
  ], "TTeamStopResult", stopResultSchema),
220
220
  // 彻底删除一支团队(运行中的与已归档的都收):设置页「运行中的团队」那一行的「删除」按钮。
@@ -223,14 +223,14 @@ const DESCRIPTORS = [
223
223
  name: "teamId",
224
224
  wire: "teamId",
225
225
  source: "json",
226
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
226
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128), create: () => z.string().min(1).max(128) },
227
227
  },
228
228
  {
229
229
  name: "workspacePath",
230
230
  wire: "workspacePath",
231
231
  source: "json",
232
232
  acceptsUndefined: true,
233
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(1024) },
233
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(1024), create: () => z.string().max(1024) },
234
234
  },
235
235
  ], "TTeamDeleteResult", deleteTeamResultSchema),
236
236
  // 自建专家的增删改:都回传刷新后的整份名册快照,面板拿到就能直接重绘。
@@ -239,7 +239,7 @@ const DESCRIPTORS = [
239
239
  name: "expert",
240
240
  wire: "expert",
241
241
  source: "json",
242
- codec: { mode: "strict", typeSymbol: "TTeamCustomExpertInput", schema: customExpertInputSchema },
242
+ codec: { mode: "strict", typeSymbol: "TTeamCustomExpertInput", schema: customExpertInputSchema, create: () => customExpertInputSchema },
243
243
  },
244
244
  ], "TTeamCatalog", catalogSnapshotSchema),
245
245
  descriptor("updateExpert", [
@@ -247,19 +247,19 @@ const DESCRIPTORS = [
247
247
  name: "slug",
248
248
  wire: "slug",
249
249
  source: "json",
250
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
250
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128), create: () => z.string().min(1).max(128) },
251
251
  },
252
252
  {
253
253
  name: "expert",
254
254
  wire: "expert",
255
255
  source: "json",
256
- codec: { mode: "strict", typeSymbol: "TTeamCustomExpertInput", schema: customExpertInputSchema },
256
+ codec: { mode: "strict", typeSymbol: "TTeamCustomExpertInput", schema: customExpertInputSchema, create: () => customExpertInputSchema },
257
257
  },
258
258
  {
259
259
  name: "expectedHash",
260
260
  wire: "expectedHash",
261
261
  source: "json",
262
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(64) },
262
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(64), create: () => z.string().max(64) },
263
263
  },
264
264
  ], "TTeamCatalog", catalogSnapshotSchema),
265
265
  descriptor("deleteExpert", [
@@ -267,13 +267,13 @@ const DESCRIPTORS = [
267
267
  name: "slug",
268
268
  wire: "slug",
269
269
  source: "json",
270
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
270
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128), create: () => z.string().min(1).max(128) },
271
271
  },
272
272
  {
273
273
  name: "expectedHash",
274
274
  wire: "expectedHash",
275
275
  source: "json",
276
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(64) },
276
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(64), create: () => z.string().max(64) },
277
277
  },
278
278
  ], "TTeamCatalog", catalogSnapshotSchema),
279
279
  // 分类管理:都回传刷新后的整份名册快照(面板拿到就能直接重绘)。
@@ -282,13 +282,13 @@ const DESCRIPTORS = [
282
282
  name: "key",
283
283
  wire: "key",
284
284
  source: "json",
285
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
285
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128), create: () => z.string().min(1).max(128) },
286
286
  },
287
287
  {
288
288
  name: "label",
289
289
  wire: "label",
290
290
  source: "json",
291
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(40) },
291
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(40), create: () => z.string().max(40) },
292
292
  },
293
293
  ], "TTeamCatalog", catalogSnapshotSchema),
294
294
  descriptor("updateCategory", [
@@ -296,13 +296,13 @@ const DESCRIPTORS = [
296
296
  name: "key",
297
297
  wire: "key",
298
298
  source: "json",
299
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
299
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128), create: () => z.string().min(1).max(128) },
300
300
  },
301
301
  {
302
302
  name: "label",
303
303
  wire: "label",
304
304
  source: "json",
305
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(40) },
305
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(40), create: () => z.string().max(40) },
306
306
  },
307
307
  ], "TTeamCatalog", catalogSnapshotSchema),
308
308
  descriptor("deleteCategory", [
@@ -310,7 +310,7 @@ const DESCRIPTORS = [
310
310
  name: "key",
311
311
  wire: "key",
312
312
  source: "json",
313
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
313
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128), create: () => z.string().min(1).max(128) },
314
314
  },
315
315
  ], "TTeamCatalog", catalogSnapshotSchema),
316
316
  ];
package/lib/schedule.js CHANGED
@@ -5,8 +5,8 @@
5
5
  * 为什么自建而不是直接依赖 `@weibaohui/dsh-tasks`:用户明确要求两个插件可以同时安装、
6
6
  * 同时可用。所以这里与它**零共享**——
7
7
  * · 数据不落它的 `dsh_tasks` 存储域,而是 T专家 自己的数据目录(`<root 同级>/schedule.json`);
8
- * · 服务名 `tTeamSchedule`、错误码前缀 `tTeam/schedule-*`、消息来源 `plugin: dsh-plugin-t-expert`
9
- * 全部独立,两个插件的任务互不可见、互不干扰。
8
+ * · 服务名 `tTeamSchedule`、错误码前缀 `tTeam/schedule-*` 全部独立,两个插件的任务互不可见、互不干扰。
9
+ * 提交给会话的提示词用 `source.kind: "user"`(聊天右侧才渲染用户气泡;`plugin` 源会被收成 inject)。
10
10
  *
11
11
  * 三件事在这里闭环:
12
12
  * 1. 持久化:一个 JSON 文件,原子写(与 `teams.json` 同级、同风格);
@@ -284,11 +284,14 @@ export function createScheduleEngine(options) {
284
284
  } catch (error) {
285
285
  logger?.warn?.(`[t-team] 定时任务「${item.title}」的提示词改写失败,按原文提交:${error instanceof Error ? error.message : String(error)}`);
286
286
  }
287
+ // 必须用 source.kind: "user":DSH 聊天把非 user 的 user/message 收成左侧
288
+ // inject/context(plugin 源只显示插件名,正文不进右侧气泡)。定时任务的指令
289
+ // 本身就是用户写的提示词,应当像手打那条一样出现在对话右侧。
287
290
  handle.agent.followup({
288
291
  id: randomUUID(),
289
292
  role: "user",
290
293
  content: [{ type: "text", text: promptText }],
291
- source: { kind: "plugin", plugin: PLUGIN_ID },
294
+ source: { kind: "user" },
292
295
  });
293
296
  return withRun(item, { at: startedAt, ok: true, sessionId });
294
297
  } catch (error) {
package/lib/skill.js CHANGED
@@ -1,18 +1,14 @@
1
1
  // @ts-check
2
2
  /**
3
- * 随包发布的 skill:把「T专家 运维」与「DeepSeek Harness 项目知识」做成**随 npm 包分发**的 skill。
3
+ * 随包发布的 skill:把「T专家 运维」做成**随 npm 包分发**的 skill。
4
4
  *
5
- * 为什么是 skill 而不是再加一段 systemPrompt:这些内容只在特定任务里需要(改名册/小队/装机/发布,
6
- * 或在一个 deepseek-harness 检出里读写代码),常驻提示段是纯浪费 token;skill 的 description 本来就是路由面。
5
+ * 为什么是 skill 而不是再加一段 systemPrompt:这些内容只在特定任务里需要(改名册/小队/装机/发布),
6
+ * 常驻提示段是纯浪费 token;skill 的 description 本来就是路由面。
7
7
  *
8
8
  * 形态:`<包根>/skills/<name>/SKILL.md` 是标准 skill 文件(任何 skill 扫描器都能直接发现),
9
9
  * 这里额外把它们注册进宿主的 skill 注册表,这样 npm 安装(插件在 node_modules 里、没有运维台)也能用。
10
10
  *
11
- * 三个 skill 的来源:
12
- * - `t-expert-manager`:本仓运维入口,正文带占位符,注册时按本机布局渲染。
13
- * - `dsh-harness-project` / `dsh-harness-languages`:`dsh-project-expert` agent preset 里那两个
14
- * 项目知识 skill 的**逐字节拷贝**(preset 是源,这里是随插件发布的副本),
15
- * 所以插件用户在任何仓库里都能加载这套 harness 项目知识。
11
+ * 唯一的 skill 是 `t-expert-manager`:本仓运维入口,正文带占位符,注册时按本机布局渲染。
16
12
  *
17
13
  * 两条刻意的设计:
18
14
  * 1. **可选依赖**:走 `ctx.inject(["skills"], …)`,而不是把 `skills` 加进插件的静态 `inject`。
@@ -33,9 +29,9 @@ export const SKILLS_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."
33
29
  * 随包发布的 skill 清单(目录名 = 宿主路由用的 skill 名)。
34
30
  *
35
31
  * 顺序即注册顺序。缺文件的条目会被跳过并逐条告警,而不是让整段注册失败 ——
36
- * 一个 skill 文件缺失不该带走另外两个(见 {@link LOCAL_ONLY_SKILL_NAMES} 的例外)。
32
+ * 一个 skill 文件缺失不该带走其余(见 {@link LOCAL_ONLY_SKILL_NAMES} 的例外)。
37
33
  */
38
- export const BUNDLED_SKILL_NAMES = ["t-expert-manager", "dsh-harness-project", "dsh-harness-languages"];
34
+ export const BUNDLED_SKILL_NAMES = ["t-expert-manager"];
39
35
 
40
36
  /**
41
37
  * **只在本机布局里存在**的 skill:不进 npm 包(见 `package.json` 的 `files`)。
@@ -204,7 +200,7 @@ export function buildBundledSkills(config = {}) {
204
200
  /**
205
201
  * 把随包 skill 注册进宿主。`skills` 服务缺席时什么都不做(可选依赖,见文件头注释)。
206
202
  *
207
- * 缺失条目**逐条 warn**(一个 skill 文件缺失不该带走另外两个,但也不能悄悄消失);
203
+ * 缺失条目**逐条 warn**(一个 skill 文件缺失不该带走其余,但也不能悄悄消失);
208
204
  * 全军覆没时额外再给一条汇总 warn,保持既有的「都读不到」信号。
209
205
  * @param ctx - 插件上下文。
210
206
  * @param config - 插件配置。
@@ -217,7 +213,10 @@ export function installBundledSkills(ctx, config) {
217
213
  ctx.logger?.warn?.(`[t-team] 随包 skill「${item.name}」未注册:${item.reason}(其余 skill 照常注册)`);
218
214
  }
219
215
  if (skills.length === 0) {
220
- ctx.logger?.warn?.(`[t-team] 随包 skill 未注册:读不到 ${join(SKILLS_ROOT, "<name>", "SKILL.md")}`);
216
+ /* 没有 `missing` 就说明缺席的全是 LOCAL_ONLY:那是 npm 安装的预期状态,出声反而是噪音。 */
217
+ if (missing.length > 0) {
218
+ ctx.logger?.warn?.(`[t-team] 随包 skill 未注册:读不到 ${join(SKILLS_ROOT, "<name>", "SKILL.md")}`);
219
+ }
221
220
  return 0;
222
221
  }
223
222
  ctx.inject(["skills"], (scoped) => {
@@ -60,10 +60,14 @@ export function buildActivationDirective(goal, profile, taskPlanning = 'seed') {
60
60
  if (profile !== undefined) {
61
61
  lines.push(`Use profile="${profile}" when creating a new team.`);
62
62
  if (taskPlanning === 'captain') {
63
- lines.push('This profile supplies the roster and guardrails. After create, do not recreate members.', 'Derive the smallest useful task graph from the goal while the team is staged; do not ask the user whether to split, merge, serialize, or parallelize.', 'Independent supplemental work must become separate ready tasks so idle members can run in parallel. Add dependencies only for genuine prerequisites and later synthesis.');
63
+ // 编制授权:profile 是「这类目标通常涉及哪些角色」,不是「这次必须全员上场」。
64
+ // 缺了这句,队长会把上面那句 "do not recreate members" 读成「roster 不可动」,转而给
65
+ // 边缘角色硬造任务;而下面那句原文(idle members can run in parallel)更是在明示
66
+ // 「给闲着的成员找活」—— 实测一份单文件 HTML 简历就是这样被派给 5 个人的。
67
+ lines.push('This profile supplies the roster and guardrails. After create, do not recreate members.', 'The roster is a staffing list, not a mandate to field everyone: drop any role whose distinct output this goal does not need (t_team_edit_plan remove_member while staged), and trim BEFORE creating tasks — once a task references a member, removing that member means fixing references first. When unsure, keep the role. Keep at least two members (a producer plus an independent verifier). State in the plan whom you kept, whom you trimmed, and why.', 'Derive the smallest useful task graph from the goal while the team is staged; do not ask the user whether to split, merge, serialize, or parallelize.', 'An idle member is not a reason to invent work: add a task only when the goal itself needs that output, and say why. When work genuinely is independent, make it a separate ready task so it can run in parallel instead of serializing it. Add dependencies only for genuine prerequisites and later synthesis.');
64
68
  }
65
69
  else {
66
- lines.push('Do not recreate the same members or seed tasks manually.');
70
+ lines.push('Do not recreate the same members or seed tasks manually.', 'You may still drop a role this goal does not need (t_team_edit_plan remove_member while the team is staged); state in the plan whom you kept, whom you trimmed, and why.');
67
71
  }
68
72
  }
69
73
  lines.push(goal === '' ? 'The goal was not given — ask the user what the team should accomplish.' : `Goal: ${goal}`);
@@ -92,10 +92,10 @@ export function usageSectionText(toolNames, profilesText = '') {
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
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
- 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.
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. A member report already states what changed, so do not call status again merely to confirm it; call it when you need the parts the report does not cover (the current graph, other members' state).
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.
98
- 8. When full quality mode is requested: requirements → implementation → verification → review → integration. Plan the entire DAG while staged, including implementation before requirements finishes and integration depending on review round 1. Failed review automatically adds repair + next review and rewires pending downstream gates. Do not recreate this loop, omit integration or depend on a failed task. Review acceptance judges the latest implementation. Do not put smoke-test scripts into task instructions.
98
+ 8. When full quality mode is requested: requirements → implementation → verification → review → integration. Plan the entire DAG while staged, including implementation before requirements finishes and integration depending on review round 1. Failed review automatically adds repair + next review and rewires pending downstream gates. Do not recreate this loop, omit integration or depend on a failed task. The automatic repair task already carries the review findings and is already assigned to the implementer, so do NOT relay those findings through send_message: the implementer has them, and the relay only makes it stop and explain that the repair is already done. Review acceptance judges the latest implementation. Do not put smoke-test scripts into task instructions.
99
99
  9. Halted means the user stopped work (including the captain turn). Resume only on a later explicit user request with a reason, via t_team_resume or create_task({resume:true,resumeReason}); creating tasks alone never resumes. Escalated means the review loop hit its limit, not a halt. Deployment requires explicit user confirmation.
100
100
  10. Wait for all required tasks to be terminal and members idle/ready, present results, then delete/archive unless the user wants to continue. Never discard unfinished work without authorization.
101
101
  Tools: ${toolNames}${profilesText === '' ? '' : `\n\n${profilesText}`}`;
@@ -447,8 +447,8 @@ Working rules:
447
447
  - include a concise output in either case;
448
448
  - a stale-attempt rejection means the captain reassigned or took over the task; stop touching that task and wait for new work.
449
449
  claimed cannot jump to completed. Mark in_progress first, then completed or failed.
450
- Include attempt_id on every update. Then send_message to captain and become idle.
451
- 4. Send a short report to the captain with t_team_send_message (to=captain) when you complete a task or hit a blocker.
450
+ Include attempt_id on every update, then become idle.
451
+ 4. Write the report exactly once. t_team_update_task IS your report: the captain and every downstream task read output/summary/tried from it. Then send_message (to=captain) a ONE-LINE notice ("<the task id> completed — result in the task output") — that line is what wakes the captain, it must not restate the report — and keep your closing turn message to ONE line as well, because it is delivered to the captain in full. A completion written out three times costs three times the context and says nothing new. Use a longer send_message only for what the task record cannot carry: a blocker, or a decision only the captain can make.
452
452
  5. To ask a teammate something, use t_team_send_message with to=<teammate name>; the message lands in their mailbox and wakes them directly — teammates talk to each other without the captain in the loop. The same applies to the captain (to=captain).
453
453
  6. After your turn becomes idle, the shared task scheduler may assign your next ready task automatically. Never claim a second task while you still own unfinished work.
454
454
  7. If you already own an open attempt (claimed or in_progress) and receive mail, treat it as guidance for that same attempt_id unless the mail explicitly tells you to stop or fail. Do not claim a new task in that turn.
@@ -682,7 +682,7 @@ export function isCommandResult(value) {
682
682
  // optional tool parameters as "" instead of omitting them (e.g. GPT-5.6
683
683
  // sending reviewedTaskId:"" or profile:""), which would otherwise be written
684
684
  // to team.json and then brick the whole team state on reload.
685
- const BLANK_SENSITIVE_STRING_FIELDS = ['objective', 'reviewedTaskId', 'sourceTaskId'];
685
+ const BLANK_SENSITIVE_STRING_FIELDS = ['objective', 'reviewedTaskId', 'sourceTaskId', 'summary'];
686
686
  const BLANK_SENSITIVE_STRING_LIST_FIELDS = [
687
687
  'inScope',
688
688
  'outOfScope',
@@ -693,6 +693,7 @@ const BLANK_SENSITIVE_STRING_LIST_FIELDS = [
693
693
  'changedPaths',
694
694
  'sourceFindingIds',
695
695
  'coverageOf',
696
+ 'tried',
696
697
  ];
697
698
  /**
698
699
  * Normalize blank optional task fields to omitted ("blank means absent").
@@ -735,10 +736,15 @@ export function hasValidQualityTaskFields(value) {
735
736
  return false;
736
737
  if (value['sourceTaskId'] !== undefined && !nonemptyString(value['sourceTaskId']))
737
738
  return false;
739
+ // 交接契约(2026-09-20):`summary` 是给下游看的压缩结论,`tried` 是走过的死路。
740
+ // 只校验形状、**不限长度** —— 过长的摘要在渲染时截断即可,绝不能因为写多了就让一次
741
+ // 已完成的工作被判成非法状态(那会连带整份 team.json 在重载时失效)。
742
+ if (value['summary'] !== undefined && !nonemptyString(value['summary']))
743
+ return false;
738
744
  if (value['reviewedAttempt'] !== undefined && !(Number.isSafeInteger(value['reviewedAttempt']) && value['reviewedAttempt'] >= 0)) {
739
745
  return false;
740
746
  }
741
- const stringLists = ['inScope', 'outOfScope', 'acceptance', 'verify', 'deliverables', 'nonGoals', 'changedPaths', 'sourceFindingIds', 'coverageOf'];
747
+ const stringLists = ['inScope', 'outOfScope', 'acceptance', 'verify', 'deliverables', 'nonGoals', 'changedPaths', 'sourceFindingIds', 'coverageOf', 'tried'];
742
748
  for (const key of stringLists) {
743
749
  if (value[key] === undefined)
744
750
  continue;