dsh-plugin-teamflow 0.1.7 → 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 +32 -0
- package/README.en.md +6 -5
- package/README.md +25 -16
- package/lib/client.js +1702 -143
- package/lib/descriptors.mjs +54 -0
- package/lib/host.mjs +612 -252
- package/package.json +19 -4
package/lib/host.mjs
CHANGED
|
@@ -8,8 +8,20 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
|
|
|
8
8
|
import { fileAddressFor } from "@deepseek-ai/dsh-util-workspace-path";
|
|
9
9
|
import { execFileSync } from "node:child_process";
|
|
10
10
|
//#region host/constants.ts
|
|
11
|
-
/**
|
|
12
|
-
|
|
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;
|
|
13
25
|
/** 任务夹产物展示顺序(ADR-0008):工作台只列其中**真实存在**的文件,按此顺序出「一键右侧栏预览」按钮。 */
|
|
14
26
|
const TEAMFLOW_ARTIFACT_ORDER = [
|
|
15
27
|
"PRD.md",
|
|
@@ -25,8 +37,23 @@ const GUARD_POLL_MS = 15e3;
|
|
|
25
37
|
const GUARD_SILENCE_MS = 6e5;
|
|
26
38
|
/** 空转判定:会话仍在产出事件但连续这么久没有任何工具调用(纯推理打转/改写式循环)→ stalled。要求已见过至少一次工具调用。 */
|
|
27
39
|
const GUARD_NO_TOOL_MS = 9e5;
|
|
28
|
-
/**
|
|
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
|
+
*/
|
|
29
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;
|
|
30
57
|
/** 各阶段最小产出长度(防"假完成":空话/一句话冒充交付)。 */
|
|
31
58
|
const STAGE_MIN_LENGTH = {
|
|
32
59
|
prd: 400,
|
|
@@ -302,6 +329,41 @@ const SAFE_SIGNAL = {
|
|
|
302
329
|
function normalizeSignal(s) {
|
|
303
330
|
return s && typeof s === "object" && typeof s.addEventListener === "function" && typeof s.aborted === "boolean" && typeof s.throwIfAborted === "function" ? s : SAFE_SIGNAL;
|
|
304
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
|
+
}
|
|
305
367
|
/** 分支 slug 派生(ADR-2026-08-27):branchName > triageSlug > 需求中的英文标识词 > reqId 数字 > 'feature'。
|
|
306
368
|
* 实锤 feat/feature:lite 显式时 triage 不跑(无 slug)+ 分支检查早于 reqId 生成 → fallback 'feature'。 */
|
|
307
369
|
function deriveBranchSlug(requirement, reqId, triageSlug, branchName) {
|
|
@@ -330,12 +392,66 @@ function deriveBranchSlug(requirement, reqId, triageSlug, branchName) {
|
|
|
330
392
|
if (num) return `r${num[0]}`;
|
|
331
393
|
return "feature";
|
|
332
394
|
}
|
|
333
|
-
/**
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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);
|
|
337
417
|
const min = STAGE_MIN_LENGTH[phase] ?? 100;
|
|
338
|
-
|
|
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
|
+
};
|
|
339
455
|
}
|
|
340
456
|
/** 不可重试的失败原因(上下文耗尽/超长/provider 客户端拒绝等——重试同一 prompt 大概率复现)。
|
|
341
457
|
* 实锤 tf-mtcnejqj:opencode-go 400 invalid_request_error(tool 消息序列非法)被当作可重试 → 烧 1.98M 熔断。 */
|
|
@@ -1102,6 +1218,288 @@ function hasOpenBlockingBugs(journal) {
|
|
|
1102
1218
|
return false;
|
|
1103
1219
|
}
|
|
1104
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
|
+
}
|
|
1105
1503
|
/** 默认团队配置文件内容。 */
|
|
1106
1504
|
const DEFAULT_TEAMS_FILE = {
|
|
1107
1505
|
version: 1,
|
|
@@ -1227,6 +1625,32 @@ function gitCmd(cwd, args, timeoutMs = 8e3) {
|
|
|
1227
1625
|
}
|
|
1228
1626
|
}
|
|
1229
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
|
+
/**
|
|
1230
1654
|
* 跑一次状态核对。
|
|
1231
1655
|
* @param path - 工作区绝对路径(workspaceScopeOf(agent).path)。
|
|
1232
1656
|
*/
|
|
@@ -1416,11 +1840,24 @@ function scannedUsageOf(run) {
|
|
|
1416
1840
|
buckets.calls = seen.size || 1;
|
|
1417
1841
|
return buckets;
|
|
1418
1842
|
}
|
|
1419
|
-
/** 官方口径总消耗(billed input + output,含 cacheRead/cacheWrite
|
|
1843
|
+
/** 官方口径总消耗(billed input + output,含 cacheRead/cacheWrite)——**汇报/展示**口径。 */
|
|
1420
1844
|
function totalTokensOf(usage) {
|
|
1421
1845
|
if (!usage) return 0;
|
|
1422
1846
|
return (usage.input || 0) + (usage.cacheRead || 0) + (usage.cacheWrite || 0) + (usage.output || 0);
|
|
1423
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
|
+
}
|
|
1424
1861
|
//#endregion
|
|
1425
1862
|
//#region host/core/guard.ts
|
|
1426
1863
|
/**
|
|
@@ -1949,15 +2386,21 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
1949
2386
|
const stop = result && result.stopReason;
|
|
1950
2387
|
const text = extractText(result && result.output);
|
|
1951
2388
|
stageText = text;
|
|
2389
|
+
const verdict = judgeDeliverable(phase, text);
|
|
1952
2390
|
if (journal.cancelled) {
|
|
1953
2391
|
stage.status = "cancelled";
|
|
1954
2392
|
stage.outcome = "cancelled";
|
|
1955
2393
|
return null;
|
|
1956
2394
|
}
|
|
1957
|
-
if (stop === "completed" && text &&
|
|
2395
|
+
if (stop === "completed" && text && verdict.ok) {
|
|
1958
2396
|
stage.status = "done";
|
|
1959
2397
|
stage.outcome = "completed";
|
|
1960
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
|
+
});
|
|
1961
2404
|
return text;
|
|
1962
2405
|
}
|
|
1963
2406
|
if (stage.guardReason) {
|
|
@@ -1976,20 +2419,19 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
1976
2419
|
stage.outcome = stop === "completed" && text ? "insubstantial" : stop || "error";
|
|
1977
2420
|
const errDetail = result && result.error;
|
|
1978
2421
|
if (stage.outcome === "insubstantial") {
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
stage.summary = `产出未通过实质校验:命中拒绝词「${hit.phrase}」(原文:${hit.context}),视为未交付`;
|
|
2422
|
+
if (verdict.reason === "refusal" && verdict.refusal) {
|
|
2423
|
+
stage.summary = `产出未通过实质校验:无验证证据块且命中拒绝词「${verdict.refusal.phrase}」(原文:${verdict.refusal.context}),视为未交付`;
|
|
1982
2424
|
journal.logs.push({
|
|
1983
2425
|
t: Date.now(),
|
|
1984
2426
|
level: "warn",
|
|
1985
|
-
message: `${label} 产出命中拒绝词「${
|
|
2427
|
+
message: `${label} 产出命中拒绝词「${verdict.refusal.phrase}」且无 [Verification evidence] 块`
|
|
1986
2428
|
});
|
|
1987
2429
|
} else {
|
|
1988
|
-
stage.summary = `产出未通过实质校验:内容过短(${
|
|
2430
|
+
stage.summary = `产出未通过实质校验:内容过短(${verdict.length} 字符 < ${verdict.min} 下限),视为未交付`;
|
|
1989
2431
|
journal.logs.push({
|
|
1990
2432
|
t: Date.now(),
|
|
1991
2433
|
level: "warn",
|
|
1992
|
-
message: `${label} 产出过短(${
|
|
2434
|
+
message: `${label} 产出过短(${verdict.length} 字符),未通过实质校验`
|
|
1993
2435
|
});
|
|
1994
2436
|
}
|
|
1995
2437
|
if (text) stage.output = clip(text, 4e3);
|
|
@@ -2029,11 +2471,14 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
2029
2471
|
} catch (e2) {}
|
|
2030
2472
|
}
|
|
2031
2473
|
}
|
|
2032
|
-
/**
|
|
2474
|
+
/**
|
|
2475
|
+
* 单阶段重试 + token 熔断(**新增口径**:input+cacheWrite+output,排除 cacheRead——见
|
|
2476
|
+
* `FRESH_TOKEN_BUDGET` 与 metering.freshTokensOf;汇报仍走官方 totalTokensOf 口径)。
|
|
2477
|
+
* 顺序:不可重试/外部中止/护栏中止 → 预算门 → 自动重试(预算合理时重试优先于熔断,2026-09-11 修正)。
|
|
2033
2478
|
* `effortHint`:机械阶段的推理强度降档提示(第 1 次尝试生效,重试自动回升 high,见 resolveStageEffort)。 */
|
|
2034
2479
|
async function withRetry(journal, parent, label, phase, prompt, signal, taskKey, effortHint) {
|
|
2035
2480
|
let attempts = 0;
|
|
2036
|
-
let
|
|
2481
|
+
let freshTokens = 0;
|
|
2037
2482
|
let lastStage = null;
|
|
2038
2483
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
2039
2484
|
attempts = attempt;
|
|
@@ -2042,17 +2487,17 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
2042
2487
|
const beforeLen = journal.stages.length;
|
|
2043
2488
|
const result = await runAgent(journal, parent, labelNow, phase, promptNow, signal, taskKey, attempt, effortHint);
|
|
2044
2489
|
lastStage = journal.stages[beforeLen] || null;
|
|
2045
|
-
if (lastStage && lastStage.phase === phase)
|
|
2490
|
+
if (lastStage && lastStage.phase === phase) freshTokens += freshTokensOf(lastStage.usage);
|
|
2046
2491
|
if (result) return {
|
|
2047
2492
|
text: result,
|
|
2048
2493
|
attempts,
|
|
2049
|
-
|
|
2494
|
+
freshTokens,
|
|
2050
2495
|
stage: lastStage
|
|
2051
2496
|
};
|
|
2052
2497
|
if (journal.cancelled) return {
|
|
2053
2498
|
text: null,
|
|
2054
2499
|
attempts,
|
|
2055
|
-
|
|
2500
|
+
freshTokens,
|
|
2056
2501
|
stage: lastStage
|
|
2057
2502
|
};
|
|
2058
2503
|
if (lastStage && isUnretryable(lastStage.outcome, lastStage.outcome)) {
|
|
@@ -2065,7 +2510,7 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
2065
2510
|
return {
|
|
2066
2511
|
text: null,
|
|
2067
2512
|
attempts,
|
|
2068
|
-
|
|
2513
|
+
freshTokens,
|
|
2069
2514
|
stage: lastStage
|
|
2070
2515
|
};
|
|
2071
2516
|
}
|
|
@@ -2079,7 +2524,7 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
2079
2524
|
return {
|
|
2080
2525
|
text: null,
|
|
2081
2526
|
attempts,
|
|
2082
|
-
|
|
2527
|
+
freshTokens,
|
|
2083
2528
|
stage: lastStage
|
|
2084
2529
|
};
|
|
2085
2530
|
}
|
|
@@ -2093,7 +2538,7 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
2093
2538
|
return {
|
|
2094
2539
|
text: null,
|
|
2095
2540
|
attempts,
|
|
2096
|
-
|
|
2541
|
+
freshTokens,
|
|
2097
2542
|
stage: lastStage
|
|
2098
2543
|
};
|
|
2099
2544
|
}
|
|
@@ -2107,21 +2552,21 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
2107
2552
|
return {
|
|
2108
2553
|
text: null,
|
|
2109
2554
|
attempts,
|
|
2110
|
-
|
|
2555
|
+
freshTokens,
|
|
2111
2556
|
stage: lastStage
|
|
2112
2557
|
};
|
|
2113
2558
|
}
|
|
2114
|
-
if (
|
|
2559
|
+
if (freshTokens >= 2e5) {
|
|
2115
2560
|
journal.logs.push({
|
|
2116
2561
|
t: Date.now(),
|
|
2117
2562
|
level: "error",
|
|
2118
|
-
message: `${label}
|
|
2563
|
+
message: `${label} 累计新增 token ${Math.round(freshTokens / 1e3)}k(input+cacheWrite+output,不含缓存命中)超出预算 ${Math.round(FRESH_TOKEN_BUDGET / 1e3)}k,熔断,需人工介入`
|
|
2119
2564
|
});
|
|
2120
2565
|
journal.humanIntervention = true;
|
|
2121
2566
|
return {
|
|
2122
2567
|
text: null,
|
|
2123
2568
|
attempts,
|
|
2124
|
-
|
|
2569
|
+
freshTokens,
|
|
2125
2570
|
stage: lastStage
|
|
2126
2571
|
};
|
|
2127
2572
|
}
|
|
@@ -2142,176 +2587,11 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey,
|
|
|
2142
2587
|
return {
|
|
2143
2588
|
text: null,
|
|
2144
2589
|
attempts,
|
|
2145
|
-
|
|
2590
|
+
freshTokens,
|
|
2146
2591
|
stage: lastStage
|
|
2147
2592
|
};
|
|
2148
2593
|
}
|
|
2149
2594
|
//#endregion
|
|
2150
|
-
//#region host/core/state.ts
|
|
2151
|
-
/**
|
|
2152
|
-
* dsh-plugin-teamflow core — state.json 预编译上下文索引。
|
|
2153
|
-
*
|
|
2154
|
-
* 目标:解决「每个新 run 都要从小代理全量读历史文档(PRD/TECH/QA)来重建认知」的 token 爆炸。
|
|
2155
|
-
* state.json 是跨 run 累积的结构化索引:每次 run 结束后由各阶段把「精简结论」沉淀进来,
|
|
2156
|
-
* 下一个 run 的子代理只读注入的 state slice,不再重复读全套历史文档。
|
|
2157
|
-
*
|
|
2158
|
-
* 设计原则:
|
|
2159
|
-
* - memory.md 保持权威记忆(人读);state.json 是预编译索引(机器喂给子代理)。
|
|
2160
|
-
* - 子代理不直接读 state.json 文件,由 host 在开工时按角色注入相关 slice 到 prompt。
|
|
2161
|
-
* - state.json 只存「结论/指针」,不存全文;具体内容仍指向 docs/teamflow/ 下的活文档。
|
|
2162
|
-
*/
|
|
2163
|
-
/** 空态 state。 */
|
|
2164
|
-
function emptyState() {
|
|
2165
|
-
return {
|
|
2166
|
-
version: 1,
|
|
2167
|
-
projectName: null,
|
|
2168
|
-
updatedAt: null,
|
|
2169
|
-
product: {
|
|
2170
|
-
summary: null,
|
|
2171
|
-
techStack: null
|
|
2172
|
-
},
|
|
2173
|
-
lastRunFolder: null,
|
|
2174
|
-
modules: {},
|
|
2175
|
-
verifyScripts: [],
|
|
2176
|
-
acIndex: {},
|
|
2177
|
-
stages: {},
|
|
2178
|
-
lastRun: null
|
|
2179
|
-
};
|
|
2180
|
-
}
|
|
2181
|
-
/** state.json 路径:$DSH_HOME/teamflow/<projectKey>/state.json */
|
|
2182
|
-
function stateFile(projectKey) {
|
|
2183
|
-
return join(teamflowRoot(), projectKey, "state.json");
|
|
2184
|
-
}
|
|
2185
|
-
/** 读取(不存在返回空态)。 */
|
|
2186
|
-
function loadState(projectKey) {
|
|
2187
|
-
const file = stateFile(projectKey);
|
|
2188
|
-
try {
|
|
2189
|
-
if (existsSync(file)) {
|
|
2190
|
-
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
2191
|
-
const base = emptyState();
|
|
2192
|
-
if (raw && typeof raw === "object") {
|
|
2193
|
-
base.projectName = raw.projectName ?? null;
|
|
2194
|
-
base.updatedAt = raw.updatedAt ?? null;
|
|
2195
|
-
base.product = {
|
|
2196
|
-
...base.product,
|
|
2197
|
-
...raw.product || {}
|
|
2198
|
-
};
|
|
2199
|
-
base.lastRunFolder = raw.lastRunFolder ?? null;
|
|
2200
|
-
base.modules = raw.modules || {};
|
|
2201
|
-
base.verifyScripts = Array.isArray(raw.verifyScripts) ? raw.verifyScripts : [];
|
|
2202
|
-
base.acIndex = raw.acIndex || {};
|
|
2203
|
-
base.stages = raw.stages || {};
|
|
2204
|
-
base.lastRun = raw.lastRun ?? null;
|
|
2205
|
-
return base;
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
} catch (e) {}
|
|
2209
|
-
return emptyState();
|
|
2210
|
-
}
|
|
2211
|
-
/** 保存。 */
|
|
2212
|
-
function saveState(projectKey, state) {
|
|
2213
|
-
const file = stateFile(projectKey);
|
|
2214
|
-
try {
|
|
2215
|
-
mkdirSync(join(teamflowRoot(), projectKey), { recursive: true });
|
|
2216
|
-
state.updatedAt = Date.now();
|
|
2217
|
-
writeFileSync(file, JSON.stringify(state, null, 2), "utf8");
|
|
2218
|
-
return true;
|
|
2219
|
-
} catch (e) {
|
|
2220
|
-
console.error("[teamflow] saveState failed", e?.message);
|
|
2221
|
-
return false;
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
/** 从阶段产出文本中提取 `<!-- state -->{...}<!-- /state -->` 块(找不到返回 null)。 */
|
|
2225
|
-
function extractStateBlock(text) {
|
|
2226
|
-
const m = (text === null || text === void 0 ? "" : String(text)).match(/<!--\s*state\s*-->([\s\S]*?)(?:<!--\s*\/state\s*-->|$)/);
|
|
2227
|
-
if (!m || !m[1]) return null;
|
|
2228
|
-
try {
|
|
2229
|
-
const raw = JSON.parse(m[1].trim());
|
|
2230
|
-
if (raw && typeof raw === "object") return raw;
|
|
2231
|
-
} catch (e) {}
|
|
2232
|
-
return null;
|
|
2233
|
-
}
|
|
2234
|
-
/** 把阶段产出的 state 块合并进 state.json。 */
|
|
2235
|
-
function mergeStateBlock(projectKey, block, phase) {
|
|
2236
|
-
const state = loadState(projectKey);
|
|
2237
|
-
const key = block && block.phase || phase || "other";
|
|
2238
|
-
if (block) {
|
|
2239
|
-
if (typeof block.summary === "string" && block.summary.trim()) state.stages[key] = block.summary.trim();
|
|
2240
|
-
if (Array.isArray(block.touched)) {
|
|
2241
|
-
for (const f of block.touched) if (typeof f === "string" && f) state.modules[f] = state.modules[f] || "touched";
|
|
2242
|
-
}
|
|
2243
|
-
if (typeof block.verdict === "string" && block.verdict) {
|
|
2244
|
-
state.lastRun = state.lastRun || {};
|
|
2245
|
-
state.lastRun.verdict = block.verdict;
|
|
2246
|
-
}
|
|
2247
|
-
if (block.extra && typeof block.extra === "object") {
|
|
2248
|
-
if (Array.isArray(block.extra.verifyScripts)) {
|
|
2249
|
-
for (const s of block.extra.verifyScripts) if (typeof s === "string" && s && state.verifyScripts.indexOf(s) === -1) state.verifyScripts.push(s);
|
|
2250
|
-
}
|
|
2251
|
-
if (block.extra.acIndex && typeof block.extra.acIndex === "object") state.acIndex = {
|
|
2252
|
-
...state.acIndex,
|
|
2253
|
-
...block.extra.acIndex
|
|
2254
|
-
};
|
|
2255
|
-
if (typeof block.extra.techStack === "string" && block.extra.techStack) state.product.techStack = block.extra.techStack;
|
|
2256
|
-
if (typeof block.extra.moduleContracts === "object" && block.extra.moduleContracts) state.modules = {
|
|
2257
|
-
...state.modules,
|
|
2258
|
-
...block.extra.moduleContracts
|
|
2259
|
-
};
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
saveState(projectKey, state);
|
|
2263
|
-
return state;
|
|
2264
|
-
}
|
|
2265
|
-
/** 按 run 更新 lastRun / lastRunFolder(finally 时调用)。 */
|
|
2266
|
-
function noteRun(projectKey, run) {
|
|
2267
|
-
const state = loadState(projectKey);
|
|
2268
|
-
if (run.runDocs) state.lastRunFolder = run.runDocs;
|
|
2269
|
-
state.lastRun = {
|
|
2270
|
-
runId: run.id || null,
|
|
2271
|
-
requirement: run.requirement ? String(run.requirement).slice(0, 200) : null,
|
|
2272
|
-
verdict: run.verdict || null,
|
|
2273
|
-
folder: run.runDocs || null,
|
|
2274
|
-
endedAt: run.endedAt ?? Date.now()
|
|
2275
|
-
};
|
|
2276
|
-
saveState(projectKey, state);
|
|
2277
|
-
}
|
|
2278
|
-
/** 按角色渲染 state slice(注入到子代理 prompt)。角色 → 只拿相关片段。 */
|
|
2279
|
-
function stateSliceFor(state, role) {
|
|
2280
|
-
const lines = [];
|
|
2281
|
-
if (state.__runCtx) {
|
|
2282
|
-
if (state.__runCtx.runDocs) lines.push(`【本次任务产物夹】${state.__runCtx.runDocs}/(host 已创建;本需求的 PRD/TECHNICAL/QA-REPORT/ACCEPTANCE 全部写这里,夹建后不可变、不归档不升版)`);
|
|
2283
|
-
if (state.__runCtx.sanity) lines.push(state.__runCtx.sanity);
|
|
2284
|
-
if (state.__runCtx.blueprint && (role === "arch" || role === "tech" || role === "dev")) lines.push(state.__runCtx.blueprint);
|
|
2285
|
-
}
|
|
2286
|
-
lines.push("【预编译产品状态(state.json · 权威记忆在 docs/teamflow/memory.md,本块已是够用的索引,勿再全量读历史文档)】");
|
|
2287
|
-
if (state.product.summary) lines.push(`- 产品概要:${state.product.summary}`);
|
|
2288
|
-
if (state.product.techStack && (role === "tech" || role === "dev" || role === "arch")) lines.push(`- 技术栈:${state.product.techStack}`);
|
|
2289
|
-
if (Object.keys(state.acIndex).length && (role === "pm" || role === "qa" || role === "acceptance" || role === "tech")) {
|
|
2290
|
-
const acs = Object.entries(state.acIndex).slice(0, 40);
|
|
2291
|
-
lines.push(`- AC 索引(${acs.length} 条):${acs.map(([k, v]) => `${k} ${v}`).join(";")}`);
|
|
2292
|
-
}
|
|
2293
|
-
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(",")}`);
|
|
2294
|
-
if (state.verifyScripts.length && (role === "qa" || role === "tech" || role === "dev")) lines.push(`- 验证脚本:${state.verifyScripts.join(",")}`);
|
|
2295
|
-
if (role === "pm" || role === "acceptance") {
|
|
2296
|
-
if (state.stages.prd) lines.push(`- PRD 摘要:${state.stages.prd}`);
|
|
2297
|
-
if (state.stages.tech) lines.push(`- 技术方案摘要:${state.stages.tech}`);
|
|
2298
|
-
if (state.stages.qa) lines.push(`- QA 摘要:${state.stages.qa}`);
|
|
2299
|
-
} else if (role === "dev" || role === "tech") {
|
|
2300
|
-
if (state.stages.tech) lines.push(`- 技术方案摘要:${state.stages.tech}`);
|
|
2301
|
-
if (state.stages.prd) lines.push(`- PRD 摘要:${state.stages.prd}`);
|
|
2302
|
-
} else if (role === "qa") {
|
|
2303
|
-
if (state.stages.qa) lines.push(`- 上轮 QA 摘要:${state.stages.qa}`);
|
|
2304
|
-
}
|
|
2305
|
-
if (state.lastRun) {
|
|
2306
|
-
const r = state.lastRun;
|
|
2307
|
-
lines.push(`- 上轮:${r.requirement ? r.requirement : ""}${r.verdict ? " → " + r.verdict : ""}${r.folder ? `(${r.folder})` : ""}`);
|
|
2308
|
-
}
|
|
2309
|
-
return lines.join("\n");
|
|
2310
|
-
}
|
|
2311
|
-
/** 让每个阶段产出末尾附带 state 块(将并入 stage output,由 host 提取)。 */
|
|
2312
|
-
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):
|
|
2313
|
-
<!-- state -->{"phase":"<stage-key>","summary":"<≤500 chars: this stage's conclusion, useful for the next run>","memory":["<memory points>"]}<!-- /state -->`;
|
|
2314
|
-
//#endregion
|
|
2315
2595
|
//#region host/prompts/index.ts
|
|
2316
2596
|
/**
|
|
2317
2597
|
* dsh-plugin-teamflow — Prompt 模板(阶段提示词 + 团队模板)。
|
|
@@ -2369,7 +2649,7 @@ const AGENTS_TEMPLATE = `# AGENTS.md — 团队协作守则与文档索引({{P
|
|
|
2369
2649
|
| 任务产物 | ${TF_DOCS}/<yyyyMMdd-rN-slug>/ | 每个需求一个自包含任务夹:PRD/设计/技术方案/QA 报告/验收报告(按日期倒序即迭代史) |
|
|
2370
2650
|
| 架构总览 | ${TF_DOCS}/architecture/ARCHITECTURE.md | 工程方案与脚手架说明(产品级长期文档) |
|
|
2371
2651
|
| 产品记忆 | ${TF_DOCS}/memory.md | 团队约定/技术栈/已知待办(低频更新) |
|
|
2372
|
-
| 运行日志 | logs/teamflow/<runId>/ | TeamFlow
|
|
2652
|
+
| 运行日志 | logs/teamflow/<runId>/ | TeamFlow 流水线各阶段命令日志(日常不读);**布局约定**(全部在该目录内,项目根不得出现 scripts/ probe/):regression-<phase>.log(套件输出,重跑追加)/ scripts/(一次性校验脚本)/ captures.json(命令载荷汇总)/ probe/(探针)——每用途一个文件,不新增同名变体 |
|
|
2373
2653
|
|
|
2374
2654
|
## 3. 团队角色与标准流程
|
|
2375
2655
|
|
|
@@ -2451,6 +2731,13 @@ const TOKEN_HYGIENE = (runId) => `[TOKEN HYGIENE · policy] Context is expensive
|
|
|
2451
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.
|
|
2452
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.
|
|
2453
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.
|
|
2454
2741
|
- Keep reports/summaries tight (QA ≤150 lines, acceptance ≤80 lines, dev ≤40 lines); put details in files.
|
|
2455
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.
|
|
2456
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.
|
|
@@ -2573,7 +2860,7 @@ ${clip(tech, 12e3)}` : ""}
|
|
|
2573
2860
|
4. Actually write/modify code (grep + segmented reads to locate; no repeated whole-file reads), then run relevant build/verification to ensure green.
|
|
2574
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.
|
|
2575
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.
|
|
2576
|
-
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).
|
|
2577
2864
|
7. Output an implementation summary (≤40 lines): changed files, key implementation points, leftovers. No big code pastes.
|
|
2578
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):
|
|
2579
2866
|
[Verification evidence]
|
|
@@ -2608,7 +2895,7 @@ ${clip(devSummary, 15e3)}
|
|
|
2608
2895
|
- Always-available sandbox-legal paths: build/assembly checks, unit tests, DOM-level E2E (jsdom or equivalent), static audit, adversarial spot-checks.
|
|
2609
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.
|
|
2610
2897
|
3. Read AGENTS.md §4 engineering conventions (verify commands) and the code changes first, then actually run those sandbox-legal verifications.
|
|
2611
|
-
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.
|
|
2612
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).
|
|
2613
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:
|
|
2614
2901
|
| 编号 | 严重级(P0/P1/P2/P3) | 功能模块 | 复现步骤 | 期望行为 | 实际行为 | 关联验收项 |
|
|
@@ -2630,7 +2917,7 @@ ${tech && String(tech).trim() ? clip(tech, 12e3) : ""}
|
|
|
2630
2917
|
1. [Confirm first, then fix] For each defect, verify one by one whether it truly holds (read code / reproduce / compare actual vs expected):
|
|
2631
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).
|
|
2632
2919
|
2. Touch ONLY defect-related files (grep to locate; no whole-file reads of irrelevant big files); respect existing architecture & code style.
|
|
2633
|
-
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.
|
|
2634
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.
|
|
2635
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):
|
|
2636
2923
|
[Verification evidence]
|
|
@@ -3054,6 +3341,42 @@ function buildResumeProducts(journal) {
|
|
|
3054
3341
|
}
|
|
3055
3342
|
return products;
|
|
3056
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
|
+
}
|
|
3057
3380
|
/** 任务夹产物读取(单轨契约:文件即产物——QA/验收 host 只读文件,回复仅摘要)。
|
|
3058
3381
|
* 缺失/空/读取异常返回 null(调用方决定硬失败或 journal 兜底)。 */
|
|
3059
3382
|
function artifactText(journal, fileName) {
|
|
@@ -3200,10 +3523,10 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3200
3523
|
const stageFailError = (label, r) => {
|
|
3201
3524
|
const last = [...journal.stages || []].reverse().find((s) => phaseKeyOf(s.phase) === label);
|
|
3202
3525
|
const attempts = r && r.attempts ? r.attempts : 2;
|
|
3203
|
-
const burnt = Math.round((r && r.
|
|
3204
|
-
const breaker = (r && r.
|
|
3526
|
+
const burnt = Math.round((r && r.freshTokens || 0) / 1e3);
|
|
3527
|
+
const breaker = (r && r.freshTokens || 0) >= 2e5 ? ",超出新增 token 预算熔断" : "";
|
|
3205
3528
|
const detail = last ? `末次 ${last.outcome || "unknown"}${last.summary ? `(${last.summary})` : ""}` : "无阶段记录";
|
|
3206
|
-
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},需人工介入`);
|
|
3207
3530
|
};
|
|
3208
3531
|
try {
|
|
3209
3532
|
if (resume) journal.logs.push({
|
|
@@ -3315,7 +3638,8 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3315
3638
|
}
|
|
3316
3639
|
else if (!resume && journal.workspacePath && options.preAction === "commit") try {
|
|
3317
3640
|
const msg = typeof options.commitMessage === "string" && options.commitMessage.trim() ? options.commitMessage.trim() : `chore(teamflow): 流水线启动前提交现有改动(${journal.id})`;
|
|
3318
|
-
|
|
3641
|
+
ensureLogGitignore(journal.workspacePath, journal);
|
|
3642
|
+
const add = gitCmd(journal.workspacePath, tfAddArgs());
|
|
3319
3643
|
const cm = gitCmd(journal.workspacePath, [
|
|
3320
3644
|
"commit",
|
|
3321
3645
|
"-m",
|
|
@@ -3386,6 +3710,15 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3386
3710
|
if (stage) stage.verifyEvidence = ev;
|
|
3387
3711
|
} catch (e) {}
|
|
3388
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;
|
|
3389
3722
|
let prd = null;
|
|
3390
3723
|
if (resumed("prd")) {
|
|
3391
3724
|
prd = resume.products.prd;
|
|
@@ -3525,12 +3858,13 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3525
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 || ""))));
|
|
3526
3859
|
const resumePrompt = devPrompt(task, tech, prd, root, journal.id, state) + (prevStage ? buildRetryDiagnostic(2, prevStage) : "");
|
|
3527
3860
|
const devR = await withRetry(journal, parent, `开发 · ${task.title}(补跑)`, "dev", resumePrompt, signal, task.title);
|
|
3528
|
-
|
|
3861
|
+
const rerunText = stageTextOf(devR);
|
|
3862
|
+
noteVerifyEvidence(devR.stage, rerunText);
|
|
3529
3863
|
const ok = !!devR.text;
|
|
3530
3864
|
return {
|
|
3531
3865
|
title: task.title,
|
|
3532
3866
|
failed: !ok,
|
|
3533
|
-
output:
|
|
3867
|
+
output: rerunText || "开发失败(Agent 未产出结果)"
|
|
3534
3868
|
};
|
|
3535
3869
|
});
|
|
3536
3870
|
for (const t of rerun) {
|
|
@@ -3581,16 +3915,17 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3581
3915
|
}
|
|
3582
3916
|
}
|
|
3583
3917
|
const devR = await withRetry(journal, parent, `开发 · ${task.title}`, "dev", devPrompt(task, tech, prd, root, journal.id, state), signal, task.title);
|
|
3584
|
-
|
|
3918
|
+
const devText = stageTextOf(devR);
|
|
3919
|
+
noteVerifyEvidence(devR.stage, devText);
|
|
3585
3920
|
const ok = !!devR.text;
|
|
3586
3921
|
if (sub) {
|
|
3587
|
-
completeSubtask(journal, sub.id, !ok,
|
|
3922
|
+
completeSubtask(journal, sub.id, !ok, devText ? snippet(devText, 1e3) : null, null);
|
|
3588
3923
|
if (devR.stage) noteSubtaskUsage(journal, sub.id, devR.stage);
|
|
3589
3924
|
}
|
|
3590
3925
|
return {
|
|
3591
3926
|
title: task.title,
|
|
3592
3927
|
failed: !ok,
|
|
3593
|
-
output:
|
|
3928
|
+
output: devText || "开发失败(Agent 未产出结果)"
|
|
3594
3929
|
};
|
|
3595
3930
|
});
|
|
3596
3931
|
timeline.dev = devResults;
|
|
@@ -3671,7 +4006,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3671
4006
|
advanceTask(journal, "needs-human", null, "QA-REPORT.md 未落盘(单轨契约未兑现)", { by: "qa" });
|
|
3672
4007
|
throw stageFailError("qa", {
|
|
3673
4008
|
attempts: qaR.attempts,
|
|
3674
|
-
|
|
4009
|
+
freshTokens: qaR.freshTokens
|
|
3675
4010
|
});
|
|
3676
4011
|
}
|
|
3677
4012
|
timeline.qa = qa;
|
|
@@ -3713,7 +4048,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3713
4048
|
});
|
|
3714
4049
|
advanceTask(journal, "rework", snippet(qa, 3e3), `QA 打回开发修复(第 ${round}/3 轮)`, { by: "qa" });
|
|
3715
4050
|
const fixR = await withRetry(journal, parent, `开发 · QA 缺陷修复(第 ${round} 轮)`, "dev", qaFixPrompt(blocking, qa, tech, prd, root, journal.id, state), signal, null);
|
|
3716
|
-
noteVerifyEvidence(fixR.stage, fixR
|
|
4051
|
+
noteVerifyEvidence(fixR.stage, stageTextOf(fixR));
|
|
3717
4052
|
if (!fixR.text) {
|
|
3718
4053
|
advanceTask(journal, "needs-human", null, "QA 打回后开发修复失败", { by: "qa" });
|
|
3719
4054
|
throw stageFailError("开发(QA 打回修复)", fixR);
|
|
@@ -3779,7 +4114,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3779
4114
|
advanceTask(journal, "needs-human", null, "ACCEPTANCE.md 未落盘(单轨契约未兑现)", { by: "pm" });
|
|
3780
4115
|
throw stageFailError("acceptance", {
|
|
3781
4116
|
attempts: accR.attempts,
|
|
3782
|
-
|
|
4117
|
+
freshTokens: accR.freshTokens
|
|
3783
4118
|
});
|
|
3784
4119
|
}
|
|
3785
4120
|
timeline.acceptance = acceptance;
|
|
@@ -3863,7 +4198,8 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3863
4198
|
activeProducts.delete(scopeKey);
|
|
3864
4199
|
if (journal.workspacePath && journal.status === "completed" && !journal.humanIntervention) try {
|
|
3865
4200
|
const reqHead = String(journal.requirement || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
3866
|
-
|
|
4201
|
+
ensureLogGitignore(journal.workspacePath, journal);
|
|
4202
|
+
const add = gitCmd(journal.workspacePath, tfAddArgs());
|
|
3867
4203
|
if ((add === null ? null : gitCmd(journal.workspacePath, [
|
|
3868
4204
|
"commit",
|
|
3869
4205
|
"-m",
|
|
@@ -4108,18 +4444,7 @@ function resumeRun(runId, sessionId) {
|
|
|
4108
4444
|
* 运行环境:宿主组合(web profile)的真实 Node 进程。
|
|
4109
4445
|
*/
|
|
4110
4446
|
/** 阶段顺序/key 映射见 constants.ts(PHASE_ORDER/PHASE_KEY_OF/PHASE_KEY_BY_NAME)。 */
|
|
4111
|
-
/**
|
|
4112
|
-
function runsFor(ws) {
|
|
4113
|
-
const arr = [];
|
|
4114
|
-
for (const j of runs.values()) {
|
|
4115
|
-
if (ws) {
|
|
4116
|
-
if ((j.workspace || (ws === "default" ? "default" : null)) !== ws) continue;
|
|
4117
|
-
}
|
|
4118
|
-
arr.push(j);
|
|
4119
|
-
}
|
|
4120
|
-
arr.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
|
|
4121
|
-
return arr;
|
|
4122
|
-
}
|
|
4447
|
+
/** 按工作区作用域过滤运行见 core/products.ts(runsFor;全局面板与远程面共用)。 */
|
|
4123
4448
|
/** 由 sessionId 推导会话所属 workspace(项目)作用域。 */
|
|
4124
4449
|
function sessionScope(sessionId) {
|
|
4125
4450
|
const sid = typeof sessionId === "string" && sessionId ? sessionId : null;
|
|
@@ -4132,6 +4457,8 @@ function snapshotOf(j) {
|
|
|
4132
4457
|
status: j.status,
|
|
4133
4458
|
startedAt: j.startedAt,
|
|
4134
4459
|
endedAt: j.endedAt,
|
|
4460
|
+
address: runAddress(j.workspace || "default", j.id),
|
|
4461
|
+
ownerSession: j.ownerSession || null,
|
|
4135
4462
|
requirement: clip(j.requirement, 2e3),
|
|
4136
4463
|
options: sanitizeSnapOptions(j.options),
|
|
4137
4464
|
agentsStarted: j.agentsStarted,
|
|
@@ -4907,28 +5234,21 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4907
5234
|
list(sessionId) {
|
|
4908
5235
|
const sc = sessionScope(sessionId);
|
|
4909
5236
|
return {
|
|
4910
|
-
runs: runsFor(sc.projectKey).slice(0, 30).map(
|
|
4911
|
-
id: j.id,
|
|
4912
|
-
status: j.status,
|
|
4913
|
-
startedAt: j.startedAt,
|
|
4914
|
-
endedAt: j.endedAt,
|
|
4915
|
-
agentsStarted: j.agentsStarted,
|
|
4916
|
-
stageCount: j.stages.length,
|
|
4917
|
-
incompleteStages: (j.stages || []).some((x) => x.status !== "done"),
|
|
4918
|
-
requirement: clip(j.requirement, 60)
|
|
4919
|
-
})),
|
|
5237
|
+
runs: runsFor(sc.projectKey).slice(0, 30).map(runBrief),
|
|
4920
5238
|
workspace: sc
|
|
4921
5239
|
};
|
|
4922
5240
|
}
|
|
4923
|
-
|
|
4924
|
-
|
|
5241
|
+
/** run 详情(快照)。productOverride:全局面板按产品线 key 寻址时传入(跳过会话推导)。 */
|
|
5242
|
+
snapshot(runId, sessionId, productOverride) {
|
|
5243
|
+
const key = productOverride ? productKeyOf(productOverride) : sessionScope(sessionId).projectKey;
|
|
5244
|
+
if (!key) return null;
|
|
4925
5245
|
if (runId && typeof runId === "string") {
|
|
4926
5246
|
const j = runs.get(runId);
|
|
4927
5247
|
if (!j) return null;
|
|
4928
|
-
if (j
|
|
5248
|
+
if (!runVisibleIn(j, key)) return null;
|
|
4929
5249
|
return snapshotOf(j);
|
|
4930
5250
|
}
|
|
4931
|
-
const latest = runsFor(
|
|
5251
|
+
const latest = runsFor(key)[0];
|
|
4932
5252
|
if (!latest) return null;
|
|
4933
5253
|
const j = runs.get(latest.id);
|
|
4934
5254
|
return j ? snapshotOf(j) : null;
|
|
@@ -4936,12 +5256,13 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4936
5256
|
/** 阶段详情:卡片点击查看 —— 状态/耗时/官方 usage + 产物全文(超 24k 截断)。
|
|
4937
5257
|
* 2026-09-06 状态机化:返回同任务全部尝试(attempts 聚合——按 stage.taskKey(旧数据 label 兜底),
|
|
4938
5258
|
* 按 seq 排序)——client 弹窗单次渲染现状、多次渲染时间线。 */
|
|
4939
|
-
stageDetail(runId, seq, sessionId) {
|
|
5259
|
+
stageDetail(runId, seq, sessionId, productOverride) {
|
|
4940
5260
|
if (typeof runId !== "string" || !runId || seq === void 0 || seq === null) return null;
|
|
4941
|
-
const
|
|
5261
|
+
const key = productOverride ? productKeyOf(productOverride) : sessionScope(sessionId).projectKey;
|
|
5262
|
+
if (!key) return null;
|
|
4942
5263
|
const j = runs.get(runId);
|
|
4943
5264
|
if (!j) return null;
|
|
4944
|
-
if (j
|
|
5265
|
+
if (!runVisibleIn(j, key)) return null;
|
|
4945
5266
|
const s = (j.stages || []).find((st) => Number(st.seq) === Number(seq));
|
|
4946
5267
|
if (!s) return null;
|
|
4947
5268
|
const taskKeyOf = (x) => String(x.taskKey || String(x.label || "").replace(/^开发 · /, "").replace(/((?:第 \d+ 次重试|补跑))$/, "").trim());
|
|
@@ -4977,22 +5298,23 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4977
5298
|
};
|
|
4978
5299
|
}
|
|
4979
5300
|
/** Backlog 条目详情:卡片点击查看 —— 完整字段 + 流转时间线 + 关联(子卡/缺陷)+ 任务夹路径。 */
|
|
4980
|
-
itemDetail(kind, id, sessionId) {
|
|
5301
|
+
itemDetail(kind, id, sessionId, productOverride) {
|
|
4981
5302
|
const k = typeof kind === "string" && [
|
|
4982
5303
|
"req",
|
|
4983
5304
|
"task",
|
|
4984
5305
|
"bug"
|
|
4985
5306
|
].indexOf(kind) !== -1 ? kind : null;
|
|
4986
5307
|
if (!k || typeof id !== "string" || !id) return null;
|
|
4987
|
-
const
|
|
4988
|
-
|
|
5308
|
+
const key = productOverride ? productKeyOf(productOverride) : sessionScope(sessionId).projectKey;
|
|
5309
|
+
if (!key) return null;
|
|
5310
|
+
const store = storeFor(key);
|
|
4989
5311
|
const item = store.find(k, id);
|
|
4990
5312
|
if (!item) return null;
|
|
4991
5313
|
const reqId = k === "req" ? item.id : item.reqId || null;
|
|
4992
5314
|
let runDocs = null;
|
|
4993
5315
|
let runDocsRoot = null;
|
|
4994
5316
|
let runInfo = null;
|
|
4995
|
-
for (const j of runsFor(
|
|
5317
|
+
for (const j of runsFor(key)) {
|
|
4996
5318
|
if (j.reqId !== reqId) continue;
|
|
4997
5319
|
if (j.runDocs && !runDocs) {
|
|
4998
5320
|
runDocs = j.runDocs;
|
|
@@ -5003,17 +5325,19 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
5003
5325
|
status: j.status,
|
|
5004
5326
|
requirement: String(j.requirement || ""),
|
|
5005
5327
|
startedAt: j.startedAt || null,
|
|
5006
|
-
endedAt: j.endedAt || null
|
|
5328
|
+
endedAt: j.endedAt || null,
|
|
5329
|
+
ownerSession: j.ownerSession || null
|
|
5007
5330
|
};
|
|
5008
5331
|
}
|
|
5332
|
+
const artifactSession = runInfo && runInfo.ownerSession || sessionId;
|
|
5009
5333
|
const runArtifacts = [];
|
|
5010
|
-
if (runDocs && runDocsRoot && typeof
|
|
5334
|
+
if (runDocs && runDocsRoot && typeof artifactSession === "string" && artifactSession) try {
|
|
5011
5335
|
const present = new Set(readdirSync(join(runDocsRoot, runDocs)));
|
|
5012
5336
|
for (const name of TEAMFLOW_ARTIFACT_ORDER) {
|
|
5013
5337
|
if (!present.has(name)) continue;
|
|
5014
5338
|
runArtifacts.push({
|
|
5015
5339
|
name,
|
|
5016
|
-
address: fileAddressFor(
|
|
5340
|
+
address: fileAddressFor(artifactSession, void 0, `${runDocs}/${name}`)
|
|
5017
5341
|
});
|
|
5018
5342
|
}
|
|
5019
5343
|
} catch (e) {}
|
|
@@ -5092,6 +5416,41 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
5092
5416
|
bugs
|
|
5093
5417
|
};
|
|
5094
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
|
+
}
|
|
5095
5454
|
start(sessionId, requirement, options) {
|
|
5096
5455
|
const sid = typeof sessionId === "string" ? sessionId : null;
|
|
5097
5456
|
const req = typeof requirement === "string" && requirement.trim() ? requirement.trim() : null;
|
|
@@ -5128,11 +5487,12 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
5128
5487
|
};
|
|
5129
5488
|
return { ok: cancelRun(id) };
|
|
5130
5489
|
}
|
|
5131
|
-
/** 工作区级 backlog 视图(自动按当前会话 workspace 隔离)。 */
|
|
5132
|
-
backlog(sessionId) {
|
|
5133
|
-
const
|
|
5134
|
-
|
|
5135
|
-
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);
|
|
5136
5496
|
const runOf = (reqId) => {
|
|
5137
5497
|
if (!reqId) return null;
|
|
5138
5498
|
let last = null;
|