dsh-plugin-teamflow 0.1.6 → 0.1.8
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/CHANGELOG.md +51 -0
- package/README.en.md +6 -5
- package/README.md +35 -21
- package/lib/client.js +1740 -143
- package/lib/descriptors.mjs +54 -0
- package/lib/host.mjs +891 -314
- package/package.json +27 -6
package/lib/host.mjs
CHANGED
|
@@ -4,19 +4,56 @@ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
|
4
4
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
5
|
import { parameterSchemaSpecToJsonSchema } from "@deepseek-ai/dsh-tools";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { fileAddressFor } from "@deepseek-ai/dsh-util-workspace-path";
|
|
8
9
|
import { execFileSync } from "node:child_process";
|
|
9
10
|
//#region host/constants.ts
|
|
10
|
-
/**
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* 单次 withRetry 调用的**新增** token 熔断预算(2026-09-11 口径修正)。
|
|
13
|
+
*
|
|
14
|
+
* 口径 = `freshTokensOf`(input + cacheWrite + output),**排除 cacheRead**:
|
|
15
|
+
* 缓存命中是上下文复用的廉价重放,把它计入「烧钱」会得出荒谬结论——实锤 assetd
|
|
16
|
+
* tf-mtwvwpxa-p3vw08 的 T5:熔断日志报「累计 token 1886k 超出阶段预算 60k」,
|
|
17
|
+
* 而其中 1830k 是 cacheRead,真实新增只有 55k。旧口径的后果不是数字难看,而是
|
|
18
|
+
* **任何 dev 任务只要失败一次就必然熔断**(正常 dev 单次新增实测 45–80k,而缓存命中
|
|
19
|
+
* 恒在 1M 量级)→ RETRY_LIMIT=2 形同虚设,一次失败直接转人工停线。
|
|
20
|
+
*
|
|
21
|
+
* 量级依据(实测单次尝试新增 token):T5 55.4k / T8 49.3k / T9 79.6k / T11 45.9k
|
|
22
|
+
* → 200k ≈ 允许 RETRY_LIMIT 的两轮尝试各留余量,只在该量级的 3 倍以上(真跑飞)才熔断。
|
|
23
|
+
*/
|
|
24
|
+
const FRESH_TOKEN_BUDGET = 2e5;
|
|
25
|
+
/** 任务夹产物展示顺序(ADR-0008):工作台只列其中**真实存在**的文件,按此顺序出「一键右侧栏预览」按钮。 */
|
|
26
|
+
const TEAMFLOW_ARTIFACT_ORDER = [
|
|
27
|
+
"PRD.md",
|
|
28
|
+
"DESIGN.md",
|
|
29
|
+
"TECHNICAL.md",
|
|
30
|
+
"QA-REPORT.md",
|
|
31
|
+
"ACCEPTANCE.md",
|
|
32
|
+
"meta.json"
|
|
33
|
+
];
|
|
12
34
|
/** 护栏轮询间隔 ms。 */
|
|
13
35
|
const GUARD_POLL_MS = 15e3;
|
|
14
36
|
/** 挂死判定:连续这么久没有任何新会话事件(provider 挂起/静默死亡)→ stalled(走预算门转人工)。 */
|
|
15
37
|
const GUARD_SILENCE_MS = 6e5;
|
|
16
38
|
/** 空转判定:会话仍在产出事件但连续这么久没有任何工具调用(纯推理打转/改写式循环)→ stalled。要求已见过至少一次工具调用。 */
|
|
17
39
|
const GUARD_NO_TOOL_MS = 9e5;
|
|
18
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* 拒绝/放弃措辞词表(诊断信号 + 兜底判据,**不再是唯一的交付门禁**)。
|
|
42
|
+
*
|
|
43
|
+
* 2026-09-11 信号换轨:旧实现把它当交付门禁全文扫描,实锤 assetd tf-mtwvwpxa-p3vw08 的 T5
|
|
44
|
+
* ——子代理 `stopReason=completed`、41 次工具调用、证据块与 state 块齐全、代码已落盘,
|
|
45
|
+
* 只因**如实汇报环境限制**(「7 条 runCli 用例与 spec/verify.mjs 全部 26 例无法执行」)
|
|
46
|
+
* 命中「无法执行」→ 判 insubstantial「视为未交付」→ 提测门禁停线 + 人工 resume。
|
|
47
|
+
* 模型汇报环境限制是本分,不是拒绝——措辞不能当交付判据。
|
|
48
|
+
* 现用法见 `judgeDeliverable`:仅在**无验证证据块**时才作为否决依据;命中即回传供留痕。
|
|
49
|
+
*/
|
|
19
50
|
const REFUSAL_PATTERN = /(无法完成|不能完成|无法继续|抱歉|对不起|我(无法|不能)|无法执行|cannot complete|unable to)/i;
|
|
51
|
+
/**
|
|
52
|
+
* 真交付信号(结构件,非措辞):prompt 强制的 `[Verification evidence]` 块——「命令 + 退出码 +
|
|
53
|
+
* 断言计数」的具体自述。拒绝/放弃类产出给不出具体命令细节,故它出现即判交付,与措辞无关。
|
|
54
|
+
* 这是「防假完成(光说不做)」的客观判据,取代此前对散文措辞的依赖。
|
|
55
|
+
*/
|
|
56
|
+
const DELIVERY_EVIDENCE_PATTERN = /\[Verification evidence\]/i;
|
|
20
57
|
/** 各阶段最小产出长度(防"假完成":空话/一句话冒充交付)。 */
|
|
21
58
|
const STAGE_MIN_LENGTH = {
|
|
22
59
|
prd: 400,
|
|
@@ -292,6 +329,41 @@ const SAFE_SIGNAL = {
|
|
|
292
329
|
function normalizeSignal(s) {
|
|
293
330
|
return s && typeof s === "object" && typeof s.addEventListener === "function" && typeof s.aborted === "boolean" && typeof s.throwIfAborted === "function" ? s : SAFE_SIGNAL;
|
|
294
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* 幂等合并 .gitignore 条目(纯函数,便于回归测试)。
|
|
334
|
+
*
|
|
335
|
+
* 场景(实锤 assetd `tf-mtwvwpxa-p3vw08`):插件强制把命令日志/临时脚本写进 `logs/teamflow/`,
|
|
336
|
+
* 目标仓库没忽略它时,收口提交的 227 个文件里 208 个是这批噪音(92%)——本函数负责「补规则」这一半,
|
|
337
|
+
* 另一半(提交面强制排除)在 `sanity.tfAddArgs()`。
|
|
338
|
+
*
|
|
339
|
+
* 覆盖判定不只看字面相等:已有 `logs/`、`logs/**` 这类**更宽的目录规则**同样算已忽略
|
|
340
|
+
* (否则会往一个已经生效的仓库里塞冗余规则)。返回 `changed=false` 时调用方**不要写文件**。
|
|
341
|
+
*/
|
|
342
|
+
function mergeGitignore(existing, entries) {
|
|
343
|
+
const src = existing === null || existing === void 0 ? "" : String(existing);
|
|
344
|
+
const lines = src.split(/\r?\n/).map((l) => l.trim());
|
|
345
|
+
/** 归一:去首尾斜杠与尾部 glob(`logs/`、`logs/**` → `logs`)。 */
|
|
346
|
+
const norm = (s) => s.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\/\*\*?$/, "");
|
|
347
|
+
const coveredBy = (entry) => {
|
|
348
|
+
const e = norm(entry);
|
|
349
|
+
return lines.some((l) => {
|
|
350
|
+
if (!l || l.startsWith("#")) return false;
|
|
351
|
+
const n = norm(l);
|
|
352
|
+
return !!n && (n === e || e.startsWith(`${n}/`));
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
const added = entries.filter((e) => e && !coveredBy(e));
|
|
356
|
+
if (added.length === 0) return {
|
|
357
|
+
text: src,
|
|
358
|
+
changed: false,
|
|
359
|
+
added: []
|
|
360
|
+
};
|
|
361
|
+
return {
|
|
362
|
+
text: (src ? `${src}${src.endsWith("\n") ? "" : "\n"}` : "") + `${src ? "\n" : ""}# TeamFlow 运行日志(插件自有产物,非交付物;host 提交时另有 pathspec 强制排除)\n${added.join("\n")}\n`,
|
|
363
|
+
changed: true,
|
|
364
|
+
added
|
|
365
|
+
};
|
|
366
|
+
}
|
|
295
367
|
/** 分支 slug 派生(ADR-2026-08-27):branchName > triageSlug > 需求中的英文标识词 > reqId 数字 > 'feature'。
|
|
296
368
|
* 实锤 feat/feature:lite 显式时 triage 不跑(无 slug)+ 分支检查早于 reqId 生成 → fallback 'feature'。 */
|
|
297
369
|
function deriveBranchSlug(requirement, reqId, triageSlug, branchName) {
|
|
@@ -320,12 +392,66 @@ function deriveBranchSlug(requirement, reqId, triageSlug, branchName) {
|
|
|
320
392
|
if (num) return `r${num[0]}`;
|
|
321
393
|
return "feature";
|
|
322
394
|
}
|
|
323
|
-
/**
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
395
|
+
/**
|
|
396
|
+
* 交付判定(信号分级;2026-09-11 信号换轨,原 `hasSubstance`)。
|
|
397
|
+
*
|
|
398
|
+
* 旧判据 = 非空 + **全文拒绝词** + 长度下限——把「措辞」当交付门禁。实锤 assetd
|
|
399
|
+
* tf-mtwvwpxa-p3vw08 的 T5:子代理 `stopReason=completed`、41 次工具调用、证据块与
|
|
400
|
+
* state 块齐全、`src/query.mjs` 已落盘,只因如实汇报「7 条 runCli 用例与 spec/verify.mjs
|
|
401
|
+
* 全部 26 例无法执行(沙箱禁止子进程管道)」命中「无法执行」→ 判 insubstantial
|
|
402
|
+
* 「视为未交付」→ 提测门禁停整条线 + 人工 resume(16 分钟 + 一轮重跑)。
|
|
403
|
+
* 教训不是「词表少了一个词」,而是**措辞不能用来判定是否交付**:模型如实汇报环境限制
|
|
404
|
+
* 是本分,换一种说法(跑不通/环境不允许/需在无限制 shell 复跑)旧判据照样误杀。
|
|
405
|
+
*
|
|
406
|
+
* 现判据按「客观优先、措辞退为兜底」分级:
|
|
407
|
+
* 1. 客观形态:非空 + 达阶段长度下限(不读语义);
|
|
408
|
+
* 2. 真交付信号:含 `[Verification evidence]` 块 → 判交付(拒绝/放弃类产出给不出具体
|
|
409
|
+
* 命令+退出码细节);命中拒绝词只记诊断、不否决;
|
|
410
|
+
* 3. 兜底:无证据块且命中拒绝词 → 判未交付(这才是「光说不做 / 自称做不到」的形态)。
|
|
411
|
+
*
|
|
412
|
+
* 因此「如实汇报环境限制」这类假阳性在**结构上**消失,而「没干活就说完成」仍被抓:
|
|
413
|
+
* 无证据块的假交付照旧落到第 3 级或长度级。
|
|
414
|
+
*/
|
|
415
|
+
function judgeDeliverable(phase, text) {
|
|
416
|
+
const s = toText(text);
|
|
327
417
|
const min = STAGE_MIN_LENGTH[phase] ?? 100;
|
|
328
|
-
|
|
418
|
+
const length = s.trim().length;
|
|
419
|
+
if (length === 0) return {
|
|
420
|
+
ok: false,
|
|
421
|
+
reason: "empty",
|
|
422
|
+
refusal: null,
|
|
423
|
+
min,
|
|
424
|
+
length
|
|
425
|
+
};
|
|
426
|
+
if (length < min) return {
|
|
427
|
+
ok: false,
|
|
428
|
+
reason: "too-short",
|
|
429
|
+
refusal: null,
|
|
430
|
+
min,
|
|
431
|
+
length
|
|
432
|
+
};
|
|
433
|
+
const refusal = refusalHit(s);
|
|
434
|
+
if (!refusal) return {
|
|
435
|
+
ok: true,
|
|
436
|
+
reason: "ok",
|
|
437
|
+
refusal: null,
|
|
438
|
+
min,
|
|
439
|
+
length
|
|
440
|
+
};
|
|
441
|
+
if (DELIVERY_EVIDENCE_PATTERN.test(s)) return {
|
|
442
|
+
ok: true,
|
|
443
|
+
reason: "ok",
|
|
444
|
+
refusal,
|
|
445
|
+
min,
|
|
446
|
+
length
|
|
447
|
+
};
|
|
448
|
+
return {
|
|
449
|
+
ok: false,
|
|
450
|
+
reason: "refusal",
|
|
451
|
+
refusal,
|
|
452
|
+
min,
|
|
453
|
+
length
|
|
454
|
+
};
|
|
329
455
|
}
|
|
330
456
|
/** 不可重试的失败原因(上下文耗尽/超长/provider 客户端拒绝等——重试同一 prompt 大概率复现)。
|
|
331
457
|
* 实锤 tf-mtcnejqj:opencode-go 400 invalid_request_error(tool 消息序列非法)被当作可重试 → 烧 1.98M 熔断。 */
|
|
@@ -471,20 +597,27 @@ function extractBlueprint(text) {
|
|
|
471
597
|
//#region host/core/context.ts
|
|
472
598
|
/**
|
|
473
599
|
* dsh-plugin-teamflow core — 运行期共享状态(进程单例)。
|
|
474
|
-
* - runtime(agents/subagents/
|
|
600
|
+
* - runtime(agents/subagents/workspaceRegistry/agentDefaultModel/llm):由 index=TeamflowService 的 static inject 注入(setRuntime)。
|
|
475
601
|
* - runs/inFlight/activeProducts:流水线运行期 Map(跨 runner/pipeline/report/服务共享)。
|
|
476
602
|
* 这是 ADR-0004「共享状态」在编排层的落点:共享对象集中、单向被 core 各模块 import(不反向)。
|
|
477
603
|
*/
|
|
478
604
|
/** 子代理/计量等宿主能力(由 TeamflowService 装配时 setRuntime 注入)。字段为鸭子类型:消费方自行窄化。 */
|
|
479
605
|
const runtime = {};
|
|
480
|
-
function setRuntime(agents, subagents,
|
|
606
|
+
function setRuntime(agents, subagents, workspaceRegistry, agentDefaultModel, llm) {
|
|
481
607
|
runtime.agents = agents;
|
|
482
608
|
runtime.subagents = subagents;
|
|
483
|
-
runtime.tokenMeter = tokenMeter;
|
|
484
609
|
runtime.workspaceRegistry = workspaceRegistry;
|
|
485
610
|
runtime.agentDefaultModel = agentDefaultModel;
|
|
486
611
|
runtime.llm = llm;
|
|
487
612
|
}
|
|
613
|
+
/**
|
|
614
|
+
* 可选能力:官方 Session 投影注册表(ctx.sessionProjections,dsh-session-projection)。
|
|
615
|
+
* 单独 setter 而非并入 setRuntime——它是**可选**依赖:用 ctx.inject 在服务可用时注册,
|
|
616
|
+
* 未挂载(最小 profile)时计量自动回退事件扫描,插件照常加载。
|
|
617
|
+
*/
|
|
618
|
+
function setSessionProjections(projections) {
|
|
619
|
+
runtime.sessionProjections = projections;
|
|
620
|
+
}
|
|
488
621
|
/** 运行期 run 注册表(runId → Journal)。 */
|
|
489
622
|
const runs = /* @__PURE__ */ new Map();
|
|
490
623
|
/** 进行中的 stage 注册表(runId → { run, stage }),供取消/完成清理。 */
|
|
@@ -530,15 +663,19 @@ async function currentModelSupportsVision(provider, model) {
|
|
|
530
663
|
/**
|
|
531
664
|
* 从发起会话推导工作区作用域。
|
|
532
665
|
*
|
|
533
|
-
*
|
|
534
|
-
* 1. workspaceRegistry.resolveByPath(cwd) → 用 workspace.id(UUID
|
|
535
|
-
*
|
|
666
|
+
* 优先级(**实际生效的只有第 2 条**):
|
|
667
|
+
* 1. workspaceRegistry.resolveByPath(cwd) → 用 workspace.id(UUID)作 projectKey
|
|
668
|
+
* —— ⚠️ **当前不可达**:宿主 `resolveByPath` 是 `async`(返回 Promise,见
|
|
669
|
+
* `packages/workspace/workspace/src/index.ts`),本函数同步调用 → `ws.id` 恒为 undefined,
|
|
670
|
+
* 永远落到第 2 条。分支保留是为将来迁移(需 await + 存储 key 迁移,见 docs/TODO.md)。
|
|
671
|
+
* 2. session cwd 的 basename + 短 hash(`slugPath`)—— **当前实际使用的 key**
|
|
536
672
|
* 3. 兜底 'default'
|
|
537
673
|
*
|
|
538
674
|
* projectKey 用于 $DSH_HOME/teamflow/<projectKey>/ 目录,要求:
|
|
539
|
-
* -
|
|
540
|
-
* - 不同
|
|
675
|
+
* - 同一路径永远解析到同一个 key(sha1 派生,满足)
|
|
676
|
+
* - 不同 cwd 即使 basename 相同也不碰撞(hash 参与,满足)
|
|
541
677
|
* - 目录名安全(只含 [a-zA-Z0-9_-])
|
|
678
|
+
* ⚠️ 代价:key 绑定**路径字符串**,同一工作区换个写法(盘符大小写/软链/尾斜杠)会得到不同 key。
|
|
542
679
|
*/
|
|
543
680
|
function workspaceScopeOf(agent) {
|
|
544
681
|
const session = agent?.session;
|
|
@@ -1081,6 +1218,288 @@ function hasOpenBlockingBugs(journal) {
|
|
|
1081
1218
|
return false;
|
|
1082
1219
|
}
|
|
1083
1220
|
}
|
|
1221
|
+
//#endregion
|
|
1222
|
+
//#region host/core/state.ts
|
|
1223
|
+
/**
|
|
1224
|
+
* dsh-plugin-teamflow core — state.json 预编译上下文索引。
|
|
1225
|
+
*
|
|
1226
|
+
* 目标:解决「每个新 run 都要从小代理全量读历史文档(PRD/TECH/QA)来重建认知」的 token 爆炸。
|
|
1227
|
+
* state.json 是跨 run 累积的结构化索引:每次 run 结束后由各阶段把「精简结论」沉淀进来,
|
|
1228
|
+
* 下一个 run 的子代理只读注入的 state slice,不再重复读全套历史文档。
|
|
1229
|
+
*
|
|
1230
|
+
* 设计原则:
|
|
1231
|
+
* - memory.md 保持权威记忆(人读);state.json 是预编译索引(机器喂给子代理)。
|
|
1232
|
+
* - 子代理不直接读 state.json 文件,由 host 在开工时按角色注入相关 slice 到 prompt。
|
|
1233
|
+
* - state.json 只存「结论/指针」,不存全文;具体内容仍指向 docs/teamflow/ 下的活文档。
|
|
1234
|
+
*/
|
|
1235
|
+
/** 空态 state。 */
|
|
1236
|
+
function emptyState() {
|
|
1237
|
+
return {
|
|
1238
|
+
version: 1,
|
|
1239
|
+
projectName: null,
|
|
1240
|
+
updatedAt: null,
|
|
1241
|
+
product: {
|
|
1242
|
+
summary: null,
|
|
1243
|
+
techStack: null
|
|
1244
|
+
},
|
|
1245
|
+
lastRunFolder: null,
|
|
1246
|
+
modules: {},
|
|
1247
|
+
verifyScripts: [],
|
|
1248
|
+
acIndex: {},
|
|
1249
|
+
stages: {},
|
|
1250
|
+
lastRun: null
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
/** state.json 路径:$DSH_HOME/teamflow/<projectKey>/state.json */
|
|
1254
|
+
function stateFile(projectKey) {
|
|
1255
|
+
return join(teamflowRoot(), projectKey, "state.json");
|
|
1256
|
+
}
|
|
1257
|
+
/** 读取(不存在返回空态)。 */
|
|
1258
|
+
function loadState(projectKey) {
|
|
1259
|
+
const file = stateFile(projectKey);
|
|
1260
|
+
try {
|
|
1261
|
+
if (existsSync(file)) {
|
|
1262
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
1263
|
+
const base = emptyState();
|
|
1264
|
+
if (raw && typeof raw === "object") {
|
|
1265
|
+
base.projectName = raw.projectName ?? null;
|
|
1266
|
+
base.updatedAt = raw.updatedAt ?? null;
|
|
1267
|
+
base.product = {
|
|
1268
|
+
...base.product,
|
|
1269
|
+
...raw.product || {}
|
|
1270
|
+
};
|
|
1271
|
+
base.lastRunFolder = raw.lastRunFolder ?? null;
|
|
1272
|
+
base.modules = raw.modules || {};
|
|
1273
|
+
base.verifyScripts = Array.isArray(raw.verifyScripts) ? raw.verifyScripts : [];
|
|
1274
|
+
base.acIndex = raw.acIndex || {};
|
|
1275
|
+
base.stages = raw.stages || {};
|
|
1276
|
+
base.lastRun = raw.lastRun ?? null;
|
|
1277
|
+
return base;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
} catch (e) {}
|
|
1281
|
+
return emptyState();
|
|
1282
|
+
}
|
|
1283
|
+
/** 保存。 */
|
|
1284
|
+
function saveState(projectKey, state) {
|
|
1285
|
+
const file = stateFile(projectKey);
|
|
1286
|
+
try {
|
|
1287
|
+
mkdirSync(join(teamflowRoot(), projectKey), { recursive: true });
|
|
1288
|
+
state.updatedAt = Date.now();
|
|
1289
|
+
writeFileSync(file, JSON.stringify(state, null, 2), "utf8");
|
|
1290
|
+
return true;
|
|
1291
|
+
} catch (e) {
|
|
1292
|
+
console.error("[teamflow] saveState failed", e?.message);
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
/** 从阶段产出文本中提取 `<!-- state -->{...}<!-- /state -->` 块(找不到返回 null)。 */
|
|
1297
|
+
function extractStateBlock(text) {
|
|
1298
|
+
const m = (text === null || text === void 0 ? "" : String(text)).match(/<!--\s*state\s*-->([\s\S]*?)(?:<!--\s*\/state\s*-->|$)/);
|
|
1299
|
+
if (!m || !m[1]) return null;
|
|
1300
|
+
try {
|
|
1301
|
+
const raw = JSON.parse(m[1].trim());
|
|
1302
|
+
if (raw && typeof raw === "object") return raw;
|
|
1303
|
+
} catch (e) {}
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
/** 把阶段产出的 state 块合并进 state.json。 */
|
|
1307
|
+
function mergeStateBlock(projectKey, block, phase) {
|
|
1308
|
+
const state = loadState(projectKey);
|
|
1309
|
+
const key = block && block.phase || phase || "other";
|
|
1310
|
+
if (block) {
|
|
1311
|
+
if (typeof block.summary === "string" && block.summary.trim()) state.stages[key] = block.summary.trim();
|
|
1312
|
+
if (Array.isArray(block.touched)) {
|
|
1313
|
+
for (const f of block.touched) if (typeof f === "string" && f) state.modules[f] = state.modules[f] || "touched";
|
|
1314
|
+
}
|
|
1315
|
+
if (typeof block.verdict === "string" && block.verdict) {
|
|
1316
|
+
state.lastRun = state.lastRun || {};
|
|
1317
|
+
state.lastRun.verdict = block.verdict;
|
|
1318
|
+
}
|
|
1319
|
+
if (block.extra && typeof block.extra === "object") {
|
|
1320
|
+
if (Array.isArray(block.extra.verifyScripts)) {
|
|
1321
|
+
for (const s of block.extra.verifyScripts) if (typeof s === "string" && s && state.verifyScripts.indexOf(s) === -1) state.verifyScripts.push(s);
|
|
1322
|
+
}
|
|
1323
|
+
if (block.extra.acIndex && typeof block.extra.acIndex === "object") state.acIndex = {
|
|
1324
|
+
...state.acIndex,
|
|
1325
|
+
...block.extra.acIndex
|
|
1326
|
+
};
|
|
1327
|
+
if (typeof block.extra.techStack === "string" && block.extra.techStack) state.product.techStack = block.extra.techStack;
|
|
1328
|
+
if (typeof block.extra.moduleContracts === "object" && block.extra.moduleContracts) state.modules = {
|
|
1329
|
+
...state.modules,
|
|
1330
|
+
...block.extra.moduleContracts
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
saveState(projectKey, state);
|
|
1335
|
+
return state;
|
|
1336
|
+
}
|
|
1337
|
+
/** 按 run 更新 lastRun / lastRunFolder(finally 时调用)。 */
|
|
1338
|
+
function noteRun(projectKey, run) {
|
|
1339
|
+
const state = loadState(projectKey);
|
|
1340
|
+
if (run.runDocs) state.lastRunFolder = run.runDocs;
|
|
1341
|
+
state.lastRun = {
|
|
1342
|
+
runId: run.id || null,
|
|
1343
|
+
requirement: run.requirement ? String(run.requirement).slice(0, 200) : null,
|
|
1344
|
+
verdict: run.verdict || null,
|
|
1345
|
+
folder: run.runDocs || null,
|
|
1346
|
+
endedAt: run.endedAt ?? Date.now()
|
|
1347
|
+
};
|
|
1348
|
+
saveState(projectKey, state);
|
|
1349
|
+
}
|
|
1350
|
+
/** 按角色渲染 state slice(注入到子代理 prompt)。角色 → 只拿相关片段。 */
|
|
1351
|
+
function stateSliceFor(state, role) {
|
|
1352
|
+
const lines = [];
|
|
1353
|
+
if (state.__runCtx) {
|
|
1354
|
+
if (state.__runCtx.runDocs) lines.push(`【本次任务产物夹】${state.__runCtx.runDocs}/(host 已创建;本需求的 PRD/TECHNICAL/QA-REPORT/ACCEPTANCE 全部写这里,夹建后不可变、不归档不升版)`);
|
|
1355
|
+
if (state.__runCtx.sanity) lines.push(state.__runCtx.sanity);
|
|
1356
|
+
if (state.__runCtx.blueprint && (role === "arch" || role === "tech" || role === "dev")) lines.push(state.__runCtx.blueprint);
|
|
1357
|
+
}
|
|
1358
|
+
lines.push("【预编译产品状态(state.json · 权威记忆在 docs/teamflow/memory.md,本块已是够用的索引,勿再全量读历史文档)】");
|
|
1359
|
+
if (state.product.summary) lines.push(`- 产品概要:${state.product.summary}`);
|
|
1360
|
+
if (state.product.techStack && (role === "tech" || role === "dev" || role === "arch")) lines.push(`- 技术栈:${state.product.techStack}`);
|
|
1361
|
+
if (Object.keys(state.acIndex).length && (role === "pm" || role === "qa" || role === "acceptance" || role === "tech")) {
|
|
1362
|
+
const acs = Object.entries(state.acIndex).slice(0, 40);
|
|
1363
|
+
lines.push(`- AC 索引(${acs.length} 条):${acs.map(([k, v]) => `${k} ${v}`).join(";")}`);
|
|
1364
|
+
}
|
|
1365
|
+
if (Object.keys(state.modules).length && (role === "tech" || role === "dev" || role === "arch" || role === "qa")) lines.push(`- 模块(${Object.keys(state.modules).length}):${Object.entries(state.modules).map(([f, c]) => `${f}${c ? "→" + c : ""}`).join(",")}`);
|
|
1366
|
+
if (state.verifyScripts.length && (role === "qa" || role === "tech" || role === "dev")) lines.push(`- 验证脚本:${state.verifyScripts.join(",")}`);
|
|
1367
|
+
if (role === "pm" || role === "acceptance") {
|
|
1368
|
+
if (state.stages.prd) lines.push(`- PRD 摘要:${state.stages.prd}`);
|
|
1369
|
+
if (state.stages.tech) lines.push(`- 技术方案摘要:${state.stages.tech}`);
|
|
1370
|
+
if (state.stages.qa) lines.push(`- QA 摘要:${state.stages.qa}`);
|
|
1371
|
+
} else if (role === "dev" || role === "tech") {
|
|
1372
|
+
if (state.stages.tech) lines.push(`- 技术方案摘要:${state.stages.tech}`);
|
|
1373
|
+
if (state.stages.prd) lines.push(`- PRD 摘要:${state.stages.prd}`);
|
|
1374
|
+
} else if (role === "qa") {
|
|
1375
|
+
if (state.stages.qa) lines.push(`- 上轮 QA 摘要:${state.stages.qa}`);
|
|
1376
|
+
}
|
|
1377
|
+
if (state.lastRun) {
|
|
1378
|
+
const r = state.lastRun;
|
|
1379
|
+
lines.push(`- 上轮:${r.requirement ? r.requirement : ""}${r.verdict ? " → " + r.verdict : ""}${r.folder ? `(${r.folder})` : ""}`);
|
|
1380
|
+
}
|
|
1381
|
+
return lines.join("\n");
|
|
1382
|
+
}
|
|
1383
|
+
/** 让每个阶段产出末尾附带 state 块(将并入 stage output,由 host 提取)。 */
|
|
1384
|
+
const STATE_BLOCK_INSTRUCTION = `\n\n[STATE BLOCK · mandatory at the end] Append one section at the END of your answer (same output as the body; the host indexes it):
|
|
1385
|
+
<!-- state -->{"phase":"<stage-key>","summary":"<≤500 chars: this stage's conclusion, useful for the next run>","memory":["<memory points>"]}<!-- /state -->`;
|
|
1386
|
+
//#endregion
|
|
1387
|
+
//#region host/core/products.ts
|
|
1388
|
+
/**
|
|
1389
|
+
* dsh-plugin-teamflow core — 产品线(product line)装配。
|
|
1390
|
+
*
|
|
1391
|
+
* 全局面板(侧边栏图标 + root `main` 面板)没有会话上下文:宿主据产品线 key
|
|
1392
|
+
* (`$DSH_HOME/teamflow/<key>/`)装配「产品线清单 / 产品线视图 / run 摘要」,
|
|
1393
|
+
* 与按 sessionId 寻址的路由**共用同一批 journal 与 state.json**(同源、不新增数据模型)。
|
|
1394
|
+
*
|
|
1395
|
+
* 依赖方向:types/constants/util → store → core/*;本文件只依赖 store、core/context、core/state。
|
|
1396
|
+
*/
|
|
1397
|
+
/** 按产品线 key 过滤运行(未落 workspace 的旧运行只见于 default)。 */
|
|
1398
|
+
function runsFor(ws) {
|
|
1399
|
+
const arr = [];
|
|
1400
|
+
for (const j of runs.values()) {
|
|
1401
|
+
const rec = j;
|
|
1402
|
+
if (ws) {
|
|
1403
|
+
if ((rec.workspace || (ws === "default" ? "default" : null)) !== ws) continue;
|
|
1404
|
+
}
|
|
1405
|
+
arr.push(rec);
|
|
1406
|
+
}
|
|
1407
|
+
arr.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
|
|
1408
|
+
return arr;
|
|
1409
|
+
}
|
|
1410
|
+
/** run 详情地址(host 生成,client 直接交给右侧栏 openResource——与产物地址同一条原则:
|
|
1411
|
+
* 地址里的产品线 + runId 由 host 决定,client 不拼地址、不引宿主包)。 */
|
|
1412
|
+
function runAddress(product, runId) {
|
|
1413
|
+
return `dsh-resource://teamflow/run/${encodeURIComponent(String(product || "default"))}/${encodeURIComponent(String(runId))}`;
|
|
1414
|
+
}
|
|
1415
|
+
/** 产品线 key 归一化:复用工具层白名单(拒绝盘符/穿越/空白),非法 → null。 */
|
|
1416
|
+
function productKeyOf(product) {
|
|
1417
|
+
return normalizeRoot(product);
|
|
1418
|
+
}
|
|
1419
|
+
/** 该 run 是否属于该产品线(无 workspace 的旧 run 只在 default 兜底可见)。 */
|
|
1420
|
+
function runVisibleIn(j, key) {
|
|
1421
|
+
return !j.workspace || j.workspace === key || key === "default";
|
|
1422
|
+
}
|
|
1423
|
+
/** 单 run 官方口径 usage 汇总(run 列表展示;stage 级明细仍走 snapshot)。 */
|
|
1424
|
+
function runUsageSum(j) {
|
|
1425
|
+
const t = {
|
|
1426
|
+
input: 0,
|
|
1427
|
+
cacheRead: 0,
|
|
1428
|
+
cacheWrite: 0,
|
|
1429
|
+
output: 0,
|
|
1430
|
+
calls: 0
|
|
1431
|
+
};
|
|
1432
|
+
for (const s of j.stages || []) {
|
|
1433
|
+
const u = s && s.usage;
|
|
1434
|
+
if (!u) continue;
|
|
1435
|
+
t.input += u.input || 0;
|
|
1436
|
+
t.cacheRead += u.cacheRead || 0;
|
|
1437
|
+
t.cacheWrite += u.cacheWrite || 0;
|
|
1438
|
+
t.output += u.output || 0;
|
|
1439
|
+
t.calls += u.calls || 0;
|
|
1440
|
+
}
|
|
1441
|
+
return t;
|
|
1442
|
+
}
|
|
1443
|
+
/** run 摘要(list() 与 productView() 共用同一形状)。 */
|
|
1444
|
+
function runBrief(j) {
|
|
1445
|
+
const stages = j.stages || [];
|
|
1446
|
+
return {
|
|
1447
|
+
id: j.id,
|
|
1448
|
+
status: j.status,
|
|
1449
|
+
mode: j.options && j.options.mode || null,
|
|
1450
|
+
startedAt: j.startedAt,
|
|
1451
|
+
endedAt: j.endedAt,
|
|
1452
|
+
agentsStarted: j.agentsStarted,
|
|
1453
|
+
stageCount: stages.length,
|
|
1454
|
+
doneStages: stages.filter((x) => x.status === "done").length,
|
|
1455
|
+
incompleteStages: stages.some((x) => x.status !== "done"),
|
|
1456
|
+
requirement: clip(j.requirement, 60),
|
|
1457
|
+
usage: runUsageSum(j),
|
|
1458
|
+
address: runAddress(j.workspace || "default", j.id),
|
|
1459
|
+
ownerSession: j.ownerSession || null
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
/** 单产品线元信息(产品线清单与产品线视图共用)。 */
|
|
1463
|
+
function productMetaOf(key) {
|
|
1464
|
+
const js = runsFor(key);
|
|
1465
|
+
const st = loadState(key);
|
|
1466
|
+
const path = (js.find((j) => j.workspacePath) || {}).workspacePath || null;
|
|
1467
|
+
const lastRun = st.lastRun || null;
|
|
1468
|
+
return {
|
|
1469
|
+
key,
|
|
1470
|
+
title: st.projectName || (path ? String(path).replace(/\\/g, "/").split("/").filter(Boolean).pop() : key),
|
|
1471
|
+
path,
|
|
1472
|
+
updatedAt: st.updatedAt || (js[0] ? js[0].endedAt || js[0].startedAt || null : null),
|
|
1473
|
+
totalRuns: js.length,
|
|
1474
|
+
activeRuns: js.filter((j) => j.status === "running" || j.status === "pending").length,
|
|
1475
|
+
lastRequirement: lastRun && lastRun.requirement ? clip(lastRun.requirement, 80) : js[0] ? clip(js[0].requirement, 80) : null,
|
|
1476
|
+
lastVerdict: lastRun && lastRun.verdict || null
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
/** 产品线清单:`$DSH_HOME/teamflow` 下带 `backlog/` 或 `runs/` 的目录(无会话上下文也能用)。 */
|
|
1480
|
+
function listProducts() {
|
|
1481
|
+
const out = [];
|
|
1482
|
+
const root = teamflowRoot();
|
|
1483
|
+
let entries = [];
|
|
1484
|
+
try {
|
|
1485
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
1486
|
+
} catch (e) {
|
|
1487
|
+
return out;
|
|
1488
|
+
}
|
|
1489
|
+
for (const ent of entries) {
|
|
1490
|
+
if (!ent.isDirectory() || ent.name === "runs") continue;
|
|
1491
|
+
let sub = [];
|
|
1492
|
+
try {
|
|
1493
|
+
sub = readdirSync(join(root, ent.name));
|
|
1494
|
+
} catch (e) {
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
if (!sub.includes("backlog") && !sub.includes("runs")) continue;
|
|
1498
|
+
out.push(productMetaOf(ent.name));
|
|
1499
|
+
}
|
|
1500
|
+
out.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
1501
|
+
return out;
|
|
1502
|
+
}
|
|
1084
1503
|
/** 默认团队配置文件内容。 */
|
|
1085
1504
|
const DEFAULT_TEAMS_FILE = {
|
|
1086
1505
|
version: 1,
|
|
@@ -1206,6 +1625,32 @@ function gitCmd(cwd, args, timeoutMs = 8e3) {
|
|
|
1206
1625
|
}
|
|
1207
1626
|
}
|
|
1208
1627
|
/**
|
|
1628
|
+
* TeamFlow 自有日志命名空间(工作区相对路径)。
|
|
1629
|
+
*
|
|
1630
|
+
* 为什么单独拎出来:prompts 强制子代理把命令输出与临时验证脚本写进这里(Log discipline / TOKEN_HYGIENE),
|
|
1631
|
+
* 而 prompts 的资源表同时把它定性为**非交付物**(「运行日志 … 日常不读」)。也就是说这批文件是插件
|
|
1632
|
+
* 自己必然生产、且自己声明不该交付的东西——绝不能靠目标仓库的 .gitignore 兜底。
|
|
1633
|
+
* 实锤 assetd `tf-mtwvwpxa-p3vw08`:收口提交 227 个文件里 **208 个(92%)** 是这里的内容
|
|
1634
|
+
* (100 log / 52 json / 44 临时 .mjs / 5 .cjs,623.8 KB),真交付只有 19 个文件。
|
|
1635
|
+
*/
|
|
1636
|
+
const TF_LOG_DIR = "logs/teamflow";
|
|
1637
|
+
/**
|
|
1638
|
+
* 插件发起的提交统一走这里(**禁止裸 `git add -A`**)。
|
|
1639
|
+
*
|
|
1640
|
+
* 用 git magic pathspec 强制排除自有日志:不依赖目标仓库有没有配 .gitignore、也不怕用户改回去。
|
|
1641
|
+
* `-- .` 把提交面收敛到工作区(workspace = 项目根)——与旧 `add -A` 在根目录等价,
|
|
1642
|
+
* 但不再把工作区之外/无关路径一并卷入。
|
|
1643
|
+
*/
|
|
1644
|
+
function tfAddArgs() {
|
|
1645
|
+
return [
|
|
1646
|
+
"add",
|
|
1647
|
+
"-A",
|
|
1648
|
+
"--",
|
|
1649
|
+
".",
|
|
1650
|
+
`:(exclude)${TF_LOG_DIR}`
|
|
1651
|
+
];
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1209
1654
|
* 跑一次状态核对。
|
|
1210
1655
|
* @param path - 工作区绝对路径(workspaceScopeOf(agent).path)。
|
|
1211
1656
|
*/
|
|
@@ -1254,7 +1699,63 @@ function runSanityCheck(path) {
|
|
|
1254
1699
|
//#endregion
|
|
1255
1700
|
//#region host/core/metering.ts
|
|
1256
1701
|
/**
|
|
1257
|
-
*
|
|
1702
|
+
* dsh-plugin-teamflow core — token 计量(官方口径)。
|
|
1703
|
+
* 依赖:types.ts、context.ts(runtime.sessionProjections)。
|
|
1704
|
+
*
|
|
1705
|
+
* 口径与模型 provider 账单一致(模型无关):
|
|
1706
|
+
* - input : 输入(缓存未命中)
|
|
1707
|
+
* - cacheRead : 输入(缓存命中)
|
|
1708
|
+
* - cacheWrite : 输入写入缓存
|
|
1709
|
+
* - output : 输出
|
|
1710
|
+
* billed input = input + cacheRead + cacheWrite。
|
|
1711
|
+
* 缓存命中率 = cacheRead / (input + cacheRead)。
|
|
1712
|
+
*
|
|
1713
|
+
* 来源优先级(2026-09-10 适配 dsh 0.1.5-rc.2):
|
|
1714
|
+
* 1) **官方 Session 投影**(首选):`ctx.sessionProjections.stateOf(session,'tokenUsage')` 取四桶 +
|
|
1715
|
+
* `'sessionStats'` 取调用数——零历史扫描,且与官方 token-meter 同一份 fold(不再自行复刻口径)。
|
|
1716
|
+
* 2) **事件扫描回退**(存量路径):宿主未挂载投影(最小 profile/未来移除)或投影无 provider usage 时,
|
|
1717
|
+
* 沿用 events → snapshotEvents() → ownEvents() 多源回退。宿主自 2026-09-09 起把这三个同步历史读取器
|
|
1718
|
+
* 标记为 deprecated(存量可留、新调用禁止),本路径仅为无投影宿主保底,不再扩展(见 docs/TODO.md)。
|
|
1719
|
+
*/
|
|
1720
|
+
/** 投影字段读数(宽进严出:非法/非正一律 0,不虚报)。 */
|
|
1721
|
+
function countOf(value) {
|
|
1722
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* 投影路径(官方口径首选):
|
|
1726
|
+
* - `tokenUsage`(dsh-token-meter 注册,stateVersion 2)→ totals 四桶:与官方同一份 fold,
|
|
1727
|
+
* `assistant/attempt` 内嵌 stream usage 同样计入、`llm/retry-started` 会先关掉被替换的重试槽位
|
|
1728
|
+
* ——比旧事件扫描(只认 assistant/message)更准,重试不重复计。
|
|
1729
|
+
* - `sessionStats`(dsh-session-stats 注册)steps → 调用数(一个 step = 一次模型请求;
|
|
1730
|
+
* 旧扫描按 assistant/message 的 turn.step 去重,语义等价)。
|
|
1731
|
+
* 返回 null = 投影不可用或该会话无 provider usage —— 交给事件扫描回退(不虚报 0)。
|
|
1732
|
+
*/
|
|
1733
|
+
function projectedUsageOf(run) {
|
|
1734
|
+
try {
|
|
1735
|
+
const projections = runtime.sessionProjections;
|
|
1736
|
+
if (!projections || typeof projections.stateOf !== "function") return null;
|
|
1737
|
+
const session = run && run.localAgent ? run.localAgent.session : null;
|
|
1738
|
+
if (!session) return null;
|
|
1739
|
+
const usage = projections.stateOf(session, "tokenUsage");
|
|
1740
|
+
const totals = usage && usage.totals;
|
|
1741
|
+
if (!totals) return null;
|
|
1742
|
+
const buckets = {
|
|
1743
|
+
input: countOf(totals.uncachedInputTokens),
|
|
1744
|
+
cacheRead: countOf(totals.cacheReadTokens),
|
|
1745
|
+
cacheWrite: countOf(totals.cacheWriteTokens),
|
|
1746
|
+
output: countOf(totals.outputTokens),
|
|
1747
|
+
calls: 0
|
|
1748
|
+
};
|
|
1749
|
+
if (totalTokensOf(buckets) <= 0) return null;
|
|
1750
|
+
const stats = projections.stateOf(session, "sessionStats");
|
|
1751
|
+
buckets.calls = countOf(stats && stats.steps) || 1;
|
|
1752
|
+
return buckets;
|
|
1753
|
+
} catch (e) {
|
|
1754
|
+
return null;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* 采集子代理会话事件(存量回退路径——2026-09-07 实锤 r38 usage 全空):
|
|
1258
1759
|
* 宿主新版 Session(session v2)已无 `events` 属性/getter(仅私有 eventsSnapshot 缓存 +
|
|
1259
1760
|
* 官方 snapshotEvents()/ownEvents() 方法),旧实现读 session.events = undefined → usage 全 null。
|
|
1260
1761
|
* 回退链(与 guard.eventsOf 同款语义):events(老宿主快照,兼容)→ snapshotEvents()(官方完整日志)
|
|
@@ -1302,9 +1803,16 @@ function usageOfEvent(e) {
|
|
|
1302
1803
|
}
|
|
1303
1804
|
/**
|
|
1304
1805
|
* 累计子代理会话中所有 LLM 调用的真实 usage(官方三桶 + 调用数)。
|
|
1305
|
-
*
|
|
1806
|
+
* 来源优先级:官方 Session 投影(首选,零历史扫描)→ 事件扫描(无投影宿主的存量回退)。
|
|
1807
|
+
* 返回 null 表示两条路径都拿不到 usage(会话未暴露投影与事件 / 无数据)。
|
|
1306
1808
|
*/
|
|
1307
1809
|
function accumulateSessionUsage(run) {
|
|
1810
|
+
const projected = projectedUsageOf(run);
|
|
1811
|
+
if (projected) return projected;
|
|
1812
|
+
return scannedUsageOf(run);
|
|
1813
|
+
}
|
|
1814
|
+
/** 事件扫描回退(投影未挂载/无数据时使用;沿用 2026-09-07 的多源回退语义,不改行为)。 */
|
|
1815
|
+
function scannedUsageOf(run) {
|
|
1308
1816
|
const events = sessionEventsOf(run);
|
|
1309
1817
|
if (events.length === 0) return null;
|
|
1310
1818
|
const buckets = {
|
|
@@ -1332,11 +1840,24 @@ function accumulateSessionUsage(run) {
|
|
|
1332
1840
|
buckets.calls = seen.size || 1;
|
|
1333
1841
|
return buckets;
|
|
1334
1842
|
}
|
|
1335
|
-
/** 官方口径总消耗(billed input + output,含 cacheRead/cacheWrite
|
|
1843
|
+
/** 官方口径总消耗(billed input + output,含 cacheRead/cacheWrite)——**汇报/展示**口径。 */
|
|
1336
1844
|
function totalTokensOf(usage) {
|
|
1337
1845
|
if (!usage) return 0;
|
|
1338
1846
|
return (usage.input || 0) + (usage.cacheRead || 0) + (usage.cacheWrite || 0) + (usage.output || 0);
|
|
1339
1847
|
}
|
|
1848
|
+
/**
|
|
1849
|
+
* 熔断口径「新增消耗」= input + cacheWrite + output(**排除 cacheRead**)。
|
|
1850
|
+
*
|
|
1851
|
+
* 为什么与汇报口径分家(2026-09-11):cacheRead 是上下文复用的缓存重放,单价低且是
|
|
1852
|
+
* **复用证据**而非烧钱信号;把它计入熔断,会让预算被「每步 1M 量级的命中」瞬间打爆——
|
|
1853
|
+
* 实锤 assetd tf-mtwvwpxa-p3vw08 的 T5:报「累计 token 1886k 超出阶段预算 60k」,
|
|
1854
|
+
* 其中 1830k 是 cacheRead,真实新增仅 55k;后果是任何 dev 任务一失败就熔断,
|
|
1855
|
+
* RETRY_LIMIT 永不生效。汇报仍用 `totalTokensOf`(官方口径,AGENTS §4 不变)。
|
|
1856
|
+
*/
|
|
1857
|
+
function freshTokensOf(usage) {
|
|
1858
|
+
if (!usage) return 0;
|
|
1859
|
+
return (usage.input || 0) + (usage.cacheWrite || 0) + (usage.output || 0);
|
|
1860
|
+
}
|
|
1340
1861
|
//#endregion
|
|
1341
1862
|
//#region host/core/guard.ts
|
|
1342
1863
|
/**
|
|
@@ -1353,8 +1874,12 @@ function totalTokensOf(usage) {
|
|
|
1353
1874
|
* ⚠️ 状态判定(实锤 run tf-mte906e9):大文件 read-edit 循环是正常模式——模型反复 read 同一大文件
|
|
1354
1875
|
* (每次 edit 后内容已变,必须重读确认)、输出高度相似的「读后分析」,逐字片段在 400 条窗口内
|
|
1355
1876
|
* 可累积 ≥12 次——伴随 edit/write 变更调用时只记录观察,不中止(否则大文件修改任务全被误杀)。
|
|
1356
|
-
* B. 挂死检测:连续 GUARD_SILENCE_MS
|
|
1877
|
+
* B. 挂死检测:连续 GUARD_SILENCE_MS 没有**任何已提交事件**(provider 层挂起/连接静默死亡)
|
|
1357
1878
|
* → outcome='stalled'(走正常预算门 → 熔断转人工,不自动重试烧钱)。
|
|
1879
|
+
* 2026-09-10:时间来源改为**官方 `subagentTiming` 投影**(`active.through` = 该投影 cut 上
|
|
1880
|
+
* 最新事件时间,由宿主在已提交事件上折叠)——不再依赖「三源取最长视图」的长度启发式
|
|
1881
|
+
* (r1 QA 误判的根因就是那个视图会失明);投影不可用时回退旧启发式。长工具静默执行
|
|
1882
|
+
* (跑 12 分钟测试无输出)仍由 agent 活动守卫豁免,不做误杀。
|
|
1358
1883
|
* C. 空转检测:会话仍在产出事件,但连续 GUARD_NO_TOOL_MS 没有任何工具调用
|
|
1359
1884
|
* (纯推理打转/改写式循环;正常 agent 每分钟都在调工具)→ outcome='stalled'。
|
|
1360
1885
|
* 兜底关系:复读判定放宽后,edit 后陷入死循环的漏网场景由 C(长时间无工具调用)兜住。
|
|
@@ -1414,36 +1939,54 @@ function eventsOf(run) {
|
|
|
1414
1939
|
function normalizeFragment(s) {
|
|
1415
1940
|
return String(s || "").toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "");
|
|
1416
1941
|
}
|
|
1417
|
-
/**
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1422
|
-
|
|
1423
|
-
|
|
1942
|
+
/**
|
|
1943
|
+
* 官方 `subagentTiming` 投影读数(挂死检测首选源,2026-09-10 改)。
|
|
1944
|
+
* 形状 `{settledMs, active?:{since, through}}`——`through` 是该投影 cut 上**最新事件时间**,
|
|
1945
|
+
* 由宿主在已提交事件上折叠,不受 session.events 快照失明影响(r1 QA 误判根因)。
|
|
1946
|
+
* 返回 null = 投影不可用(未挂载 / 该子代理无 descriptor)→ 回退事件数增长启发式。
|
|
1947
|
+
*/
|
|
1948
|
+
function timingOf(run) {
|
|
1424
1949
|
try {
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1950
|
+
const projections = runtime.sessionProjections;
|
|
1951
|
+
if (!projections || typeof projections.stateOf !== "function") return null;
|
|
1952
|
+
const local = run && run.localAgent;
|
|
1953
|
+
const session = local && local.session;
|
|
1954
|
+
if (!session) return null;
|
|
1955
|
+
const timing = projections.stateOf(session, "subagentTiming");
|
|
1956
|
+
if (!timing || typeof timing !== "object") return null;
|
|
1957
|
+
const through = timing.active && timing.active.through;
|
|
1958
|
+
return { activeThrough: typeof through === "number" && Number.isFinite(through) ? through : void 0 };
|
|
1959
|
+
} catch (e) {
|
|
1960
|
+
return null;
|
|
1961
|
+
}
|
|
1428
1962
|
}
|
|
1429
|
-
|
|
1963
|
+
/** 观测→执行闭环:向运行中的子代理注入轻提醒(不打断,下一 step 可见)。
|
|
1964
|
+
*
|
|
1965
|
+
* 通道(2026-09-10 改):`run.localAgent.inject()` —— 宿主官方 Agent 通道。next-step 队列由
|
|
1966
|
+
* agent loop 在 `preStep` 内、tool/result 之后整批认领,因此**不存在**「插进
|
|
1967
|
+
* assistant(tool_calls) → tool/result 之间触发 provider 400」的窗口;旧实现「先入队
|
|
1968
|
+
* `__teamflowPending`、观察到 step/end 再 session.append」的时序状态机整体删除。
|
|
1969
|
+
* (旧注释「subagents.start 句柄无 inject」是错的:`run.localAgent` 是活 Agent,
|
|
1970
|
+
* 有 `inject/steer/followup` —— `packages/core/agent/src/runtime-types.ts`。)
|
|
1971
|
+
*
|
|
1972
|
+
* 语义:`inject` 是 best-effort(可能晚一个 step),且不唤醒 idle driver——提醒只用于
|
|
1973
|
+
* 「仍在跑的 agent」;退化中止仍走 fire()/dispose(),不改为 steer 纠偏(后者是独立课题)。 */
|
|
1974
|
+
function injectReminder(run, text) {
|
|
1430
1975
|
try {
|
|
1431
|
-
const
|
|
1432
|
-
if (!
|
|
1433
|
-
|
|
1434
|
-
if (!local || typeof local.session?.append !== "function") return;
|
|
1435
|
-
for (const text of queue.splice(0)) local.session.append("user/message", {
|
|
1436
|
-
id: crypto.randomUUID(),
|
|
1437
|
-
role: "user",
|
|
1976
|
+
const agent = run && run.localAgent;
|
|
1977
|
+
if (!agent || typeof agent.inject !== "function") return;
|
|
1978
|
+
agent.inject(createUserMessage({
|
|
1438
1979
|
content: [{
|
|
1439
1980
|
type: "text",
|
|
1440
1981
|
text
|
|
1441
1982
|
}],
|
|
1442
1983
|
source: {
|
|
1443
1984
|
kind: "plugin",
|
|
1444
|
-
plugin: "teamflow"
|
|
1985
|
+
plugin: "dsh-plugin-teamflow",
|
|
1986
|
+
form: "notice",
|
|
1987
|
+
summary: "护栏轻提醒"
|
|
1445
1988
|
}
|
|
1446
|
-
}
|
|
1989
|
+
}));
|
|
1447
1990
|
} catch (e) {}
|
|
1448
1991
|
}
|
|
1449
1992
|
/**
|
|
@@ -1485,31 +2028,37 @@ function startStageGuard(opts) {
|
|
|
1485
2028
|
stage.guardReason = reason;
|
|
1486
2029
|
stage.guardOutcome = outcome;
|
|
1487
2030
|
if (outcome === "stalled") try {
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
2031
|
+
const timing = timingOf(run);
|
|
2032
|
+
let detail;
|
|
2033
|
+
if (timing) detail = `subagentTiming.through=${timing.activeThrough === void 0 ? "(无 open turn)" : timing.activeThrough}`;
|
|
2034
|
+
else {
|
|
2035
|
+
const local = run.localAgent;
|
|
2036
|
+
const session = local && local.session;
|
|
2037
|
+
const lens = [];
|
|
2038
|
+
if (session) {
|
|
2039
|
+
try {
|
|
2040
|
+
const r = session.events;
|
|
2041
|
+
lens.push(`events=${Array.isArray(r) ? r.length : typeof r === "function" ? r().length : "?"}`);
|
|
2042
|
+
} catch (e) {
|
|
2043
|
+
lens.push("events=err");
|
|
2044
|
+
}
|
|
2045
|
+
try {
|
|
2046
|
+
lens.push(`snap=${typeof session.snapshotEvents === "function" ? session.snapshotEvents().length : "-"}`);
|
|
2047
|
+
} catch (e) {
|
|
2048
|
+
lens.push("snap=err");
|
|
2049
|
+
}
|
|
2050
|
+
try {
|
|
2051
|
+
lens.push(`own=${typeof session.ownEvents === "function" ? session.ownEvents().length : "-"}`);
|
|
2052
|
+
} catch (e) {
|
|
2053
|
+
lens.push("own=err");
|
|
2054
|
+
}
|
|
1507
2055
|
}
|
|
2056
|
+
detail = `投影不可用,回退事件视图:${lens.join(" / ") || "session 不可访问"}`;
|
|
1508
2057
|
}
|
|
1509
2058
|
journal.logs.push({
|
|
1510
2059
|
t: Date.now(),
|
|
1511
2060
|
level: "warn",
|
|
1512
|
-
message: `${label} 挂死诊断:${
|
|
2061
|
+
message: `${label} 挂死诊断:${detail}`
|
|
1513
2062
|
});
|
|
1514
2063
|
} catch (e) {}
|
|
1515
2064
|
try {
|
|
@@ -1567,7 +2116,6 @@ function startStageGuard(opts) {
|
|
|
1567
2116
|
}
|
|
1568
2117
|
}
|
|
1569
2118
|
observeToolCalls(newEvents);
|
|
1570
|
-
if (newEvents.some((ev) => ev?.type === "step/end")) flushReminders(run);
|
|
1571
2119
|
processed = events.length;
|
|
1572
2120
|
}
|
|
1573
2121
|
for (const ev of events.slice(-200)) {
|
|
@@ -1581,7 +2129,28 @@ function startStageGuard(opts) {
|
|
|
1581
2129
|
break;
|
|
1582
2130
|
}
|
|
1583
2131
|
}
|
|
1584
|
-
|
|
2132
|
+
const timing = timingOf(run);
|
|
2133
|
+
if (timing) {
|
|
2134
|
+
if (timing.activeThrough === void 0) lastGrowthAt = Date.now();
|
|
2135
|
+
else if (Date.now() - timing.activeThrough > 6e5) {
|
|
2136
|
+
if (lastMutationAt > 0 && isAgentBusy(run)) {
|
|
2137
|
+
if (!busyWarned) {
|
|
2138
|
+
busyWarned = true;
|
|
2139
|
+
try {
|
|
2140
|
+
journal.logs.push({
|
|
2141
|
+
t: Date.now(),
|
|
2142
|
+
level: "warn",
|
|
2143
|
+
message: `${label} 已提交事件静默(subagentTiming.through ${Math.round((Date.now() - timing.activeThrough) / 1e3)}s 未推进)但 agent 仍活动——视为长工具执行而非挂死,继续观察`
|
|
2144
|
+
});
|
|
2145
|
+
} catch (e) {}
|
|
2146
|
+
}
|
|
2147
|
+
lastGrowthAt = Date.now();
|
|
2148
|
+
} else {
|
|
2149
|
+
fire(`挂死(${Math.round(GUARD_SILENCE_MS / 6e4)} 分钟无任何已提交事件,来源:subagentTiming 投影)`, "stalled");
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
} else lastGrowthAt = Date.now();
|
|
2153
|
+
} else if (events.length !== lastEventCount) {
|
|
1585
2154
|
lastEventCount = events.length;
|
|
1586
2155
|
lastGrowthAt = Date.now();
|
|
1587
2156
|
} else if (Date.now() - lastGrowthAt > 6e5) {
|
|
@@ -1644,6 +2213,45 @@ function startStageGuard(opts) {
|
|
|
1644
2213
|
* dsh-plugin-teamflow core — 子代理执行器(并发池 / 单阶段运行 / 重试与熔断)。
|
|
1645
2214
|
* 依赖:util/constants/types + core(context/metering)。
|
|
1646
2215
|
*/
|
|
2216
|
+
/**
|
|
2217
|
+
* 推理强度能力探测(缓存,2026-09-11):只有宿主明确声明该路由支持某档位才下发。
|
|
2218
|
+
* 宿主对**不支持的值硬失败且不降级**(`UNSUPPORTED_REASONING_EFFORT`),所以宁可不下发。
|
|
2219
|
+
* 返回 null = 探测不可用(老宿主/未声明容量)→ 调用方一律不下发,保持宿主默认。
|
|
2220
|
+
*/
|
|
2221
|
+
const effortSupportCache = /* @__PURE__ */ new Map();
|
|
2222
|
+
async function supportedEfforts(route) {
|
|
2223
|
+
if (!route.provider || !route.model) return null;
|
|
2224
|
+
const llm = runtime.llm;
|
|
2225
|
+
if (!llm || typeof llm.resolveModelInfo !== "function") return null;
|
|
2226
|
+
const key = `${route.provider}/${route.model || ""}`;
|
|
2227
|
+
if (effortSupportCache.has(key)) return effortSupportCache.get(key) || null;
|
|
2228
|
+
let out = null;
|
|
2229
|
+
try {
|
|
2230
|
+
const info = await llm.resolveModelInfo(route.provider, route.model);
|
|
2231
|
+
const efforts = info && info.reasoning && info.reasoning.efforts;
|
|
2232
|
+
if (Array.isArray(efforts)) out = efforts.map((e) => typeof e === "string" ? e : e && typeof e === "object" && typeof e.id === "string" ? e.id : null).filter((x) => typeof x === "string" && x.length > 0);
|
|
2233
|
+
} catch (e) {
|
|
2234
|
+
out = null;
|
|
2235
|
+
}
|
|
2236
|
+
effortSupportCache.set(key, out);
|
|
2237
|
+
return out;
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* 解析本阶段要下发的推理强度:
|
|
2241
|
+
* - 阶段未要求降档(effortHint 空)→ 不传,宿主默认(DeepSeek high)
|
|
2242
|
+
* - 第 1 次尝试用 hint;**重试回升 'high'**(质量优先,ADR-0006)
|
|
2243
|
+
* - 只有探测到该路由支持该档位才返回,否则不传(防 `UNSUPPORTED_REASONING_EFFORT` 硬失败)
|
|
2244
|
+
* - 未下发时返回原因文本 → 调用方记 warn(这类静默失败必须可见,见 2026-09-11 实锤)
|
|
2245
|
+
*/
|
|
2246
|
+
async function resolveStageEffort(route, attempt, effortHint) {
|
|
2247
|
+
const base = effortHint && String(effortHint).trim() ? String(effortHint).trim() : null;
|
|
2248
|
+
if (!base) return {};
|
|
2249
|
+
const wanted = attempt > 1 ? "high" : base;
|
|
2250
|
+
const supported = await supportedEfforts(route);
|
|
2251
|
+
if (!supported) return { skip: `路由 ${route.provider || "?"}/${route.model || "?"} 未声明 reasoning.efforts(或探测不可用)` };
|
|
2252
|
+
if (supported.indexOf(wanted) === -1) return { skip: `路由不支持 ${wanted}(可用:${supported.join("/") || "无"})` };
|
|
2253
|
+
return { effort: wanted };
|
|
2254
|
+
}
|
|
1647
2255
|
/** 并发池:按 max 个 worker 消费 items,返回同序结果。 */
|
|
1648
2256
|
async function runPool(items, max, fn) {
|
|
1649
2257
|
const results = new Array(items.length);
|
|
@@ -1702,7 +2310,7 @@ function resolveChildRoute(parent) {
|
|
|
1702
2310
|
return out;
|
|
1703
2311
|
}
|
|
1704
2312
|
/** 运行单个阶段子代理:执行 + 产出实质校验 + token 双口径计量 + stage 状态流转。 */
|
|
1705
|
-
async function runAgent(journal, parent, label, phase, prompt, signal, taskKey) {
|
|
2313
|
+
async function runAgent(journal, parent, label, phase, prompt, signal, taskKey, attempt = 1, effortHint) {
|
|
1706
2314
|
const maxSeq = journal.stages.length ? Math.max(...journal.stages.map((s) => s.seq)) : 0;
|
|
1707
2315
|
let stageText = null;
|
|
1708
2316
|
const stage = {
|
|
@@ -1726,11 +2334,24 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey)
|
|
|
1726
2334
|
let cancelGuard = null;
|
|
1727
2335
|
try {
|
|
1728
2336
|
const route = resolveChildRoute(parent);
|
|
1729
|
-
const
|
|
2337
|
+
const eff = await resolveStageEffort(route, attempt, effortHint);
|
|
2338
|
+
const effort = eff.effort;
|
|
2339
|
+
const agentOptions = route.provider || route.model || effort ? {
|
|
1730
2340
|
...route.provider ? { provider: route.provider } : {},
|
|
1731
2341
|
...route.model ? { model: route.model } : {},
|
|
1732
|
-
...route.maxTokens ? { maxTokens: route.maxTokens } : {}
|
|
2342
|
+
...route.maxTokens ? { maxTokens: route.maxTokens } : {},
|
|
2343
|
+
...effort ? { reasoningEffort: effort } : {}
|
|
1733
2344
|
} : void 0;
|
|
2345
|
+
if (effort && !journal.cancelled) journal.logs.push({
|
|
2346
|
+
t: Date.now(),
|
|
2347
|
+
level: "info",
|
|
2348
|
+
message: `${label} 推理强度:${effort}${attempt > 1 ? "(重试回升)" : "(机械阶段降档)"}`
|
|
2349
|
+
});
|
|
2350
|
+
else if (eff.skip && !journal.cancelled) journal.logs.push({
|
|
2351
|
+
t: Date.now(),
|
|
2352
|
+
level: "warn",
|
|
2353
|
+
message: `${label} 推理强度未降档:${eff.skip}——保持宿主默认`
|
|
2354
|
+
});
|
|
1734
2355
|
run = await runtime.subagents.start(providerName(), {
|
|
1735
2356
|
label,
|
|
1736
2357
|
prompt: [{
|
|
@@ -1765,15 +2386,21 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey)
|
|
|
1765
2386
|
const stop = result && result.stopReason;
|
|
1766
2387
|
const text = extractText(result && result.output);
|
|
1767
2388
|
stageText = text;
|
|
2389
|
+
const verdict = judgeDeliverable(phase, text);
|
|
1768
2390
|
if (journal.cancelled) {
|
|
1769
2391
|
stage.status = "cancelled";
|
|
1770
2392
|
stage.outcome = "cancelled";
|
|
1771
2393
|
return null;
|
|
1772
2394
|
}
|
|
1773
|
-
if (stop === "completed" && text &&
|
|
2395
|
+
if (stop === "completed" && text && verdict.ok) {
|
|
1774
2396
|
stage.status = "done";
|
|
1775
2397
|
stage.outcome = "completed";
|
|
1776
2398
|
stage.output = clip(text, 5e4);
|
|
2399
|
+
if (verdict.refusal) journal.logs.push({
|
|
2400
|
+
t: Date.now(),
|
|
2401
|
+
level: "warn",
|
|
2402
|
+
message: `${label} 产出含疑似拒绝措辞「${verdict.refusal.phrase}」(原文:${verdict.refusal.context})——但已带 [Verification evidence] 块,判为交付;措辞仅作诊断不再否决`
|
|
2403
|
+
});
|
|
1777
2404
|
return text;
|
|
1778
2405
|
}
|
|
1779
2406
|
if (stage.guardReason) {
|
|
@@ -1792,20 +2419,19 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey)
|
|
|
1792
2419
|
stage.outcome = stop === "completed" && text ? "insubstantial" : stop || "error";
|
|
1793
2420
|
const errDetail = result && result.error;
|
|
1794
2421
|
if (stage.outcome === "insubstantial") {
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
stage.summary = `产出未通过实质校验:命中拒绝词「${hit.phrase}」(原文:${hit.context}),视为未交付`;
|
|
2422
|
+
if (verdict.reason === "refusal" && verdict.refusal) {
|
|
2423
|
+
stage.summary = `产出未通过实质校验:无验证证据块且命中拒绝词「${verdict.refusal.phrase}」(原文:${verdict.refusal.context}),视为未交付`;
|
|
1798
2424
|
journal.logs.push({
|
|
1799
2425
|
t: Date.now(),
|
|
1800
2426
|
level: "warn",
|
|
1801
|
-
message: `${label} 产出命中拒绝词「${
|
|
2427
|
+
message: `${label} 产出命中拒绝词「${verdict.refusal.phrase}」且无 [Verification evidence] 块`
|
|
1802
2428
|
});
|
|
1803
2429
|
} else {
|
|
1804
|
-
stage.summary = `产出未通过实质校验:内容过短(${
|
|
2430
|
+
stage.summary = `产出未通过实质校验:内容过短(${verdict.length} 字符 < ${verdict.min} 下限),视为未交付`;
|
|
1805
2431
|
journal.logs.push({
|
|
1806
2432
|
t: Date.now(),
|
|
1807
2433
|
level: "warn",
|
|
1808
|
-
message: `${label} 产出过短(${
|
|
2434
|
+
message: `${label} 产出过短(${verdict.length} 字符),未通过实质校验`
|
|
1809
2435
|
});
|
|
1810
2436
|
}
|
|
1811
2437
|
if (text) stage.output = clip(text, 4e3);
|
|
@@ -1845,29 +2471,33 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey)
|
|
|
1845
2471
|
} catch (e2) {}
|
|
1846
2472
|
}
|
|
1847
2473
|
}
|
|
1848
|
-
/**
|
|
1849
|
-
|
|
2474
|
+
/**
|
|
2475
|
+
* 单阶段重试 + token 熔断(**新增口径**:input+cacheWrite+output,排除 cacheRead——见
|
|
2476
|
+
* `FRESH_TOKEN_BUDGET` 与 metering.freshTokensOf;汇报仍走官方 totalTokensOf 口径)。
|
|
2477
|
+
* 顺序:不可重试/外部中止/护栏中止 → 预算门 → 自动重试(预算合理时重试优先于熔断,2026-09-11 修正)。
|
|
2478
|
+
* `effortHint`:机械阶段的推理强度降档提示(第 1 次尝试生效,重试自动回升 high,见 resolveStageEffort)。 */
|
|
2479
|
+
async function withRetry(journal, parent, label, phase, prompt, signal, taskKey, effortHint) {
|
|
1850
2480
|
let attempts = 0;
|
|
1851
|
-
let
|
|
2481
|
+
let freshTokens = 0;
|
|
1852
2482
|
let lastStage = null;
|
|
1853
2483
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
1854
2484
|
attempts = attempt;
|
|
1855
2485
|
const labelNow = attempt > 1 ? `${label}(第 ${attempt} 次重试)` : label;
|
|
1856
2486
|
const promptNow = attempt > 1 && lastStage ? prompt + buildRetryDiagnostic(attempt, lastStage) : prompt;
|
|
1857
2487
|
const beforeLen = journal.stages.length;
|
|
1858
|
-
const result = await runAgent(journal, parent, labelNow, phase, promptNow, signal, taskKey);
|
|
2488
|
+
const result = await runAgent(journal, parent, labelNow, phase, promptNow, signal, taskKey, attempt, effortHint);
|
|
1859
2489
|
lastStage = journal.stages[beforeLen] || null;
|
|
1860
|
-
if (lastStage && lastStage.phase === phase)
|
|
2490
|
+
if (lastStage && lastStage.phase === phase) freshTokens += freshTokensOf(lastStage.usage);
|
|
1861
2491
|
if (result) return {
|
|
1862
2492
|
text: result,
|
|
1863
2493
|
attempts,
|
|
1864
|
-
|
|
2494
|
+
freshTokens,
|
|
1865
2495
|
stage: lastStage
|
|
1866
2496
|
};
|
|
1867
2497
|
if (journal.cancelled) return {
|
|
1868
2498
|
text: null,
|
|
1869
2499
|
attempts,
|
|
1870
|
-
|
|
2500
|
+
freshTokens,
|
|
1871
2501
|
stage: lastStage
|
|
1872
2502
|
};
|
|
1873
2503
|
if (lastStage && isUnretryable(lastStage.outcome, lastStage.outcome)) {
|
|
@@ -1880,7 +2510,7 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey)
|
|
|
1880
2510
|
return {
|
|
1881
2511
|
text: null,
|
|
1882
2512
|
attempts,
|
|
1883
|
-
|
|
2513
|
+
freshTokens,
|
|
1884
2514
|
stage: lastStage
|
|
1885
2515
|
};
|
|
1886
2516
|
}
|
|
@@ -1894,7 +2524,7 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey)
|
|
|
1894
2524
|
return {
|
|
1895
2525
|
text: null,
|
|
1896
2526
|
attempts,
|
|
1897
|
-
|
|
2527
|
+
freshTokens,
|
|
1898
2528
|
stage: lastStage
|
|
1899
2529
|
};
|
|
1900
2530
|
}
|
|
@@ -1908,7 +2538,7 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey)
|
|
|
1908
2538
|
return {
|
|
1909
2539
|
text: null,
|
|
1910
2540
|
attempts,
|
|
1911
|
-
|
|
2541
|
+
freshTokens,
|
|
1912
2542
|
stage: lastStage
|
|
1913
2543
|
};
|
|
1914
2544
|
}
|
|
@@ -1922,21 +2552,21 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey)
|
|
|
1922
2552
|
return {
|
|
1923
2553
|
text: null,
|
|
1924
2554
|
attempts,
|
|
1925
|
-
|
|
2555
|
+
freshTokens,
|
|
1926
2556
|
stage: lastStage
|
|
1927
2557
|
};
|
|
1928
2558
|
}
|
|
1929
|
-
if (
|
|
2559
|
+
if (freshTokens >= 2e5) {
|
|
1930
2560
|
journal.logs.push({
|
|
1931
2561
|
t: Date.now(),
|
|
1932
2562
|
level: "error",
|
|
1933
|
-
message: `${label}
|
|
2563
|
+
message: `${label} 累计新增 token ${Math.round(freshTokens / 1e3)}k(input+cacheWrite+output,不含缓存命中)超出预算 ${Math.round(FRESH_TOKEN_BUDGET / 1e3)}k,熔断,需人工介入`
|
|
1934
2564
|
});
|
|
1935
2565
|
journal.humanIntervention = true;
|
|
1936
2566
|
return {
|
|
1937
2567
|
text: null,
|
|
1938
2568
|
attempts,
|
|
1939
|
-
|
|
2569
|
+
freshTokens,
|
|
1940
2570
|
stage: lastStage
|
|
1941
2571
|
};
|
|
1942
2572
|
}
|
|
@@ -1957,176 +2587,11 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey)
|
|
|
1957
2587
|
return {
|
|
1958
2588
|
text: null,
|
|
1959
2589
|
attempts,
|
|
1960
|
-
|
|
2590
|
+
freshTokens,
|
|
1961
2591
|
stage: lastStage
|
|
1962
2592
|
};
|
|
1963
2593
|
}
|
|
1964
2594
|
//#endregion
|
|
1965
|
-
//#region host/core/state.ts
|
|
1966
|
-
/**
|
|
1967
|
-
* dsh-plugin-teamflow core — state.json 预编译上下文索引。
|
|
1968
|
-
*
|
|
1969
|
-
* 目标:解决「每个新 run 都要从小代理全量读历史文档(PRD/TECH/QA)来重建认知」的 token 爆炸。
|
|
1970
|
-
* state.json 是跨 run 累积的结构化索引:每次 run 结束后由各阶段把「精简结论」沉淀进来,
|
|
1971
|
-
* 下一个 run 的子代理只读注入的 state slice,不再重复读全套历史文档。
|
|
1972
|
-
*
|
|
1973
|
-
* 设计原则:
|
|
1974
|
-
* - memory.md 保持权威记忆(人读);state.json 是预编译索引(机器喂给子代理)。
|
|
1975
|
-
* - 子代理不直接读 state.json 文件,由 host 在开工时按角色注入相关 slice 到 prompt。
|
|
1976
|
-
* - state.json 只存「结论/指针」,不存全文;具体内容仍指向 docs/teamflow/ 下的活文档。
|
|
1977
|
-
*/
|
|
1978
|
-
/** 空态 state。 */
|
|
1979
|
-
function emptyState() {
|
|
1980
|
-
return {
|
|
1981
|
-
version: 1,
|
|
1982
|
-
projectName: null,
|
|
1983
|
-
updatedAt: null,
|
|
1984
|
-
product: {
|
|
1985
|
-
summary: null,
|
|
1986
|
-
techStack: null
|
|
1987
|
-
},
|
|
1988
|
-
lastRunFolder: null,
|
|
1989
|
-
modules: {},
|
|
1990
|
-
verifyScripts: [],
|
|
1991
|
-
acIndex: {},
|
|
1992
|
-
stages: {},
|
|
1993
|
-
lastRun: null
|
|
1994
|
-
};
|
|
1995
|
-
}
|
|
1996
|
-
/** state.json 路径:$DSH_HOME/teamflow/<projectKey>/state.json */
|
|
1997
|
-
function stateFile(projectKey) {
|
|
1998
|
-
return join(teamflowRoot(), projectKey, "state.json");
|
|
1999
|
-
}
|
|
2000
|
-
/** 读取(不存在返回空态)。 */
|
|
2001
|
-
function loadState(projectKey) {
|
|
2002
|
-
const file = stateFile(projectKey);
|
|
2003
|
-
try {
|
|
2004
|
-
if (existsSync(file)) {
|
|
2005
|
-
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
2006
|
-
const base = emptyState();
|
|
2007
|
-
if (raw && typeof raw === "object") {
|
|
2008
|
-
base.projectName = raw.projectName ?? null;
|
|
2009
|
-
base.updatedAt = raw.updatedAt ?? null;
|
|
2010
|
-
base.product = {
|
|
2011
|
-
...base.product,
|
|
2012
|
-
...raw.product || {}
|
|
2013
|
-
};
|
|
2014
|
-
base.lastRunFolder = raw.lastRunFolder ?? null;
|
|
2015
|
-
base.modules = raw.modules || {};
|
|
2016
|
-
base.verifyScripts = Array.isArray(raw.verifyScripts) ? raw.verifyScripts : [];
|
|
2017
|
-
base.acIndex = raw.acIndex || {};
|
|
2018
|
-
base.stages = raw.stages || {};
|
|
2019
|
-
base.lastRun = raw.lastRun ?? null;
|
|
2020
|
-
return base;
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
} catch (e) {}
|
|
2024
|
-
return emptyState();
|
|
2025
|
-
}
|
|
2026
|
-
/** 保存。 */
|
|
2027
|
-
function saveState(projectKey, state) {
|
|
2028
|
-
const file = stateFile(projectKey);
|
|
2029
|
-
try {
|
|
2030
|
-
mkdirSync(join(teamflowRoot(), projectKey), { recursive: true });
|
|
2031
|
-
state.updatedAt = Date.now();
|
|
2032
|
-
writeFileSync(file, JSON.stringify(state, null, 2), "utf8");
|
|
2033
|
-
return true;
|
|
2034
|
-
} catch (e) {
|
|
2035
|
-
console.error("[teamflow] saveState failed", e?.message);
|
|
2036
|
-
return false;
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
/** 从阶段产出文本中提取 `<!-- state -->{...}<!-- /state -->` 块(找不到返回 null)。 */
|
|
2040
|
-
function extractStateBlock(text) {
|
|
2041
|
-
const m = (text === null || text === void 0 ? "" : String(text)).match(/<!--\s*state\s*-->([\s\S]*?)(?:<!--\s*\/state\s*-->|$)/);
|
|
2042
|
-
if (!m || !m[1]) return null;
|
|
2043
|
-
try {
|
|
2044
|
-
const raw = JSON.parse(m[1].trim());
|
|
2045
|
-
if (raw && typeof raw === "object") return raw;
|
|
2046
|
-
} catch (e) {}
|
|
2047
|
-
return null;
|
|
2048
|
-
}
|
|
2049
|
-
/** 把阶段产出的 state 块合并进 state.json。 */
|
|
2050
|
-
function mergeStateBlock(projectKey, block, phase) {
|
|
2051
|
-
const state = loadState(projectKey);
|
|
2052
|
-
const key = block && block.phase || phase || "other";
|
|
2053
|
-
if (block) {
|
|
2054
|
-
if (typeof block.summary === "string" && block.summary.trim()) state.stages[key] = block.summary.trim();
|
|
2055
|
-
if (Array.isArray(block.touched)) {
|
|
2056
|
-
for (const f of block.touched) if (typeof f === "string" && f) state.modules[f] = state.modules[f] || "touched";
|
|
2057
|
-
}
|
|
2058
|
-
if (typeof block.verdict === "string" && block.verdict) {
|
|
2059
|
-
state.lastRun = state.lastRun || {};
|
|
2060
|
-
state.lastRun.verdict = block.verdict;
|
|
2061
|
-
}
|
|
2062
|
-
if (block.extra && typeof block.extra === "object") {
|
|
2063
|
-
if (Array.isArray(block.extra.verifyScripts)) {
|
|
2064
|
-
for (const s of block.extra.verifyScripts) if (typeof s === "string" && s && state.verifyScripts.indexOf(s) === -1) state.verifyScripts.push(s);
|
|
2065
|
-
}
|
|
2066
|
-
if (block.extra.acIndex && typeof block.extra.acIndex === "object") state.acIndex = {
|
|
2067
|
-
...state.acIndex,
|
|
2068
|
-
...block.extra.acIndex
|
|
2069
|
-
};
|
|
2070
|
-
if (typeof block.extra.techStack === "string" && block.extra.techStack) state.product.techStack = block.extra.techStack;
|
|
2071
|
-
if (typeof block.extra.moduleContracts === "object" && block.extra.moduleContracts) state.modules = {
|
|
2072
|
-
...state.modules,
|
|
2073
|
-
...block.extra.moduleContracts
|
|
2074
|
-
};
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
saveState(projectKey, state);
|
|
2078
|
-
return state;
|
|
2079
|
-
}
|
|
2080
|
-
/** 按 run 更新 lastRun / lastRunFolder(finally 时调用)。 */
|
|
2081
|
-
function noteRun(projectKey, run) {
|
|
2082
|
-
const state = loadState(projectKey);
|
|
2083
|
-
if (run.runDocs) state.lastRunFolder = run.runDocs;
|
|
2084
|
-
state.lastRun = {
|
|
2085
|
-
runId: run.id || null,
|
|
2086
|
-
requirement: run.requirement ? String(run.requirement).slice(0, 200) : null,
|
|
2087
|
-
verdict: run.verdict || null,
|
|
2088
|
-
folder: run.runDocs || null,
|
|
2089
|
-
endedAt: run.endedAt ?? Date.now()
|
|
2090
|
-
};
|
|
2091
|
-
saveState(projectKey, state);
|
|
2092
|
-
}
|
|
2093
|
-
/** 按角色渲染 state slice(注入到子代理 prompt)。角色 → 只拿相关片段。 */
|
|
2094
|
-
function stateSliceFor(state, role) {
|
|
2095
|
-
const lines = [];
|
|
2096
|
-
if (state.__runCtx) {
|
|
2097
|
-
if (state.__runCtx.runDocs) lines.push(`【本次任务产物夹】${state.__runCtx.runDocs}/(host 已创建;本需求的 PRD/TECHNICAL/QA-REPORT/ACCEPTANCE 全部写这里,夹建后不可变、不归档不升版)`);
|
|
2098
|
-
if (state.__runCtx.sanity) lines.push(state.__runCtx.sanity);
|
|
2099
|
-
if (state.__runCtx.blueprint && (role === "arch" || role === "tech" || role === "dev")) lines.push(state.__runCtx.blueprint);
|
|
2100
|
-
}
|
|
2101
|
-
lines.push("【预编译产品状态(state.json · 权威记忆在 docs/teamflow/memory.md,本块已是够用的索引,勿再全量读历史文档)】");
|
|
2102
|
-
if (state.product.summary) lines.push(`- 产品概要:${state.product.summary}`);
|
|
2103
|
-
if (state.product.techStack && (role === "tech" || role === "dev" || role === "arch")) lines.push(`- 技术栈:${state.product.techStack}`);
|
|
2104
|
-
if (Object.keys(state.acIndex).length && (role === "pm" || role === "qa" || role === "acceptance" || role === "tech")) {
|
|
2105
|
-
const acs = Object.entries(state.acIndex).slice(0, 40);
|
|
2106
|
-
lines.push(`- AC 索引(${acs.length} 条):${acs.map(([k, v]) => `${k} ${v}`).join(";")}`);
|
|
2107
|
-
}
|
|
2108
|
-
if (Object.keys(state.modules).length && (role === "tech" || role === "dev" || role === "arch" || role === "qa")) lines.push(`- 模块(${Object.keys(state.modules).length}):${Object.entries(state.modules).map(([f, c]) => `${f}${c ? "→" + c : ""}`).join(",")}`);
|
|
2109
|
-
if (state.verifyScripts.length && (role === "qa" || role === "tech" || role === "dev")) lines.push(`- 验证脚本:${state.verifyScripts.join(",")}`);
|
|
2110
|
-
if (role === "pm" || role === "acceptance") {
|
|
2111
|
-
if (state.stages.prd) lines.push(`- PRD 摘要:${state.stages.prd}`);
|
|
2112
|
-
if (state.stages.tech) lines.push(`- 技术方案摘要:${state.stages.tech}`);
|
|
2113
|
-
if (state.stages.qa) lines.push(`- QA 摘要:${state.stages.qa}`);
|
|
2114
|
-
} else if (role === "dev" || role === "tech") {
|
|
2115
|
-
if (state.stages.tech) lines.push(`- 技术方案摘要:${state.stages.tech}`);
|
|
2116
|
-
if (state.stages.prd) lines.push(`- PRD 摘要:${state.stages.prd}`);
|
|
2117
|
-
} else if (role === "qa") {
|
|
2118
|
-
if (state.stages.qa) lines.push(`- 上轮 QA 摘要:${state.stages.qa}`);
|
|
2119
|
-
}
|
|
2120
|
-
if (state.lastRun) {
|
|
2121
|
-
const r = state.lastRun;
|
|
2122
|
-
lines.push(`- 上轮:${r.requirement ? r.requirement : ""}${r.verdict ? " → " + r.verdict : ""}${r.folder ? `(${r.folder})` : ""}`);
|
|
2123
|
-
}
|
|
2124
|
-
return lines.join("\n");
|
|
2125
|
-
}
|
|
2126
|
-
/** 让每个阶段产出末尾附带 state 块(将并入 stage output,由 host 提取)。 */
|
|
2127
|
-
const STATE_BLOCK_INSTRUCTION = `\n\n[STATE BLOCK · mandatory at the end] Append one section at the END of your answer (same output as the body; the host indexes it):
|
|
2128
|
-
<!-- state -->{"phase":"<stage-key>","summary":"<≤500 chars: this stage's conclusion, useful for the next run>","memory":["<memory points>"]}<!-- /state -->`;
|
|
2129
|
-
//#endregion
|
|
2130
2595
|
//#region host/prompts/index.ts
|
|
2131
2596
|
/**
|
|
2132
2597
|
* dsh-plugin-teamflow — Prompt 模板(阶段提示词 + 团队模板)。
|
|
@@ -2184,7 +2649,7 @@ const AGENTS_TEMPLATE = `# AGENTS.md — 团队协作守则与文档索引({{P
|
|
|
2184
2649
|
| 任务产物 | ${TF_DOCS}/<yyyyMMdd-rN-slug>/ | 每个需求一个自包含任务夹:PRD/设计/技术方案/QA 报告/验收报告(按日期倒序即迭代史) |
|
|
2185
2650
|
| 架构总览 | ${TF_DOCS}/architecture/ARCHITECTURE.md | 工程方案与脚手架说明(产品级长期文档) |
|
|
2186
2651
|
| 产品记忆 | ${TF_DOCS}/memory.md | 团队约定/技术栈/已知待办(低频更新) |
|
|
2187
|
-
| 运行日志 | logs/teamflow/<runId>/ | TeamFlow
|
|
2652
|
+
| 运行日志 | logs/teamflow/<runId>/ | TeamFlow 流水线各阶段命令日志(日常不读);**布局约定**(全部在该目录内,项目根不得出现 scripts/ probe/):regression-<phase>.log(套件输出,重跑追加)/ scripts/(一次性校验脚本)/ captures.json(命令载荷汇总)/ probe/(探针)——每用途一个文件,不新增同名变体 |
|
|
2188
2653
|
|
|
2189
2654
|
## 3. 团队角色与标准流程
|
|
2190
2655
|
|
|
@@ -2249,6 +2714,16 @@ function headTailClip(text, head, tail) {
|
|
|
2249
2714
|
if (s.length <= head + tail) return s;
|
|
2250
2715
|
return s.slice(0, head) + "\n...\n[CHANGED SECTION]\n" + s.slice(-tail);
|
|
2251
2716
|
}
|
|
2717
|
+
/**
|
|
2718
|
+
* 产物交付 · policy(2026-09-11):把任务夹产物交给官方的 `present` 工具,用户在该会话里得到
|
|
2719
|
+
* 「交付文件卡」(预览 / 默认程序打开 / 文件管理器定位)。
|
|
2720
|
+
* 诚实机制说明:present 是**模型工具**,host 不强制(没调用只是少一张卡,产物文件仍是唯一交付物);
|
|
2721
|
+
* 卡片渲染在**本子代理会话**的轮次尾部(宿主 ui-deliverables 挂 conversation.chat.turnTail),
|
|
2722
|
+
* 主会话不显示——所以它是增强项,工作台侧的「📄 产物」按钮才是主路径。
|
|
2723
|
+
*/
|
|
2724
|
+
const ARTIFACT_DELIVERY = (runDocs) => `[Artifact delivery · policy] After the deliverable file exists (and before your final reply, before the state block), call the \`present\` tool so the user gets a delivery card (preview / open in default app / reveal in file manager):
|
|
2725
|
+
present({ files: [{ path: "${runDocs}/<your deliverable>.md", description: "<one-line what it is>" }] })
|
|
2726
|
+
Present ONLY user-facing deliverables (≤4 files, e.g. PRD.md / TECHNICAL.md / QA-REPORT.md / ACCEPTANCE.md) — never scratch files, temp scripts or command logs. This is additive: the file stays the single source of truth, and a missing file is still a hard failure.`;
|
|
2252
2727
|
const TOKEN_HYGIENE = (runId) => `[TOKEN HYGIENE · policy] Context is expensive. Budget discipline below — host enforcement is warn + live reminder only (never interrupts), follow it as self-discipline:
|
|
2253
2728
|
- [File scope] Whole-file read is allowed ONLY for target files explicitly listed in the task spec. To understand other files' interfaces, use grep for keywords (do not read whole files). Never whole-file read source files outside the task scope.
|
|
2254
2729
|
- [No duplicate reads] Same file: read ≤1 times. To verify a change, grep the change point instead of re-reading the whole file.
|
|
@@ -2256,6 +2731,13 @@ const TOKEN_HYGIENE = (runId) => `[TOKEN HYGIENE · policy] Context is expensive
|
|
|
2256
2731
|
- [Batch fixes] When verification fails: read ALL failing cases at once → fix them ALL in one edit → run verification once more. Never "fix one → run → fix one → run". At most 3 fix-verify rounds; beyond that, output a diagnostic summary and stop.
|
|
2257
2732
|
- Never whole-file read a file over 200 lines (use grep + limit segments for the rest); whole-file read targets ≤2 files; everything else: grep + limited segments.
|
|
2258
2733
|
- Redirect command output to a file (under logs/teamflow/${runId || "<runId>"}/) and read the tail summary; never echo hundreds of lines inline.
|
|
2734
|
+
- [Log layout · policy] Reuse one file per purpose — never multiply variants of the same run. **Every path below is INSIDE logs/teamflow/${runId || "<runId>"}/ — never create scripts/ or probe/ at the project root** (they land in the delivery commit as pollution; the Doc boundary already forbids scattering there). Observed cost of ignoring this: one run left 51 full-suite dumps (78% of all log bytes) + 49 loose scripts, and a later run still leaked 6 root-level scratch files into its commit.
|
|
2735
|
+
- Full suite output → logs/teamflow/${runId || "<runId>"}/regression-<phase>.log, APPENDED on re-run with a "--- <timestamp> <task> ---" header (no -run2 / -nopipe / -shim variants of the same run).
|
|
2736
|
+
- Your own one-off checkers → logs/teamflow/${runId || "<runId>"}/scripts/ (name each for what it checks).
|
|
2737
|
+
- Captured command payloads → logs/teamflow/${runId || "<runId>"}/captures.json, not one file per invocation.
|
|
2738
|
+
- Probes / scratch fixtures → logs/teamflow/${runId || "<runId>"}/probe/.
|
|
2739
|
+
- Optional bash/zsh helpers → logs/teamflow/${runId || "<runId>"}/helpers/ (same rule: inside the run dir).
|
|
2740
|
+
- Anything else scattered in the run root is noise that the next agent — and the human auditing your [Verification evidence] — has to wade through.
|
|
2259
2741
|
- Keep reports/summaries tight (QA ≤150 lines, acceptance ≤80 lines, dev ≤40 lines); put details in files.
|
|
2260
2742
|
- AGENTS.md and the memory index are already injected above — no call needed to read them in full; grep keywords if you need a particular rule.
|
|
2261
2743
|
- The contract/AC for this iteration is in the context/handoff below or in this task folder's PRD: do NOT whole-file re-read PRD.md / DESIGN.md / TECHNICAL.md from the task folder; grep/read only the code you need.
|
|
@@ -2283,6 +2765,7 @@ ${requirement}
|
|
|
2283
2765
|
4. Output the full PRD (Markdown): background & goals, user stories (each with testable acceptance criteria), scope & non-goals, interaction flow summary, priority (P0/P1/P2), dependencies & risks, milestone suggestions. ACs must be testable/quantifiable; prefer precision & brevity. **No revision table, no version fields like「版本:vX.Y / 状态:进行中」** (the folder IS the archive; its name carries the identity).
|
|
2284
2766
|
5. [Memory write-back · convention changes ONLY] Update docs/teamflow/memory.md ONLY if this requirement introduces new team conventions / tech-stack decisions (replace the same-topic line, idempotent, no changelog-style appending); otherwise do not touch memory.
|
|
2285
2767
|
6. [Engineering actions carried verbatim] Engineering instructions in the raw requirement (create/switch branch, commit, tag...) MUST be preserved verbatim into the "工程约束" section of the PRD: specify the action, timing, and baseline (e.g. "branch from latest main, then implement"). If the workspace already has uncommitted changes, note how to handle them. Never silently drop or reword engineering instructions.
|
|
2768
|
+
${ARTIFACT_DELIVERY(RUN(state))}
|
|
2286
2769
|
7. [State] End with a state block (phase="prd"): summary covers the AC highlights + one-sentence product semantics; extra contains { "acIndex": {...}, "summary": "<product one-liner>", "techStack": "..." }.${STATE_BLOCK_INSTRUCTION}`;
|
|
2287
2770
|
const designPrompt = (prd, root, runId, state) => `You are a senior UI/UX designer. The current workspace IS the target project.
|
|
2288
2771
|
${productCtx(root)}${stateSliceFor(state, "design")}
|
|
@@ -2339,6 +2822,7 @@ ${JSON.stringify(tasks)}
|
|
|
2339
2822
|
- modules: per touched file — responsibility + deps + assembly order + **architecture rationale (why)**.
|
|
2340
2823
|
- tasks: parallelizable tasks split by file boundary (disjoint files → parallel); merge or sequence where dependencies/conflicts exist.
|
|
2341
2824
|
- If you find duplication or a module that should be extracted (e.g. unified storage wrapper), add it to modules with the why.
|
|
2825
|
+
${ARTIFACT_DELIVERY(RUN(state))}
|
|
2342
2826
|
6. [State] End with a state block (phase="tech"), extra = { "verifyScripts": [...], "modules": {"/file": "contract or one-liner"} }, summary = key architecture/contract decisions.${STATE_BLOCK_INSTRUCTION}`;
|
|
2343
2827
|
/**
|
|
2344
2828
|
* 架构师 prompt(M1「认知前置 + 架构落地」):全模式启用,轻量版(lite/tech/patch)只产架构蓝图 JSON,
|
|
@@ -2376,7 +2860,7 @@ ${clip(tech, 12e3)}` : ""}
|
|
|
2376
2860
|
4. Actually write/modify code (grep + segmented reads to locate; no repeated whole-file reads), then run relevant build/verification to ensure green.
|
|
2377
2861
|
5. [Engineering action execution] If task spec or PRD 工程约束 includes git actions (e.g. new branch): **execute the action BEFORE writing code** (e.g. git checkout -b <branch>); if the workspace carries unrelated uncommitted changes, do NOT commit/clean them — state the situation in the summary.
|
|
2378
2862
|
5b. [Git discipline · policy (ADR-2026-08-27, 统一收口提交)] Work ONLY on the current branch: **never** git checkout main / merge / rebase / delete-branch / commit — main-branch actions and the final commit are performed by the host after acceptance (one commit per run: code + task-folder docs together). Just write/modify files; leave everything uncommitted. If a task asks for "merge back to main" or "commit", treat it as "prepare the delivery" (files ready + summary of what was done), do NOT commit or merge.
|
|
2379
|
-
6. [Log discipline] Redirect command output to logs/teamflow/${runId || "<runId>"}
|
|
2863
|
+
6. [Log discipline] Redirect command output to logs/teamflow/${runId || "<runId>"}/ following the log layout above — INSIDE that dir: logs/teamflow/${runId || "<runId>"}/regression-dev.log (append on re-run), .../scripts/, .../captures.json. Never create scripts/ or probe/ at the project root (they would be committed as pollution).
|
|
2380
2864
|
7. Output an implementation summary (≤40 lines): changed files, key implementation points, leftovers. No big code pastes.
|
|
2381
2865
|
7b. [Verification evidence · policy] **Mandatory block at the end of the reply (before the state block)** — host stores it verbatim for audit, cross-checkable against your command output in logs/teamflow/${runId || "<runId>"}/; missing block = contract not honored (warn only, never interrupts):
|
|
2382
2866
|
[Verification evidence]
|
|
@@ -2411,12 +2895,13 @@ ${clip(devSummary, 15e3)}
|
|
|
2411
2895
|
- Always-available sandbox-legal paths: build/assembly checks, unit tests, DOM-level E2E (jsdom or equivalent), static audit, adversarial spot-checks.
|
|
2412
2896
|
2. [人工补测清单] Items that cannot be auto-verified (audio output / real-device: 100dvh dynamic toolbar, safe-area, multi-touch / FPS performance / screen-reader): do NOT fail them — instead list each in the report's「人工补测清单」section (acceptance criteria + method + tool), note「环境限制,非交付缺陷」, for human review.
|
|
2413
2897
|
3. Read AGENTS.md §4 engineering conventions (verify commands) and the code changes first, then actually run those sandbox-legal verifications.
|
|
2414
|
-
4. [Log discipline] Redirect command output to logs/teamflow/${runId || "<runId>"}/
|
|
2898
|
+
4. [Log discipline] Redirect command output to logs/teamflow/${runId || "<runId>"}/ following the log layout above — INSIDE that dir: .../regression-qa.log (append on re-run), .../scripts/, .../captures.json. Never create scripts/ or probe/ at the project root; no scatter at project root.
|
|
2415
2899
|
5. [Reply = brief summary only · HOST-ENFORCED] Output a short reply (≤12 lines, Chinese): verdict one-liner (whether acceptance-ready) + the QA report path docs/teamflow/.../QA-REPORT.md. **Do NOT repeat the report body in the reply** — the host imports QA-REPORT.md as the single source of truth; missing file = hard failure (needs-human, pipeline stops).
|
|
2416
2900
|
6. [Defect format · HOST-ENFORCED] Report found defects as the structured table below (for direct import by the defect tracker) — the table must be in QA-REPORT.md:
|
|
2417
2901
|
| 编号 | 严重级(P0/P1/P2/P3) | 功能模块 | 复现步骤 | 期望行为 | 实际行为 | 关联验收项 |
|
|
2418
2902
|
If no defects: explicitly output 「未发现缺陷」.
|
|
2419
2903
|
7. Chinese Markdown, concrete & executable; write the **complete** report to ${RUN(state)}/QA-REPORT.md (write once, tight body) — **this file IS the deliverable**: scope & environment, cases & results (pass/fail/blocked), 人工补测清单, defect table (if any), conclusion (whether acceptance-ready). [Boundary] only under ${TF_DOCS}/.
|
|
2904
|
+
${ARTIFACT_DELIVERY(RUN(state))}
|
|
2420
2905
|
8. [State] End with a state block (phase="qa"), summary = test conclusion / blocked items, extra = { "verifyScripts": [...] }.${STATE_BLOCK_INSTRUCTION}`;
|
|
2421
2906
|
/** QA 打回后的开发修复 prompt:确认缺陷是否属实 → 修复 → 复验交接(QA→dev 打回闭环用)。 */
|
|
2422
2907
|
const qaFixPrompt = (defects, qa, tech, prd, root, runId, state) => `You are a senior full-stack engineer. The QA report points out several defects — **confirm each one** and fix them, then hand back for QA re-verification.
|
|
@@ -2432,7 +2917,7 @@ ${tech && String(tech).trim() ? clip(tech, 12e3) : ""}
|
|
|
2432
2917
|
1. [Confirm first, then fix] For each defect, verify one by one whether it truly holds (read code / reproduce / compare actual vs expected):
|
|
2433
2918
|
— confirmed → fix it directly; QA false positive / contradicts reality → state evidence explicit in the summary (no fabricated changes, and no ignoring real defects either).
|
|
2434
2919
|
2. Touch ONLY defect-related files (grep to locate; no whole-file reads of irrelevant big files); respect existing architecture & code style.
|
|
2435
|
-
3. After fixing, run relevant verification to ensure green (regression floor: existing verify suites pass untouched); redirect output to logs/teamflow/${runId || "<runId>"}
|
|
2920
|
+
3. After fixing, run relevant verification to ensure green (regression floor: existing verify suites pass untouched); redirect output to logs/teamflow/${runId || "<runId>"}/ following the log layout above — INSIDE that dir: .../regression-devfix.log (append on re-run, no -run2 variants). Never create scripts/ or probe/ at the project root.
|
|
2436
2921
|
4. Output a fix summary (≤40 lines, Chinese): per defect —「truth judgment + fix」or「false-positive evidence」, changed files, leftovers. No big code pastes.
|
|
2437
2922
|
4b. [Verification evidence · policy] **Mandatory block at the end of the reply (before the state block)** — host stores it verbatim for audit, cross-checkable against your command output in logs/teamflow/${runId || "<runId>"}/; missing block = contract not honored (warn only, never interrupts):
|
|
2438
2923
|
[Verification evidence]
|
|
@@ -2459,6 +2944,7 @@ ${vision ? "[Visual re-check] If QA saved screenshots under the task folder, spo
|
|
|
2459
2944
|
3. [Not-applicable judgment] If the PRD/tech-change/confirm doc already states「需求与现状不符」, or the dev result is explicitly「无需改动」, the verdict must be **「📝 需求不适用」** with reasons — do NOT mark ✅ 通过 just for "no defects".
|
|
2460
2945
|
4. [Acceptance report · HOST-ENFORCED] Write the **complete** report to ${RUN(state)}/ACCEPTANCE.md (write once) — **this file IS the deliverable**: verdict line, per-criterion check table, opinions & leftovers. **The verdict line MUST be the LAST line of the file, verbatim one of: 验收结论:✅ 通过 / 验收结论:⚠️ 有条件通过 / 验收结论:❌ 不通过 / 验收结论:📝 需求不适用** — the host parses ONLY this line; missing it = contract violation → the run stops for human review; missing file = hard failure (needs-human, pipeline stops). [Memory write-back · convention changes ONLY] Update docs/teamflow/memory.md only if this requirement introduces new conventions/tech-stack decisions, or the 已知待办 list changes (same-topic line replace, idempotent, no changelog appending); otherwise don't touch memory. [Boundary] only under ${TF_DOCS}/; never modify AGENTS.md beyond the <!-- teamflow --> managed zone.
|
|
2461
2946
|
5. Chinese Markdown.
|
|
2947
|
+
${ARTIFACT_DELIVERY(RUN(state))}
|
|
2462
2948
|
6. [State] End with a state block (phase="acceptance"), summary = acceptance conclusion, verdict = "accepted/rework/reject/needs-human", extra.done = confirmation of this delivery.${STATE_BLOCK_INSTRUCTION}`;
|
|
2463
2949
|
/** 需求分诊模型 prompt(模型驱动 triage;供 core/triage.runTriage 使用)。 */
|
|
2464
2950
|
const TRIAGE_PROMPT = (requirement, opts, pre, retryHint) => `You are a senior research-dev triage analyst. Do ONE thing: analyze which pipeline mode this dev requirement fits, then give the conclusion. No code, no scope speculation.
|
|
@@ -2855,6 +3341,42 @@ function buildResumeProducts(journal) {
|
|
|
2855
3341
|
}
|
|
2856
3342
|
return products;
|
|
2857
3343
|
}
|
|
3344
|
+
/**
|
|
3345
|
+
* 让工作区的 .gitignore 忽略插件自有日志目录(幂等;返回是否真的写了)。
|
|
3346
|
+
*
|
|
3347
|
+
* 为什么需要(实锤 assetd `tf-mtwvwpxa-p3vw08`):插件强制子代理把命令日志与临时验证脚本写进
|
|
3348
|
+
* `logs/teamflow/`(Log discipline / TOKEN_HYGIENE),而收口提交用裸 `add -A`——目标仓库没配
|
|
3349
|
+
* .gitignore 时,一次提交 227 个文件里 208 个(92%)是这批日志(100 log / 52 json / 44 临时 .mjs),
|
|
3350
|
+
* 真交付只有 19 个。注意当时子代理的交付报告写的是「docs/ and logs/ remain untracked as required」
|
|
3351
|
+
* ——交付前完全属实,是 host 在最后一刻扫进去的:**契约在 host 这一侧破的**。
|
|
3352
|
+
*
|
|
3353
|
+
* 两道防线缺一不可:
|
|
3354
|
+
* ① 本函数写 .gitignore → IDE / `git status` / 用户自己的 CI 也不再看到这批文件(卫生);
|
|
3355
|
+
* ② `tfAddArgs()` 的 pathspec 强制排除 → 目标仓库只读、非 git、或用户把规则删回去时仍然兜得住(保证)。
|
|
3356
|
+
* 只在**即将提交**时写入:跑失败/取消的 run 不留下一份未提交的 .gitignore 改动。
|
|
3357
|
+
*/
|
|
3358
|
+
function ensureLogGitignore(cwd, journal) {
|
|
3359
|
+
if (!cwd) return false;
|
|
3360
|
+
try {
|
|
3361
|
+
const file = `${cwd}/.gitignore`;
|
|
3362
|
+
const merged = mergeGitignore(existsSync(file) ? readFileSync(file, "utf8") : null, [`${TF_LOG_DIR}/`]);
|
|
3363
|
+
if (!merged.changed) return false;
|
|
3364
|
+
writeFileSync(file, merged.text, "utf8");
|
|
3365
|
+
journal.logs.push({
|
|
3366
|
+
t: Date.now(),
|
|
3367
|
+
level: "info",
|
|
3368
|
+
message: `工作区 .gitignore 已补忽略 ${TF_LOG_DIR}/(插件自有运行日志,非交付物;随本次提交可见)`
|
|
3369
|
+
});
|
|
3370
|
+
return true;
|
|
3371
|
+
} catch (e) {
|
|
3372
|
+
journal.logs.push({
|
|
3373
|
+
t: Date.now(),
|
|
3374
|
+
level: "warn",
|
|
3375
|
+
message: `补写 .gitignore 失败(不影响提交面排除):${String(e && e.message || e)}`
|
|
3376
|
+
});
|
|
3377
|
+
return false;
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
2858
3380
|
/** 任务夹产物读取(单轨契约:文件即产物——QA/验收 host 只读文件,回复仅摘要)。
|
|
2859
3381
|
* 缺失/空/读取异常返回 null(调用方决定硬失败或 journal 兜底)。 */
|
|
2860
3382
|
function artifactText(journal, fileName) {
|
|
@@ -3001,10 +3523,10 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3001
3523
|
const stageFailError = (label, r) => {
|
|
3002
3524
|
const last = [...journal.stages || []].reverse().find((s) => phaseKeyOf(s.phase) === label);
|
|
3003
3525
|
const attempts = r && r.attempts ? r.attempts : 2;
|
|
3004
|
-
const burnt = Math.round((r && r.
|
|
3005
|
-
const breaker = (r && r.
|
|
3526
|
+
const burnt = Math.round((r && r.freshTokens || 0) / 1e3);
|
|
3527
|
+
const breaker = (r && r.freshTokens || 0) >= 2e5 ? ",超出新增 token 预算熔断" : "";
|
|
3006
3528
|
const detail = last ? `末次 ${last.outcome || "unknown"}${last.summary ? `(${last.summary})` : ""}` : "无阶段记录";
|
|
3007
|
-
return /* @__PURE__ */ new Error(`${PHASE_KEY_OF[label] || label} 阶段失败:${attempts} 次尝试未交付,${detail}
|
|
3529
|
+
return /* @__PURE__ */ new Error(`${PHASE_KEY_OF[label] || label} 阶段失败:${attempts} 次尝试未交付,${detail},累计新增消耗 ${burnt}k token(不含缓存命中)${breaker},需人工介入`);
|
|
3008
3530
|
};
|
|
3009
3531
|
try {
|
|
3010
3532
|
if (resume) journal.logs.push({
|
|
@@ -3116,7 +3638,8 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3116
3638
|
}
|
|
3117
3639
|
else if (!resume && journal.workspacePath && options.preAction === "commit") try {
|
|
3118
3640
|
const msg = typeof options.commitMessage === "string" && options.commitMessage.trim() ? options.commitMessage.trim() : `chore(teamflow): 流水线启动前提交现有改动(${journal.id})`;
|
|
3119
|
-
|
|
3641
|
+
ensureLogGitignore(journal.workspacePath, journal);
|
|
3642
|
+
const add = gitCmd(journal.workspacePath, tfAddArgs());
|
|
3120
3643
|
const cm = gitCmd(journal.workspacePath, [
|
|
3121
3644
|
"commit",
|
|
3122
3645
|
"-m",
|
|
@@ -3187,6 +3710,15 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3187
3710
|
if (stage) stage.verifyEvidence = ev;
|
|
3188
3711
|
} catch (e) {}
|
|
3189
3712
|
};
|
|
3713
|
+
/**
|
|
3714
|
+
* 失败尝试的真实产出取用(2026-09-11 修「文本凭空丢失」):
|
|
3715
|
+
* runAgent 失败路径已把产出截断落盘到 `stage.output`(供重试诊断/详情浮层),
|
|
3716
|
+
* 但 withRetry 的 `text` 为 null → 证据存证 / state 回写 / 子卡产物全部拿不到文本,
|
|
3717
|
+
* 还会派生一条**误导性 warn**「回复缺少 [Verification evidence] 块」(实锤 assetd
|
|
3718
|
+
* tf-mtwvwpxa-p3vw08 的 T5:块明明在,只因该轮被判失败就报「契约未兑现」)。
|
|
3719
|
+
* 展示/存证/诊断一律用真实文本,成功与否仍只由 `text` 决定。
|
|
3720
|
+
*/
|
|
3721
|
+
const stageTextOf = (r) => r.text || r.stage && r.stage.output || null;
|
|
3190
3722
|
let prd = null;
|
|
3191
3723
|
if (resumed("prd")) {
|
|
3192
3724
|
prd = resume.products.prd;
|
|
@@ -3208,7 +3740,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3208
3740
|
label: "产品经理 · 梳理 PRD",
|
|
3209
3741
|
fn: prdPrompt
|
|
3210
3742
|
};
|
|
3211
|
-
const prdR = await withRetry(journal, parent, pForm.label, "prd", pForm.fn(requirement, root, journal.id, state), signal);
|
|
3743
|
+
const prdR = await withRetry(journal, parent, pForm.label, "prd", pForm.fn(requirement, root, journal.id, state), signal, void 0, options.mode === "patch" ? "low" : null);
|
|
3212
3744
|
if (!prdR.text) throw stageFailError("prd", prdR);
|
|
3213
3745
|
prd = prdR.text;
|
|
3214
3746
|
timeline.prd = prd;
|
|
@@ -3249,7 +3781,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3249
3781
|
level: "phase",
|
|
3250
3782
|
message: "进入阶段:架构规划"
|
|
3251
3783
|
});
|
|
3252
|
-
const scR = await withRetry(journal, parent, "架构师 · 脚手架规划与落地", "scaffold", scaffoldPrompt(requirement, design, root, journal.id, state), signal);
|
|
3784
|
+
const scR = await withRetry(journal, parent, "架构师 · 脚手架规划与落地", "scaffold", scaffoldPrompt(requirement, design, root, journal.id, state), signal, void 0, "low");
|
|
3253
3785
|
if (!scR.text) throw stageFailError("scaffold", scR);
|
|
3254
3786
|
scaffold = scR.text;
|
|
3255
3787
|
timeline.scaffold = scaffold;
|
|
@@ -3326,12 +3858,13 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3326
3858
|
const prevStage = [...journal.stages].reverse().find((s) => phaseKeyOf(s.phase) === "dev" && s.status !== "done" && (s.taskKey && s.taskKey === String(task.title || "") || !s.taskKey && (s.label || "").includes(String(task.title || ""))));
|
|
3327
3859
|
const resumePrompt = devPrompt(task, tech, prd, root, journal.id, state) + (prevStage ? buildRetryDiagnostic(2, prevStage) : "");
|
|
3328
3860
|
const devR = await withRetry(journal, parent, `开发 · ${task.title}(补跑)`, "dev", resumePrompt, signal, task.title);
|
|
3329
|
-
|
|
3861
|
+
const rerunText = stageTextOf(devR);
|
|
3862
|
+
noteVerifyEvidence(devR.stage, rerunText);
|
|
3330
3863
|
const ok = !!devR.text;
|
|
3331
3864
|
return {
|
|
3332
3865
|
title: task.title,
|
|
3333
3866
|
failed: !ok,
|
|
3334
|
-
output:
|
|
3867
|
+
output: rerunText || "开发失败(Agent 未产出结果)"
|
|
3335
3868
|
};
|
|
3336
3869
|
});
|
|
3337
3870
|
for (const t of rerun) {
|
|
@@ -3382,16 +3915,17 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3382
3915
|
}
|
|
3383
3916
|
}
|
|
3384
3917
|
const devR = await withRetry(journal, parent, `开发 · ${task.title}`, "dev", devPrompt(task, tech, prd, root, journal.id, state), signal, task.title);
|
|
3385
|
-
|
|
3918
|
+
const devText = stageTextOf(devR);
|
|
3919
|
+
noteVerifyEvidence(devR.stage, devText);
|
|
3386
3920
|
const ok = !!devR.text;
|
|
3387
3921
|
if (sub) {
|
|
3388
|
-
completeSubtask(journal, sub.id, !ok,
|
|
3922
|
+
completeSubtask(journal, sub.id, !ok, devText ? snippet(devText, 1e3) : null, null);
|
|
3389
3923
|
if (devR.stage) noteSubtaskUsage(journal, sub.id, devR.stage);
|
|
3390
3924
|
}
|
|
3391
3925
|
return {
|
|
3392
3926
|
title: task.title,
|
|
3393
3927
|
failed: !ok,
|
|
3394
|
-
output:
|
|
3928
|
+
output: devText || "开发失败(Agent 未产出结果)"
|
|
3395
3929
|
};
|
|
3396
3930
|
});
|
|
3397
3931
|
timeline.dev = devResults;
|
|
@@ -3472,7 +4006,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3472
4006
|
advanceTask(journal, "needs-human", null, "QA-REPORT.md 未落盘(单轨契约未兑现)", { by: "qa" });
|
|
3473
4007
|
throw stageFailError("qa", {
|
|
3474
4008
|
attempts: qaR.attempts,
|
|
3475
|
-
|
|
4009
|
+
freshTokens: qaR.freshTokens
|
|
3476
4010
|
});
|
|
3477
4011
|
}
|
|
3478
4012
|
timeline.qa = qa;
|
|
@@ -3514,7 +4048,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3514
4048
|
});
|
|
3515
4049
|
advanceTask(journal, "rework", snippet(qa, 3e3), `QA 打回开发修复(第 ${round}/3 轮)`, { by: "qa" });
|
|
3516
4050
|
const fixR = await withRetry(journal, parent, `开发 · QA 缺陷修复(第 ${round} 轮)`, "dev", qaFixPrompt(blocking, qa, tech, prd, root, journal.id, state), signal, null);
|
|
3517
|
-
noteVerifyEvidence(fixR.stage, fixR
|
|
4051
|
+
noteVerifyEvidence(fixR.stage, stageTextOf(fixR));
|
|
3518
4052
|
if (!fixR.text) {
|
|
3519
4053
|
advanceTask(journal, "needs-human", null, "QA 打回后开发修复失败", { by: "qa" });
|
|
3520
4054
|
throw stageFailError("开发(QA 打回修复)", fixR);
|
|
@@ -3580,7 +4114,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3580
4114
|
advanceTask(journal, "needs-human", null, "ACCEPTANCE.md 未落盘(单轨契约未兑现)", { by: "pm" });
|
|
3581
4115
|
throw stageFailError("acceptance", {
|
|
3582
4116
|
attempts: accR.attempts,
|
|
3583
|
-
|
|
4117
|
+
freshTokens: accR.freshTokens
|
|
3584
4118
|
});
|
|
3585
4119
|
}
|
|
3586
4120
|
timeline.acceptance = acceptance;
|
|
@@ -3664,7 +4198,8 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3664
4198
|
activeProducts.delete(scopeKey);
|
|
3665
4199
|
if (journal.workspacePath && journal.status === "completed" && !journal.humanIntervention) try {
|
|
3666
4200
|
const reqHead = String(journal.requirement || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
3667
|
-
|
|
4201
|
+
ensureLogGitignore(journal.workspacePath, journal);
|
|
4202
|
+
const add = gitCmd(journal.workspacePath, tfAddArgs());
|
|
3668
4203
|
if ((add === null ? null : gitCmd(journal.workspacePath, [
|
|
3669
4204
|
"commit",
|
|
3670
4205
|
"-m",
|
|
@@ -3909,18 +4444,7 @@ function resumeRun(runId, sessionId) {
|
|
|
3909
4444
|
* 运行环境:宿主组合(web profile)的真实 Node 进程。
|
|
3910
4445
|
*/
|
|
3911
4446
|
/** 阶段顺序/key 映射见 constants.ts(PHASE_ORDER/PHASE_KEY_OF/PHASE_KEY_BY_NAME)。 */
|
|
3912
|
-
/**
|
|
3913
|
-
function runsFor(ws) {
|
|
3914
|
-
const arr = [];
|
|
3915
|
-
for (const j of runs.values()) {
|
|
3916
|
-
if (ws) {
|
|
3917
|
-
if ((j.workspace || (ws === "default" ? "default" : null)) !== ws) continue;
|
|
3918
|
-
}
|
|
3919
|
-
arr.push(j);
|
|
3920
|
-
}
|
|
3921
|
-
arr.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
|
|
3922
|
-
return arr;
|
|
3923
|
-
}
|
|
4447
|
+
/** 按工作区作用域过滤运行见 core/products.ts(runsFor;全局面板与远程面共用)。 */
|
|
3924
4448
|
/** 由 sessionId 推导会话所属 workspace(项目)作用域。 */
|
|
3925
4449
|
function sessionScope(sessionId) {
|
|
3926
4450
|
const sid = typeof sessionId === "string" && sessionId ? sessionId : null;
|
|
@@ -3933,6 +4457,8 @@ function snapshotOf(j) {
|
|
|
3933
4457
|
status: j.status,
|
|
3934
4458
|
startedAt: j.startedAt,
|
|
3935
4459
|
endedAt: j.endedAt,
|
|
4460
|
+
address: runAddress(j.workspace || "default", j.id),
|
|
4461
|
+
ownerSession: j.ownerSession || null,
|
|
3936
4462
|
requirement: clip(j.requirement, 2e3),
|
|
3937
4463
|
options: sanitizeSnapOptions(j.options),
|
|
3938
4464
|
agentsStarted: j.agentsStarted,
|
|
@@ -4667,14 +5193,16 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4667
5193
|
static inject = [
|
|
4668
5194
|
"agents",
|
|
4669
5195
|
"subagents",
|
|
4670
|
-
"tokenMeter",
|
|
4671
5196
|
"typert",
|
|
4672
5197
|
"tools",
|
|
4673
5198
|
"llm"
|
|
4674
5199
|
];
|
|
4675
5200
|
constructor(ctx) {
|
|
4676
5201
|
super(ctx, "teamflow");
|
|
4677
|
-
setRuntime(ctx.get("agents"), ctx.get("subagents"), ctx.get("
|
|
5202
|
+
setRuntime(ctx.get("agents"), ctx.get("subagents"), ctx.get("workspaceRegistry"), ctx.get("agentDefaultModel"), ctx.get("llm"));
|
|
5203
|
+
ctx.inject(["sessionProjections"], (projectionCtx) => {
|
|
5204
|
+
setSessionProjections(projectionCtx.get("sessionProjections"));
|
|
5205
|
+
});
|
|
4678
5206
|
loadActiveTeams();
|
|
4679
5207
|
let interruptedCount = 0;
|
|
4680
5208
|
try {
|
|
@@ -4706,28 +5234,21 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4706
5234
|
list(sessionId) {
|
|
4707
5235
|
const sc = sessionScope(sessionId);
|
|
4708
5236
|
return {
|
|
4709
|
-
runs: runsFor(sc.projectKey).slice(0, 30).map(
|
|
4710
|
-
id: j.id,
|
|
4711
|
-
status: j.status,
|
|
4712
|
-
startedAt: j.startedAt,
|
|
4713
|
-
endedAt: j.endedAt,
|
|
4714
|
-
agentsStarted: j.agentsStarted,
|
|
4715
|
-
stageCount: j.stages.length,
|
|
4716
|
-
incompleteStages: (j.stages || []).some((x) => x.status !== "done"),
|
|
4717
|
-
requirement: clip(j.requirement, 60)
|
|
4718
|
-
})),
|
|
5237
|
+
runs: runsFor(sc.projectKey).slice(0, 30).map(runBrief),
|
|
4719
5238
|
workspace: sc
|
|
4720
5239
|
};
|
|
4721
5240
|
}
|
|
4722
|
-
|
|
4723
|
-
|
|
5241
|
+
/** run 详情(快照)。productOverride:全局面板按产品线 key 寻址时传入(跳过会话推导)。 */
|
|
5242
|
+
snapshot(runId, sessionId, productOverride) {
|
|
5243
|
+
const key = productOverride ? productKeyOf(productOverride) : sessionScope(sessionId).projectKey;
|
|
5244
|
+
if (!key) return null;
|
|
4724
5245
|
if (runId && typeof runId === "string") {
|
|
4725
5246
|
const j = runs.get(runId);
|
|
4726
5247
|
if (!j) return null;
|
|
4727
|
-
if (j
|
|
5248
|
+
if (!runVisibleIn(j, key)) return null;
|
|
4728
5249
|
return snapshotOf(j);
|
|
4729
5250
|
}
|
|
4730
|
-
const latest = runsFor(
|
|
5251
|
+
const latest = runsFor(key)[0];
|
|
4731
5252
|
if (!latest) return null;
|
|
4732
5253
|
const j = runs.get(latest.id);
|
|
4733
5254
|
return j ? snapshotOf(j) : null;
|
|
@@ -4735,12 +5256,13 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4735
5256
|
/** 阶段详情:卡片点击查看 —— 状态/耗时/官方 usage + 产物全文(超 24k 截断)。
|
|
4736
5257
|
* 2026-09-06 状态机化:返回同任务全部尝试(attempts 聚合——按 stage.taskKey(旧数据 label 兜底),
|
|
4737
5258
|
* 按 seq 排序)——client 弹窗单次渲染现状、多次渲染时间线。 */
|
|
4738
|
-
stageDetail(runId, seq, sessionId) {
|
|
5259
|
+
stageDetail(runId, seq, sessionId, productOverride) {
|
|
4739
5260
|
if (typeof runId !== "string" || !runId || seq === void 0 || seq === null) return null;
|
|
4740
|
-
const
|
|
5261
|
+
const key = productOverride ? productKeyOf(productOverride) : sessionScope(sessionId).projectKey;
|
|
5262
|
+
if (!key) return null;
|
|
4741
5263
|
const j = runs.get(runId);
|
|
4742
5264
|
if (!j) return null;
|
|
4743
|
-
if (j
|
|
5265
|
+
if (!runVisibleIn(j, key)) return null;
|
|
4744
5266
|
const s = (j.stages || []).find((st) => Number(st.seq) === Number(seq));
|
|
4745
5267
|
if (!s) return null;
|
|
4746
5268
|
const taskKeyOf = (x) => String(x.taskKey || String(x.label || "").replace(/^开发 · /, "").replace(/((?:第 \d+ 次重试|补跑))$/, "").trim());
|
|
@@ -4776,31 +5298,49 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4776
5298
|
};
|
|
4777
5299
|
}
|
|
4778
5300
|
/** Backlog 条目详情:卡片点击查看 —— 完整字段 + 流转时间线 + 关联(子卡/缺陷)+ 任务夹路径。 */
|
|
4779
|
-
itemDetail(kind, id, sessionId) {
|
|
5301
|
+
itemDetail(kind, id, sessionId, productOverride) {
|
|
4780
5302
|
const k = typeof kind === "string" && [
|
|
4781
5303
|
"req",
|
|
4782
5304
|
"task",
|
|
4783
5305
|
"bug"
|
|
4784
5306
|
].indexOf(kind) !== -1 ? kind : null;
|
|
4785
5307
|
if (!k || typeof id !== "string" || !id) return null;
|
|
4786
|
-
const
|
|
4787
|
-
|
|
5308
|
+
const key = productOverride ? productKeyOf(productOverride) : sessionScope(sessionId).projectKey;
|
|
5309
|
+
if (!key) return null;
|
|
5310
|
+
const store = storeFor(key);
|
|
4788
5311
|
const item = store.find(k, id);
|
|
4789
5312
|
if (!item) return null;
|
|
4790
5313
|
const reqId = k === "req" ? item.id : item.reqId || null;
|
|
4791
5314
|
let runDocs = null;
|
|
5315
|
+
let runDocsRoot = null;
|
|
4792
5316
|
let runInfo = null;
|
|
4793
|
-
for (const j of runsFor(
|
|
5317
|
+
for (const j of runsFor(key)) {
|
|
4794
5318
|
if (j.reqId !== reqId) continue;
|
|
4795
|
-
if (j.runDocs && !runDocs)
|
|
5319
|
+
if (j.runDocs && !runDocs) {
|
|
5320
|
+
runDocs = j.runDocs;
|
|
5321
|
+
runDocsRoot = j.workspacePath || null;
|
|
5322
|
+
}
|
|
4796
5323
|
if (!runInfo) runInfo = {
|
|
4797
5324
|
runId: j.id,
|
|
4798
5325
|
status: j.status,
|
|
4799
5326
|
requirement: String(j.requirement || ""),
|
|
4800
5327
|
startedAt: j.startedAt || null,
|
|
4801
|
-
endedAt: j.endedAt || null
|
|
5328
|
+
endedAt: j.endedAt || null,
|
|
5329
|
+
ownerSession: j.ownerSession || null
|
|
4802
5330
|
};
|
|
4803
5331
|
}
|
|
5332
|
+
const artifactSession = runInfo && runInfo.ownerSession || sessionId;
|
|
5333
|
+
const runArtifacts = [];
|
|
5334
|
+
if (runDocs && runDocsRoot && typeof artifactSession === "string" && artifactSession) try {
|
|
5335
|
+
const present = new Set(readdirSync(join(runDocsRoot, runDocs)));
|
|
5336
|
+
for (const name of TEAMFLOW_ARTIFACT_ORDER) {
|
|
5337
|
+
if (!present.has(name)) continue;
|
|
5338
|
+
runArtifacts.push({
|
|
5339
|
+
name,
|
|
5340
|
+
address: fileAddressFor(artifactSession, void 0, `${runDocs}/${name}`)
|
|
5341
|
+
});
|
|
5342
|
+
}
|
|
5343
|
+
} catch (e) {}
|
|
4804
5344
|
const byRole = item.byRole || null;
|
|
4805
5345
|
let subtasks = [];
|
|
4806
5346
|
let bugs = [];
|
|
@@ -4870,11 +5410,47 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4870
5410
|
byRole: k === "task" ? byRole : null,
|
|
4871
5411
|
reqId: reqId || null,
|
|
4872
5412
|
runDocs,
|
|
5413
|
+
artifacts: runArtifacts,
|
|
4873
5414
|
runInfo,
|
|
4874
5415
|
subtasks,
|
|
4875
5416
|
bugs
|
|
4876
5417
|
};
|
|
4877
5418
|
}
|
|
5419
|
+
/** 产品线清单 + 当前会话所属产品线(client 用它做默认选中)。 */
|
|
5420
|
+
products(sessionId) {
|
|
5421
|
+
return {
|
|
5422
|
+
current: sessionScope(sessionId).projectKey || null,
|
|
5423
|
+
products: listProducts()
|
|
5424
|
+
};
|
|
5425
|
+
}
|
|
5426
|
+
/** 产品线视图:元信息 + backlog + run 列表(全局面板一次取全,少往返)。 */
|
|
5427
|
+
productView(product) {
|
|
5428
|
+
const key = productKeyOf(product);
|
|
5429
|
+
if (!key) return null;
|
|
5430
|
+
return {
|
|
5431
|
+
product: productMetaOf(key),
|
|
5432
|
+
backlog: this.backlog(null, key),
|
|
5433
|
+
runs: runsFor(key).slice(0, 50).map(runBrief)
|
|
5434
|
+
};
|
|
5435
|
+
}
|
|
5436
|
+
/** 产品线级 run 详情(右栏 tab 与面板内联共用同一形状)。 */
|
|
5437
|
+
productRunDetail(product, runId) {
|
|
5438
|
+
const key = productKeyOf(product);
|
|
5439
|
+
if (!key) return null;
|
|
5440
|
+
return this.snapshot(runId, null, key);
|
|
5441
|
+
}
|
|
5442
|
+
/** 产品线级阶段详情(同 stageDetail 形状:attempts 聚合 + 验证证据)。 */
|
|
5443
|
+
productStageDetail(product, runId, seq) {
|
|
5444
|
+
const key = productKeyOf(product);
|
|
5445
|
+
if (!key) return null;
|
|
5446
|
+
return this.stageDetail(runId, seq, null, key);
|
|
5447
|
+
}
|
|
5448
|
+
/** 产品线级 backlog 条目详情(sessionId 可选:仅用于把任务夹产物地址绑到某个会话)。 */
|
|
5449
|
+
productItemDetail(product, kind, id, sessionId) {
|
|
5450
|
+
const key = productKeyOf(product);
|
|
5451
|
+
if (!key) return null;
|
|
5452
|
+
return this.itemDetail(kind, id, sessionId, key);
|
|
5453
|
+
}
|
|
4878
5454
|
start(sessionId, requirement, options) {
|
|
4879
5455
|
const sid = typeof sessionId === "string" ? sessionId : null;
|
|
4880
5456
|
const req = typeof requirement === "string" && requirement.trim() ? requirement.trim() : null;
|
|
@@ -4911,11 +5487,12 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4911
5487
|
};
|
|
4912
5488
|
return { ok: cancelRun(id) };
|
|
4913
5489
|
}
|
|
4914
|
-
/** 工作区级 backlog 视图(自动按当前会话 workspace 隔离)。 */
|
|
4915
|
-
backlog(sessionId) {
|
|
4916
|
-
const
|
|
4917
|
-
|
|
4918
|
-
const
|
|
5490
|
+
/** 工作区级 backlog 视图(自动按当前会话 workspace 隔离)。productOverride:全局面板按产品线 key 寻址。 */
|
|
5491
|
+
backlog(sessionId, productOverride) {
|
|
5492
|
+
const key = productOverride ? productKeyOf(productOverride) : sessionScope(sessionId).projectKey;
|
|
5493
|
+
if (!key) return null;
|
|
5494
|
+
const sum = backlogSummary(key);
|
|
5495
|
+
const js = runsFor(key);
|
|
4919
5496
|
const runOf = (reqId) => {
|
|
4920
5497
|
if (!reqId) return null;
|
|
4921
5498
|
let last = null;
|