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/THIRD-PARTY-NOTICES +1 -1
- package/data/experts/engineering/engineering-deepseek-harness-project-expert.md +5 -5
- package/lib/client.js +704 -518
- package/lib/command.js +7 -0
- package/lib/index.js +2 -4
- package/lib/remote.js +33 -33
- package/lib/schedule.js +6 -3
- package/lib/skill.js +11 -12
- package/lib/teams/command.js +6 -2
- package/lib/teams/index.js +2 -2
- package/lib/teams/members.js +2 -2
- package/lib/teams/quality-gates.js +8 -2
- package/lib/teams/scheduler.js +207 -14
- package/lib/teams/tools.js +12 -0
- package/package.json +1 -3
- package/skills/dsh-harness-languages/SKILL.md +0 -175
- package/skills/dsh-harness-project/SKILL.md +0 -236
package/lib/teams/scheduler.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { deliverToMember } from "./members.js";
|
|
16
16
|
import { acknowledgeMailbox, beginTaskAttempt, CAPTAIN_KEY, claimMailboxDelivery, findTeamByParticipant, invalidateTaskAttempt, readTeam, readUnreadMailbox, releaseMailboxDelivery, unsatisfiedDependencies, withTeamLock, writeTeam, } from "./state.js";
|
|
17
|
+
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
17
18
|
/** Per-dependency output cap in the assignment prompt. */
|
|
18
19
|
export const DEPENDENCY_OUTPUT_MAX_CHARS = 2_000;
|
|
19
20
|
/** Combined dependency-output budget in the assignment prompt. */
|
|
@@ -31,6 +32,9 @@ function teamProfileProtocol(team) {
|
|
|
31
32
|
*/
|
|
32
33
|
export function collectCompletedDependencyOutputs(tasks, taskId, warn) {
|
|
33
34
|
const byId = new Map(tasks.map(task => [task.id, task]));
|
|
35
|
+
// 直接依赖 vs 传递性祖先:前者是这条任务的**直接输入**(给全文),后者只是背景
|
|
36
|
+
// (给摘要)。不区分的话,一条深链会用祖先的全文把预算吃光,把真正的直接输入挤掉。
|
|
37
|
+
const direct = new Set(byId.get(taskId)?.dependencies ?? []);
|
|
34
38
|
const visiting = new Set();
|
|
35
39
|
const visited = new Set();
|
|
36
40
|
const ordered = [];
|
|
@@ -60,23 +64,64 @@ export function collectCompletedDependencyOutputs(tasks, taskId, warn) {
|
|
|
60
64
|
return {
|
|
61
65
|
id: task.id,
|
|
62
66
|
subject: task.subject,
|
|
67
|
+
direct: direct.has(task.id),
|
|
63
68
|
...profileSeedId === undefined ? {} : { profileSeedId },
|
|
64
69
|
...task.output === undefined ? {} : { output: task.output },
|
|
70
|
+
...task.summary === undefined ? {} : { summary: task.summary },
|
|
71
|
+
...Array.isArray(task.tried) && task.tried.length > 0 ? { tried: task.tried } : {},
|
|
65
72
|
};
|
|
66
73
|
});
|
|
67
74
|
}
|
|
68
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* 与本任务**并行在途**的其它任务(claimed / in_progress)。
|
|
77
|
+
*
|
|
78
|
+
* 为什么要给成员看见:两条并行任务即使写域(inScope)不重叠、闸门放行,也可能做出互相
|
|
79
|
+
* 冲突的架构或风格决策 —— Cognition 记录过的经典形态是两个子代理并行做同一个游戏,一个
|
|
80
|
+
* 画角色、一个画背景,风格完全对不上,因为彼此看不到对方的决策。事后由 integration 任务
|
|
81
|
+
* 发现时返工成本已经产生。这里只用现成的任务表告诉成员「此刻还有谁在并行决定什么」,
|
|
82
|
+
* 不新增任何状态,也不要求它们互相等待。
|
|
83
|
+
*/
|
|
84
|
+
export function collectInFlightSiblings(tasks, taskId) {
|
|
85
|
+
return tasks
|
|
86
|
+
.filter(task => task.id !== taskId
|
|
87
|
+
&& (task.status === 'claimed' || task.status === 'in_progress'))
|
|
88
|
+
.map(task => ({
|
|
89
|
+
id: task.id,
|
|
90
|
+
subject: task.subject,
|
|
91
|
+
assignee: task.assignee ?? '',
|
|
92
|
+
...Array.isArray(task.inScope) && task.inScope.length > 0 ? { inScope: task.inScope } : {},
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Format completed-dependency outputs with per-item and total truncation.
|
|
97
|
+
*
|
|
98
|
+
* 交接契约(2026-09-20):**直接依赖给全文,传递性祖先优先给 `summary`**。
|
|
99
|
+
* 过去一律传 `output` 全文再按字符数硬截断,于是长链上的祖先会把预算吃光,而下游真正
|
|
100
|
+
* 需要的直接输入反倒被切掉;截断点还落在句子中间。有 `summary` 的祖先改传那一段压缩
|
|
101
|
+
* 结论(它正是写给下游看的),信息密度高得多。
|
|
102
|
+
*
|
|
103
|
+
* `tried`(走过的死路)**无论直接与否都附上**:它的价值就是阻止下游重复同一条死路,
|
|
104
|
+
* 而重复死路恰恰最常发生在隔了一层的任务之间。
|
|
105
|
+
*/
|
|
69
106
|
export function formatDependencyOutputs(items) {
|
|
70
107
|
if (items.length === 0)
|
|
71
108
|
return '(none)';
|
|
72
109
|
const formatted = items.map((item) => {
|
|
73
110
|
const seed = item.profileSeedId === undefined ? '' : ` [${item.profileSeedId}]`;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
111
|
+
// 祖先有摘要就用摘要;直接依赖始终用全文(缺全文时退回摘要)。
|
|
112
|
+
const preferSummary = item.direct !== true && typeof item.summary === 'string' && item.summary !== '';
|
|
113
|
+
const raw = preferSummary
|
|
114
|
+
? item.summary
|
|
115
|
+
: item.output === undefined || item.output === ''
|
|
116
|
+
? (item.summary ?? '(no output recorded)')
|
|
117
|
+
: item.output;
|
|
77
118
|
const truncated = raw.length > DEPENDENCY_OUTPUT_MAX_CHARS;
|
|
78
119
|
const body = truncated ? `${raw.slice(0, DEPENDENCY_OUTPUT_MAX_CHARS)} [truncated]` : raw;
|
|
79
|
-
|
|
120
|
+
const label = preferSummary ? ' (summary)' : '';
|
|
121
|
+
const tried = Array.isArray(item.tried) && item.tried.length > 0
|
|
122
|
+
? `\n Already tried and rejected: ${item.tried.join('; ')}`
|
|
123
|
+
: '';
|
|
124
|
+
return `- ${item.id}${seed} ${item.subject}${label}:\n ${body}${tried}`;
|
|
80
125
|
});
|
|
81
126
|
let selected = formatted;
|
|
82
127
|
while (selected.length > 1 && selected.join('\n').length > DEPENDENCY_OUTPUTS_TOTAL_MAX_CHARS) {
|
|
@@ -88,6 +133,26 @@ export function formatDependencyOutputs(items) {
|
|
|
88
133
|
}
|
|
89
134
|
return selected.join('\n');
|
|
90
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* 渲染「此刻并行在途的任务」。没有并行任务时返回空串(不占提示词预算)。
|
|
138
|
+
* 措辞刻意约束成**只用于对齐决策**:不许等待、不许插手,否则会退化成互相阻塞。
|
|
139
|
+
*/
|
|
140
|
+
export function formatInFlightSiblings(items) {
|
|
141
|
+
if (items === undefined || items.length === 0)
|
|
142
|
+
return '';
|
|
143
|
+
const lines = items.map((item) => {
|
|
144
|
+
const owner = item.assignee === '' ? 'unassigned' : item.assignee;
|
|
145
|
+
const scope = item.inScope === undefined || item.inScope.length === 0
|
|
146
|
+
? ''
|
|
147
|
+
: ` — in scope: ${item.inScope.join(', ')}`;
|
|
148
|
+
return `- ${item.id} ${item.subject} (${owner})${scope}`;
|
|
149
|
+
});
|
|
150
|
+
return `
|
|
151
|
+
Running in parallel right now (do NOT wait for these, do NOT work on them):
|
|
152
|
+
${lines.join('\n')}
|
|
153
|
+
Keep your choices compatible with theirs where your work meets. If your task forces a decision that would conflict with one of them, say so in your report instead of silently diverging.
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
91
156
|
function stateRootOf(workspace, config) {
|
|
92
157
|
return join(workspace, config.stateDir);
|
|
93
158
|
}
|
|
@@ -110,17 +175,138 @@ function ownedOpenTask(tasks, memberName) {
|
|
|
110
175
|
return tasks.find(task => task.assignee === memberName
|
|
111
176
|
&& (task.status === 'claimed' || task.status === 'in_progress'));
|
|
112
177
|
}
|
|
113
|
-
|
|
178
|
+
/**
|
|
179
|
+
* 剩余关键路径长度(upward rank):沿**尚未终结**的后继任务能延伸出的最长链长。
|
|
180
|
+
*
|
|
181
|
+
* 为什么需要它:就绪前沿常常同时有好几条任务,而过去是按**创建顺序**取第一条
|
|
182
|
+
* (`ready.find`)—— 先挑走一条叶子任务会把真正卡住后续工作的那条往后推,直接拉长
|
|
183
|
+
* makespan。rank 回答的正是「先做它能解开多少后续工作」,降序取就是经典 HEFT 的
|
|
184
|
+
* upward-rank 优先。
|
|
185
|
+
*
|
|
186
|
+
* 只数**未终结**的后继:completed/failed/cancelled 不再产生剩余工作量,把它们算进去
|
|
187
|
+
* 会让早已走完的分支虚高。无权重(每节点算 1):引擎没有工期估计,节点数是唯一可得
|
|
188
|
+
* 且稳定的代价度量。环(只可能出现在状态损坏时)按 0 截断该分支,绝不无限递归。
|
|
189
|
+
* @returns Map<taskId, rank>;叶子为 1,越大越关键。
|
|
190
|
+
*/
|
|
191
|
+
export function computeRemainingRanks(tasks) {
|
|
192
|
+
const byId = new Map(tasks.map(task => [task.id, task]));
|
|
193
|
+
const successors = new Map(tasks.map(task => [task.id, []]));
|
|
194
|
+
for (const task of tasks) {
|
|
195
|
+
for (const dependency of task.dependencies ?? [])
|
|
196
|
+
successors.get(dependency)?.push(task.id);
|
|
197
|
+
}
|
|
198
|
+
const ranks = new Map();
|
|
199
|
+
const visiting = new Set();
|
|
200
|
+
const rankOf = (id) => {
|
|
201
|
+
const cached = ranks.get(id);
|
|
202
|
+
if (cached !== undefined)
|
|
203
|
+
return cached;
|
|
204
|
+
if (visiting.has(id))
|
|
205
|
+
return 0; // 状态损坏出现环:这一支就此停住,不参与比较
|
|
206
|
+
visiting.add(id);
|
|
207
|
+
let deepest = 0;
|
|
208
|
+
for (const childId of successors.get(id) ?? []) {
|
|
209
|
+
const child = byId.get(childId);
|
|
210
|
+
if (child === undefined || TERMINAL_TASK_STATUSES.includes(child.status))
|
|
211
|
+
continue;
|
|
212
|
+
deepest = Math.max(deepest, rankOf(childId));
|
|
213
|
+
}
|
|
214
|
+
visiting.delete(id);
|
|
215
|
+
const rank = deepest + 1;
|
|
216
|
+
ranks.set(id, rank);
|
|
217
|
+
return rank;
|
|
218
|
+
};
|
|
219
|
+
for (const task of tasks)
|
|
220
|
+
rankOf(task.id);
|
|
221
|
+
return ranks;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* 亲和度词元:ASCII 词(≥3 字符)+ CJK 二元组。
|
|
225
|
+
*
|
|
226
|
+
* 中文没有分词,二元组是稳定又便宜的近似(「数据库性能工程师」→ 数据/据库/库性/性能/…);
|
|
227
|
+
* ASCII 那一半来自专家 slug(`engineering-frontend-developer` → engineering/frontend/
|
|
228
|
+
* developer),是更精确的一半。两者都只用现成字段,不需要给 48 支小队补任何声明。
|
|
229
|
+
*/
|
|
230
|
+
export function affinityTokens(text) {
|
|
231
|
+
const tokens = new Set();
|
|
232
|
+
const lower = String(text ?? '').toLowerCase();
|
|
233
|
+
for (const word of lower.match(/[a-z][a-z0-9]{2,}/gu) ?? [])
|
|
234
|
+
tokens.add(word);
|
|
235
|
+
for (const run of lower.replace(/[^\u4e00-\u9fff]+/gu, ' ').split(' ')) {
|
|
236
|
+
for (let index = 0; index + 2 <= run.length; index += 1)
|
|
237
|
+
tokens.add(run.slice(index, index + 2));
|
|
238
|
+
}
|
|
239
|
+
return tokens;
|
|
240
|
+
}
|
|
241
|
+
/** 成员画像词元:中文名 + 专家 slug(`member.role` 存的就是 slug)。 */
|
|
242
|
+
export function memberAffinityTokens(member) {
|
|
243
|
+
return affinityTokens(`${member?.name ?? ''} ${member?.role ?? ''}`);
|
|
244
|
+
}
|
|
245
|
+
/** 任务文本与成员画像的重合词元数(0 = 看不出关系)。 */
|
|
246
|
+
export function taskAffinity(task, memberTokens) {
|
|
247
|
+
if (memberTokens.size === 0)
|
|
248
|
+
return 0;
|
|
249
|
+
const text = [task?.subject, task?.objective, task?.description]
|
|
250
|
+
.filter(part => typeof part === 'string' && part !== '')
|
|
251
|
+
.join(' ');
|
|
252
|
+
let score = 0;
|
|
253
|
+
for (const token of affinityTokens(text)) {
|
|
254
|
+
if (memberTokens.has(token))
|
|
255
|
+
score += 1;
|
|
256
|
+
}
|
|
257
|
+
return score;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 给一个空闲成员挑下一条任务。
|
|
261
|
+
*
|
|
262
|
+
* 排序键(依次):
|
|
263
|
+
* 1. **剩余关键路径降序** —— makespan 优先,永远不为了「派得更对口」牺牲关键路径;
|
|
264
|
+
* 2. **成员亲和度降序** —— 只在同等关键度内起作用。小队成员是**异构领域专家**,
|
|
265
|
+
* 而共享池过去完全不看这一点(`member.role` 全项目只用在成员自己的人格提示里),
|
|
266
|
+
* 于是安全任务可能落到设计师头上。亲和度做**平级决胜**而不是硬过滤:既拿到专业
|
|
267
|
+
* 匹配的收益,又不可能把任务饿死,也不可能比旧行为更差;
|
|
268
|
+
* 3. **创建顺序** —— 前两项相同时保持稳定、可预测。
|
|
269
|
+
*
|
|
270
|
+
* 导出仅为让自检能直接钉住这三条排序键(与本文件 `assignmentPrompt` 等同样的做法):
|
|
271
|
+
* 这是**行为契约**,回退成旧的 `ready.find` 不会让任何文本断言变红。
|
|
272
|
+
*
|
|
273
|
+
* @param tasks 团队全部任务(rank 需要全图,不只是就绪集)
|
|
274
|
+
* @param member 当前空闲成员记录(要用到 `name` 与 `role`)
|
|
275
|
+
*/
|
|
276
|
+
export function nextReadyTask(tasks, member) {
|
|
277
|
+
const memberName = member.name;
|
|
114
278
|
const ready = tasks.filter(task => task.status === 'pending'
|
|
115
279
|
&& task.reassigning !== true
|
|
116
280
|
&& unsatisfiedDependencies([...tasks], task.dependencies).length === 0);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
281
|
+
if (ready.length === 0)
|
|
282
|
+
return undefined;
|
|
283
|
+
const ranks = computeRemainingRanks(tasks);
|
|
284
|
+
const memberTokens = memberAffinityTokens(member);
|
|
285
|
+
const position = new Map(tasks.map((task, index) => [task.id, index]));
|
|
286
|
+
// 亲和度**每条任务只算一次**:它要把任务文本分词,而比较器会被调用 O(n log n) 次 ——
|
|
287
|
+
// 写在比较器里等于把同一段文本反复分词几百遍(本可忽略,但没有任何理由这么做)。
|
|
288
|
+
const affinity = new Map();
|
|
289
|
+
const affinityOf = (task) => {
|
|
290
|
+
let score = affinity.get(task.id);
|
|
291
|
+
if (score === undefined) {
|
|
292
|
+
score = taskAffinity(task, memberTokens);
|
|
293
|
+
affinity.set(task.id, score);
|
|
294
|
+
}
|
|
295
|
+
return score;
|
|
296
|
+
};
|
|
297
|
+
const best = (candidates) => candidates
|
|
298
|
+
.slice()
|
|
299
|
+
.sort((left, right) => (ranks.get(right.id) ?? 0) - (ranks.get(left.id) ?? 0)
|
|
300
|
+
|| affinityOf(right) - affinityOf(left)
|
|
301
|
+
|| (position.get(left.id) ?? 0) - (position.get(right.id) ?? 0))
|
|
302
|
+
.at(0);
|
|
303
|
+
const assigned = ready.filter(task => task.assignee === memberName);
|
|
304
|
+
if (assigned.length > 0)
|
|
305
|
+
return best(assigned);
|
|
120
306
|
// 审计修复(#5 调度侧):共享池里的 review 任务不得分给被审任务的实现者本人
|
|
121
307
|
// (自审)。显式 assignee 已在创建/reassign 两道闸门挡住,这里挡住「调度器自动
|
|
122
308
|
// 把 review 派回实现者」这条剩余路径。
|
|
123
|
-
|
|
309
|
+
const pool = ready.filter(task => {
|
|
124
310
|
if (task.assignee !== undefined)
|
|
125
311
|
return false;
|
|
126
312
|
if ((task.kind ?? 'work') !== 'review')
|
|
@@ -128,6 +314,7 @@ function nextReadyTask(tasks, memberName) {
|
|
|
128
314
|
const reviewed = tasks.find(candidate => candidate.id === task.reviewedTaskId);
|
|
129
315
|
return reviewed?.assignee !== memberName;
|
|
130
316
|
});
|
|
317
|
+
return pool.length === 0 ? undefined : best(pool);
|
|
131
318
|
}
|
|
132
319
|
export function assignmentPrompt(ticket, stateDir, teamId) {
|
|
133
320
|
const description = ticket.description === undefined ? '' : `\n\n${ticket.description}`;
|
|
@@ -168,15 +355,20 @@ ${executionPrompt}
|
|
|
168
355
|
`}
|
|
169
356
|
Completed dependency results:
|
|
170
357
|
${formatDependencyOutputs(ticket.dependencyOutputs)}
|
|
171
|
-
|
|
358
|
+
${formatInFlightSiblings(ticket.inFlightSiblings)}
|
|
172
359
|
Task: ${ticket.taskId}${seed} — ${ticket.subject}${description}
|
|
173
360
|
${contract === '' ? '' : `\nContract:\n${contract}\n`}
|
|
174
361
|
${structuredCompletion}
|
|
175
362
|
Attempt: ${ticket.attempt}
|
|
176
363
|
Attempt id: ${ticket.attemptId}
|
|
177
364
|
|
|
178
|
-
Call t_team_claim_task for ${ticket.taskId}; it will return this same attempt_id. Include attempt_id=${ticket.attemptId} in every t_team_update_task call. If it is rejected as stale, stop work because the task was reassigned. claimed cannot jump to completed. Mark in_progress first, then completed or failed. Include attempt_id on every update
|
|
179
|
-
|
|
365
|
+
Call t_team_claim_task for ${ticket.taskId}; it will return this same attempt_id. Include attempt_id=${ticket.attemptId} in every t_team_update_task call. If it is rejected as stale, stop work because the task was reassigned. claimed cannot jump to completed. Mark in_progress first, then completed or failed. Include attempt_id on every update, then become idle.
|
|
366
|
+
|
|
367
|
+
Write the report exactly once. t_team_update_task IS your report: the captain and every downstream task read output/summary/tried from it. After that update succeeds:
|
|
368
|
+
- send_message to the captain with a ONE-LINE notice ("${ticket.taskId} completed — result in the task output"). That line is what wakes the captain; it must not restate the report.
|
|
369
|
+
- Keep your closing turn message to ONE line too. It is delivered to the captain in full, so a write-up there is a third copy of the same text.
|
|
370
|
+
Everything the captain needs is already in the task record. A completion written out three times costs three times the context and says nothing new.
|
|
371
|
+
When finishing: use status=completed only when the task's success criteria are satisfied; use status=failed when blocking findings or validation failures mean downstream work must not proceed; include a concise output in either case. Always also send summary (the one-paragraph conclusion downstream tasks need, plus any assumption you had to make) and, whenever you hit dead ends, tried (each failed approach with why it failed) — tasks that depend on you transitively receive the summary instead of your full output, and tried is what stops them from repeating your dead ends. Quality kinds must submit structured fields: review/requirements need verdict=pass to complete (needs_revision/reject must fail with findings); implementation/repair/verification/integration need acceptanceResults and commandsRun, while implementation/repair also need in-scope changedPaths. Use status values "passed" or "failed" inside those arrays. After the work and verification finish, call t_team_update_task immediately; do not wait for captain confirmation and do not continue exploring. Do not approve your own implementation. Mail is not a formal next review. Treat the dependency results above as source material. Do not ignore them. Work only this task and only its in-scope paths in this turn.
|
|
180
372
|
|
|
181
373
|
State policy: ${stateDir}/${teamId}/ is read-only diagnostics; mutate team state only through t_team_* tools.`;
|
|
182
374
|
}
|
|
@@ -272,7 +464,7 @@ export function installTeamScheduler(ctx, config) {
|
|
|
272
464
|
const recoverOwned = owned !== undefined
|
|
273
465
|
&& (owned.attemptId === undefined || owned.attemptId !== parkedAttemptId);
|
|
274
466
|
const task = recoverOwned ? owned : owned === undefined
|
|
275
|
-
? nextReadyTask(fresh.tasks, currentMember
|
|
467
|
+
? nextReadyTask(fresh.tasks, currentMember)
|
|
276
468
|
: undefined;
|
|
277
469
|
if (task === undefined) {
|
|
278
470
|
if (currentMember.status !== 'idle') {
|
|
@@ -326,6 +518,7 @@ export function installTeamScheduler(ctx, config) {
|
|
|
326
518
|
...task.verify === undefined ? {} : { verify: task.verify },
|
|
327
519
|
...task.reviewedTaskId === undefined ? {} : { reviewedTaskId: task.reviewedTaskId },
|
|
328
520
|
dependencyOutputs: collectCompletedDependencyOutputs(fresh.tasks, task.id, (message) => ctx.logger.warn(message)),
|
|
521
|
+
inFlightSiblings: collectInFlightSiblings(fresh.tasks, task.id),
|
|
329
522
|
};
|
|
330
523
|
});
|
|
331
524
|
if (ticket === undefined)
|
package/lib/teams/tools.js
CHANGED
|
@@ -1464,6 +1464,12 @@ export function registerTTeamTools(ctx, config) {
|
|
|
1464
1464
|
description: 'New status (in_progress, completed, failed, cancelled).',
|
|
1465
1465
|
},
|
|
1466
1466
|
output: { type: 'string', description: 'Result summary; set when completing or failing.' },
|
|
1467
|
+
summary: { type: 'string', description: 'One-paragraph handoff summary (aim for <= 300 characters): the conclusion a downstream task needs, not a narration of your process. Downstream tasks that depend on you transitively receive THIS instead of the full output, so put the load-bearing facts and any assumptions you made here.' },
|
|
1468
|
+
tried: {
|
|
1469
|
+
type: 'array',
|
|
1470
|
+
items: { type: 'string' },
|
|
1471
|
+
description: 'Approaches you tried that did NOT work, each with the reason it failed. This is propagated to every downstream task so nobody repeats your dead ends. Omit when you hit none.',
|
|
1472
|
+
},
|
|
1467
1473
|
attempt_id: { type: 'string', description: 'Current execution capability returned by claim_task (required for members when present on the task).' },
|
|
1468
1474
|
verdict: {
|
|
1469
1475
|
type: 'string',
|
|
@@ -1611,6 +1617,12 @@ export function registerTTeamTools(ctx, config) {
|
|
|
1611
1617
|
task.acceptanceResults = acceptanceResults;
|
|
1612
1618
|
if (commandsRun !== undefined)
|
|
1613
1619
|
task.commandsRun = commandsRun;
|
|
1620
|
+
// 交接契约:取**归一化后**的 input(空串/空列表已被抹掉),与 changedPaths 同一口径 ——
|
|
1621
|
+
// 直接存 args 会把 summary:"" 这种空值写进盘,重载时被 hasValidQualityTaskFields 判非法。
|
|
1622
|
+
if (input.summary !== undefined)
|
|
1623
|
+
task.summary = input.summary;
|
|
1624
|
+
if (input.tried !== undefined)
|
|
1625
|
+
task.tried = input.tried;
|
|
1614
1626
|
task.updatedAt = Date.now();
|
|
1615
1627
|
const followUp = (task.status === 'failed' && (task.verdict === 'needs_revision' || task.verdict === 'reject'))
|
|
1616
1628
|
? applyQualityFollowUp(fresh, task)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-t-expert",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.88",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "T Expert — a 323-expert, 22-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",
|
|
@@ -48,8 +48,6 @@
|
|
|
48
48
|
"data/teams.json",
|
|
49
49
|
"data/teams.resolved.json",
|
|
50
50
|
"data/t-team.config.json",
|
|
51
|
-
"skills/dsh-harness-project",
|
|
52
|
-
"skills/dsh-harness-languages",
|
|
53
51
|
"cordis.patch.yml",
|
|
54
52
|
"README.md",
|
|
55
53
|
"LICENSE",
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: dsh-harness-languages
|
|
3
|
-
description: Use when writing, reviewing, or debugging any code in the deepseek-harness monorepo and you need that language's rules, layout, or toolchain — TypeScript on Node, the React browser client (TSX/CSS Modules), the Python SDK, the C Node-API addon, Cordis YAML composition, SQLite storage, shell, or the build/test toolchain (pnpm, tsc, tsdown, vitest, tsx, oxlint, Electron, Vite).
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# DeepSeek Harness:语言与技术栈
|
|
7
|
-
|
|
8
|
-
一份按语言/技术栈拆的落地参考。先看清「这段代码属于哪一面、哪个语言面」,再动手。
|
|
9
|
-
|
|
10
|
-
## 总览
|
|
11
|
-
|
|
12
|
-
| 语言 / 技术 | 位置 | 运行时与工具链 |
|
|
13
|
-
|---|---|---|
|
|
14
|
-
| **TypeScript(Host/Node)** | `packages/**`、`apps/cli`、`apps/desktop*`、`scripts/` | Node ^22.19 \|\| >=24、ESM、`tsc -b` + `tsdown`、`tsx` 跑源码 CLI |
|
|
15
|
-
| **TypeScript/TSX(Client/浏览器)** | `packages/client/**`、`apps/web` | React 18、`react-jsx`、Vite、CSS Modules + `clsx` |
|
|
16
|
-
| **Python 3.10+** | `python/sdk`、`python/sdk-runtime` | hatchling、uv、pydantic v2、pytest;stdio 上的 JSON-RPC 客户端 |
|
|
17
|
-
| **C(C11)** | `native/system/packages/entry/src/*.c` | Node-API(NAPI_VERSION=8)、`cc`/`musl-gcc`、预编译平台包 |
|
|
18
|
-
| **YAML(Cordis 组合)** | `packages/bundle/**/*.cordis.yml`、preset 的 `agent.cordis.yml`、profile patch | Loader + Schemastery `Config` 校验 |
|
|
19
|
-
| **SQLite** | `packages/storage/storage-sqlite`、`packages/session-query/session-query-sqlite` | `node:sqlite` 的 `DatabaseSync`、FTS5 |
|
|
20
|
-
| **Shell** | `packages/shell/*`、`scripts/*.sh` | bash / pwsh executor + sandbox 包装 |
|
|
21
|
-
| **Markdown(文档)** | `docs/**`、包 README、`.agents/notes/**` | 文档门禁(`doc-sync`) |
|
|
22
|
-
|
|
23
|
-
所有 npm 包名是 `@deepseek-ai/dsh-<name>`;`@deepseek-ai/cordis` 是每个 harness 包的 peerDependency(+ dev)。
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## TypeScript(Host / Node)
|
|
28
|
-
|
|
29
|
-
**编译形态**(`tsconfig.base.json`):`target: es2024`、`module: esnext`、`moduleResolution: bundler`、`strict: true`、`exactOptionalPropertyTypes: true`。
|
|
30
|
-
|
|
31
|
-
- **到处是 ESM**(`"type": "module"`)。跨包用包名 import,**包内相对导入写 `.ts` 后缀**。
|
|
32
|
-
- `dsh` CLI 的源码启动走 `node --import tsx/esm`(tsx 的 ESM-only hook),所以它能触达的模块必须保持 ESM(不能有 CJS-only 导出)——Node 原生 TS 模式在 engines 范围内不可用。
|
|
33
|
-
- **Host / Client 是两个聚合工程**:Host 包注册进 `tsconfig.host.json`,Client 包注册进 `tsconfig.client.json`;`tsconfig.json` 是 solution(`files: []`),`tsconfig.base.json` 是路径映射门面(**永远不要给它加 `include`/`files`**)。原因:两侧都在同一批 key 上 declaration-merge cordis 的 `Context`,一个 program 同时看到两份会报冲突 —— 该冲突只存在于 `ts.Program` 内,模块解析不会触发。
|
|
34
|
-
- 需要构建整仓 `ts.Program` 的脚本要显式以 `tsconfig.host.json` 或 `tsconfig.client.json` 为种子,**绝不能用 root solution**。
|
|
35
|
-
- 六个包是 Host/Client 分裂包,各带两个 leaf config + 只用 solution 的根:`api/remotes`、`api/gateway`、`api/session-controller`、`api/workspace-controller`、`client/connection`、`session-query/session-log-export`。
|
|
36
|
-
|
|
37
|
-
**插件导出形态(最关键的一条)**:
|
|
38
|
-
|
|
39
|
-
- **service 包 `export default` 它的 service class。**
|
|
40
|
-
- **function plugin 命名导出 `name` / `inject` / `Config` / `apply`,且不能有 default export。** 混用会让 Loader 丢弃该 function plugin 的 namespace(见 [postmortem 0001](docs/postmortem/0001-acp-default-export-drops-inject.md))。
|
|
41
|
-
- 可选服务用 **`ctx.get(name)`**;`ctx.<name>` 只留给已声明 injection 的服务 —— 属性代理对拓扑敏感,`ctx.get` 读全局服务存储。
|
|
42
|
-
|
|
43
|
-
**类型与写法**:
|
|
44
|
-
|
|
45
|
-
- 类型化事件用 **declaration merging** 与可合并扩展的 map。`SessionEventMap` 成员默认 required-on-read;事件 JSDoc 需要 `@mode` 和 payload `@param`;payload 里没有的 scoped key 需要 `@dshScopeScan unsupported`。只有结构性格式变化才 bump `SESSION_FORMAT_VERSION`。
|
|
46
|
-
- 判别式 tag 上做 switch;封闭联合以 `assertNever`(`@deepseek-ai/dsh-util-values`)收尾。
|
|
47
|
-
- 跨边界的不透明 id 用 `Branded<B>` / `BrandedNumber<B>` + `brandString` / `brandNumber`(`packages/util/brand`,包名 `@deepseek-ai/dsh-brand`),不用裸 `string`。
|
|
48
|
-
- **每个 module 与 export 都要有简洁 JSDoc**,函数式导出要有 `@param`/`@returns` —— `verify-export-jsdoc` 强制。公有 service 方法要记参数与非 void 返回。
|
|
49
|
-
- 剩下的 `any` 要解释为什么无法收窄。
|
|
50
|
-
- `src/types.ts` 只放类型,不放运行时代码;测试放包级 `tests/`,不放 `src/__tests__/`。
|
|
51
|
-
- 空 `catch` 命名它吞掉什么、为什么别的到不了;`try` 只包一条语句。
|
|
52
|
-
- 注释写局部契约(行为、失败、时序、所有权、模态、例外、后果),**不写推理过程、不复述代码、不写测试走查**。
|
|
53
|
-
|
|
54
|
-
**远程过程调用(Typert)**:业务 service 在 Host 用 `@Remote` / `@RemoteScope` 声明可调用方法;Host 构建生成 Host-for-Client 类型与运行时贡献,Client 的 `api-remotes` 在 `ctx.remote` / `agentCtx.remote` 下加载它们。手工写的 `remote` 类型会漂移。
|
|
55
|
-
|
|
56
|
-
**边界校验**:只在 parser/config、queued、模型/工具 JSON、durable/file、worker、process、wire 边界做运行时校验。同一进程内的静态类型边界信任 TypeScript,不加多余校验。
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## TypeScript / React(浏览器客户端)
|
|
61
|
-
|
|
62
|
-
位置:`packages/client/**`(`ui-*` 插件 + `web` 引导内核 + `store`/`connection`/`slots`/`locale`/`modules`/`resources`),应用层在 `apps/web`。
|
|
63
|
-
|
|
64
|
-
- React **18**,`jsx: react-jsx`,浏览器 lib(`ES2024 + DOM + DOM.Iterable`),`types: []` 起步(需要 Node 类型的包局部覆写)。继承 `tsconfig.base.client.json`。
|
|
65
|
-
- **样式**:CSS Modules(`*.module.css`,约 130 个)+ `clsx`。**不要引入组件库,不要用 Tailwind。**
|
|
66
|
-
- **token 归 `ui-theme` 所有**:静态色阶、语义别名、排版、动效、渐变、阴影、滚动条、明暗偏好都在 `packages/client/ui-theme/src/styles/`,对外暴露 `--dsw-*`。特性包只用 `--dsw-alias-*` 语义别名,**不写死调色板值或字面颜色**,也不在特性组件 CSS 里写主题选择器。
|
|
67
|
-
- 全局样式表放 `ui-theme/src/styles/`;组件样式放组件旁边。组件可定义局部自定义属性,但共享的颜色/排版/高度/动效归 theme 包。
|
|
68
|
-
- **先复用再改样式**:[ui-primitives 组件目录](packages/client/ui-primitives/README.md#component-catalog)是唯一跨特性包的通道;刻意的视觉差异做成它的 prop,而不是再复制一份。
|
|
69
|
-
- 抬高面(菜单、popover、modal、面板、浮动按钮、composer)设 `border: 0` 并用 `var(--dsw-elevation-*)`;**不要把 `--dsw-alias-border-*` 边框和 elevation 阴影配在一起**(ui-theme 的 spec 会拒绝)。
|
|
70
|
-
- 中立实色边框与分隔线画 `0.5px`(Chromium 上正好一个设备像素);虚线语义与状态色边框保持 1px。
|
|
71
|
-
- 圆角继承 ui-theme 的 superellipse smoothing;每个整圆 `border-radius`(`50%`、`100%`、胶囊)都要配 `corner-shape: round`。
|
|
72
|
-
- 可点击的产物链接统一用 `--dsw-alias-link` + `font-weight: 500`,静止无下划线,hover/focus 为 3px 偏移点状下划线。
|
|
73
|
-
- 保留键盘焦点可见性与 reduced-motion 行为。
|
|
74
|
-
- **产品文案归 locale 所有**:走类型化字典 + `t` 座位或本地化 primitive props。JSX、模板、helper 返回值、可访问性属性、primitive 默认值里的硬编码产品文案会被 `verify-client-ui-i18n` 拒绝;用户/模型/wire 数据与代码 token 原样保留。
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
## Python 3.10+
|
|
79
|
-
|
|
80
|
-
位置:`python/sdk`(`deepseek_harness`,PyPI `deepseek-harness-sdk`)与 `python/sdk-runtime`(`deepseek_harness_runtime`,PyPI `deepseek-harness-runtime-bin`)。
|
|
81
|
-
|
|
82
|
-
- `requires-python = ">=3.10"`;`pydantic>=2.12,<3`;构建后端 hatchling(`hatchling==1.30.1`);依赖组测试用 pytest ≥8,`pytest.ini` 在仓库根。
|
|
83
|
-
- 包布局是 `src/` 布局:`python/sdk/src/deepseek_harness/{__init__,api,client,errors,models}.py`。
|
|
84
|
-
- **角色**:通过 stdio 上的换行分隔 JSON-RPC 驱动 dsh 子进程。`HarnessClient` 是同步客户端;`HarnessConfig` 持有 `dsh_bin`、`profile`(默认 `sdk`)、`patches`、`dsh_home`、`cwd`、`env`、各类超时。
|
|
85
|
-
- **每次启动都必须显式指定 Harness home;Python 绝不静默读 `~/.dsh`。**
|
|
86
|
-
- Python 侧暴露的是 profile 选择 + 有序 patch 文件,不是完整 Cordis 树;持久外部插件通过 `dsh plugin` 安装。
|
|
87
|
-
- 上游 `errors.py` 里的 `JsonRpcError` / `TransportClosedError` 是传输层的失败类型;模型结构在 `models.py`(`IncomingRequest`、`InitializeResponse`、`JsonObject`、`JsonValue`、`Notification`)。
|
|
88
|
-
- `sdk-runtime` 把正常 `dsh` CLI 打包成 `deepseek-harness-sdk-runtime-<platform>-<arch>`;`hatch_build.py` 注入运行时可执行文件,editable 安装走 `[tool.uv.sources]`。跨平台 CI 由 master-only 的 Python-runtime 工作流负责。
|
|
89
|
-
- agent-loop / session 生命周期 / `SessionEventMap` 改动必须同步更新 **Python SDK 的单可执行快照**(`scripts/snapshots/python-sdk-single-exe/`)。
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
## C(Node-API 原生插件)
|
|
94
|
-
|
|
95
|
-
位置:`native/system`(workspace 包 `@deepseek-ai/node-addon-system`,平台包 `darwin-arm64`、`darwin-x64`、`linux-arm64`、`linux-x64` + `entry`)。
|
|
96
|
-
|
|
97
|
-
- 两个能力:**Linux Landlock launcher**(`landlock-run`:`launcherPath`、`probe`、`grantArgs`)与 **POSIX flock**(`tryLockExclusive(fd): Promise<void>`)。
|
|
98
|
-
- 编译:`-std=c11 -Wall -Wextra -Werror`;Node-API 走 `-DNAPI_VERSION=8 -fPIC -fvisibility=hidden`;musl 用 `musl-gcc -static`。
|
|
99
|
-
- 源码是 `packages/entry/src/main.c` 与 `flock.c`,通过 `lib/` 的 TS 门面导出(`tc -b` 之后 `prepack` 跑 `verify-entry-lib.mjs`)。
|
|
100
|
-
- **消费者安装从不编译原生代码**:平台二进制放在平台包里,由 entry 包以 npm optionalDependencies 承载。
|
|
101
|
-
- 语义契约在 `native/system/docs/`(`architecture.md`、`cli-contract.md`、`flock-contract.md`、`naming.md`、`packaging.md`、`release.md`、`support-matrix.md`)—— 改行为同步改契约文档。
|
|
102
|
-
- 重要语义:import 任一 entry **不会**加载 addon;Landlock 可执行文件缺失时 `probe` 报不可用,flock binding 缺失时获取锁直接 reject。两者都不会静默降级或自行编译。
|
|
103
|
-
- flock 语义:非阻塞独占 flock;竞争以 `EAGAIN`/`EWOULDBLOCK` reject;打开文件的最后一个描述符关闭时释放锁;描述符要保持到完成。
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## YAML:Cordis 组合与配置
|
|
108
|
-
|
|
109
|
-
- `cordis.yml` 里 **`!!js` 只允许出现在 plugin 的 `config` 和 entry 的 `disabled` 下**(注意是双感叹号,不是 `!js`);其他元数据保持字面量。条件组合用 patch overlay 表达。
|
|
110
|
-
- 每个 row 的 `config` 由该插件导出的 Schemastery `Config` 校验;**配置字段都有 JSDoc**,生成的 `docs/config-catalog.md` 是穷尽权威。
|
|
111
|
-
- patch **按 `id` 定位 row**:替换整条 config,或插入新行。
|
|
112
|
-
- 裸插件(bare plugin)必须出现在其 resolver manifest 的 `dependencies` 里,`verify-cordis-config` 强制。
|
|
113
|
-
- agent preset 的 `agent.cordis.yml` 是 **agent 平面**:只放该 session 往注册表里贡献的东西;发布服务必须在带 `isolate` 的 group 内。`baseUrl` 在该文件中可用(preset 自身目录),`{{model}}`/`{{cwd}}` 是 persona 模板变量。
|
|
114
|
-
- preset 显示元数据在旁边的 `preset.yml`,只允许 `name` / `description` / `order`(`id` 是目录名,`trust` 来自发现根,二者不可在此声明)。
|
|
115
|
-
- 读失败一律降级为「无元数据」:**显示文本坏了不该让 preset 起不来**。
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
## SQLite
|
|
120
|
-
|
|
121
|
-
- 用 **`node:sqlite` 的 `DatabaseSync`**(内建,无第三方驱动),不是 better-sqlite3。
|
|
122
|
-
- 两个后端:`packages/storage/storage-sqlite`(kv facet,`STORAGE_SQLITE_SCHEMA_VERSION = 1`)与 `packages/session-query/session-query-sqlite`(`SESSION_QUERY_SQLITE_SCHEMA_VERSION = 8`,FTS5 全文检索)。
|
|
123
|
-
- 版本存在 **`PRAGMA user_version`**:单调递增;`0` 视为未盖章并写入当前版本;**不匹配就报错拒绝打开**,不静默迁移、不降级。
|
|
124
|
-
- 会话数据本身是 append-only JSONL 日志(`session.jsonl[.zstd]`、v1+ 为 `session.vN.jsonl[.zstd]`)。已提交的 generation 路径**永不重命名、覆盖或删除**;SQLite 只服务查询与 kv 面。
|
|
125
|
-
|
|
126
|
-
---
|
|
127
|
-
|
|
128
|
-
## Shell
|
|
129
|
-
|
|
130
|
-
- 两个 executor provider:`dsh-bash-local` / `dsh-bash-sandbox`(Windows 上是 pwsh 对应实现),模型可见工具是 `dsh-tool-bash` / `dsh-tool-pwsh`。
|
|
131
|
-
- `ctx.shell` 的 request/spec 分离是「包边界显式优于隐式」的模板:默认值解析是拥有方显式的一步 `resolve(request): Spec`,不是 `run()` 里藏的 `?? default`。
|
|
132
|
-
- 子进程要经 sandbox backend 包装 argv。**不要把宿主的运行环境交给不受信输出**:spawn 的命令使用擦洗过的 env(丢弃 `*KEY*`/`*SECRET*`/`*TOKEN*`/`*PASSWORD*`),临时/溢出文件用私有 0700 目录、随机文件名、独占 owner-only 打开(`'wx'`、`0o600`)。
|
|
133
|
-
- 可能是符号链接或 Windows junction 的路径,先 `lstatSync().isSymbolicLink()` 再 `unlinkSync`;递归 `rmSync` 只留给确认的真实目录。
|
|
134
|
-
- 仓库脚本:`scripts/*.ts` 是主入口;只有 7 个 `.sh`(CI/打包辅助)。脚本里的诊断与门禁多数挂在 `scripts/run-gates.ts`。
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
## Markdown 与文档
|
|
139
|
-
|
|
140
|
-
见 [dsh-harness-project](SKILL.md) 的技能文档分层一节:一段一个物理行、只写当前状态、生成的参考文档不手改、跨引用用相对路径。
|
|
141
|
-
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
## 构建与验证工具链
|
|
145
|
-
|
|
146
|
-
| 工具 | 角色 |
|
|
147
|
-
|---|---|
|
|
148
|
-
| **pnpm 11.7.0**(corepack) | workspace + lockfile;workspace 目录:`vendor/*`、`packages/*/*`、`native/system`、`native/system/packages/*`、`apps/*`、`website` |
|
|
149
|
-
| **tsc 6**(project references) | `tsc -b tsconfig.host.json` → `lib/types`;Client 同理 |
|
|
150
|
-
| **tsdown** | 打包 runtime;`--env.DSH_BUILD_FACE host\|client` 选择阶段;只消费前一步 tsc 的产物,**不扫描已有构建产物来发现包** |
|
|
151
|
-
| **tsx** | 跑 TypeScript 脚本与源码 CLI(`node --import tsx/esm`) |
|
|
152
|
-
| **vitest 4** | 全部测试层;所有 vitest config 都通过 vite-tsconfig-paths 指向 `tsconfig.base.json`,**workspace import 永远解析到 `src`**,不走包 `exports` 到已构建的 `lib/`(那里会加载第二份 module singleton) |
|
|
153
|
-
| **oxlint** | lint(`.oxlintrc.json`);`lint:fix` 用 staged 配置 |
|
|
154
|
-
| **jscpd** | `duplication` 跨文件 TS 克隆检测 |
|
|
155
|
-
| **publint + NodeNext 消费者检查** | `hygiene` 门禁组 |
|
|
156
|
-
| **Vite** | `apps/web` 构建前端产物;`pnpm run dev:web` 需要先有一次完整构建 |
|
|
157
|
-
| **Electron 44** | `apps/desktop` 桌面应用;带私有的 Desktop Host 在打包的 Node 进程里加载后端与客户端图,走版本化分帧字节管道,**不开 Web server 或 loopback 端口** |
|
|
158
|
-
| **Playwright/Chromium(测试内)** | `test:web` 浏览器快照 |
|
|
159
|
-
| **hatchling / uv / pytest** | Python 侧 |
|
|
160
|
-
| **cc / musl-gcc** | 原生侧 |
|
|
161
|
-
|
|
162
|
-
**源码面 vs 产物面**:静态门禁与测试通过 tsconfig `paths` 解析到 `src`,必须在干净树上通过;消费已构建 `lib/` 的门禁(built smoke、`lib` 模式子进程)要显式声明该依赖。子进程启动模式由共享 dual-mode launcher 决定,**不要手写 `--import tsx`**。
|
|
163
|
-
|
|
164
|
-
**构建产物污染提醒**:`pnpm run typecheck` 会先跑完整的 Host lib 阶段再跑 Client tsc;`build` 会继续 Client tsdown 与 Web build。构建把版本、7 位源码 commit、脏标记嵌进去——改完代码别指望旧的 `lib/` 还有效。
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
## 各语言共同的硬约束
|
|
169
|
-
|
|
170
|
-
1. **一个异步操作对应一个生命周期控制器或事务**;把 readiness/cancellation/disposal/reservation/sentinel 拆成多份状态就必须各有独立 owner 或结算点。
|
|
171
|
-
2. **在提交点发布状态**:通知与派生状态都在操作成功之后;缓存、prompt、UI 回声、replay、查询视图都从同一个权威源派生。
|
|
172
|
-
3. **边界施加在完整结果上**:字节/token/条目/时间上限要在完整产出(含包装与元数据)已知处施加,并测试极小值、精确值、超大单块、多字节边界。
|
|
173
|
-
4. **dispose 要到达静默**:teardown 必须 await 子项退出(kill → await `done`),并在 kill 之前关闭监听/通知注册表。
|
|
174
|
-
5. **回调异常要在 dispatcher 内被兜住**:一个坏监听器不能 reject 它所在的 promise,也不能饿死后面的监听器。
|
|
175
|
-
6. **正交结果独立上报**:超时 + exit 0 可以同时为真;`timedOut`/`signal`/`exitCode` 各报各的,不要把一个塞进另一个分支。
|