dsh-plugin-teamflow 0.1.5 → 0.1.6
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 +24 -0
- package/README.md +10 -0
- package/lib/client.js +173 -18
- package/lib/host.mjs +514 -143
- package/lib/store.mjs +3 -1
- package/package.json +6 -6
package/lib/host.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { TEAMFLOW_DESCRIPTORS } from "./descriptors.mjs";
|
|
2
2
|
import { fileFor, journalFile, loadJournals, persistJournal, readJson, readJsonAny, slugPath, teamflowRoot, writeJson } from "./store.mjs";
|
|
3
3
|
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
4
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
5
|
import { parameterSchemaSpecToJsonSchema } from "@deepseek-ai/dsh-tools";
|
|
5
6
|
import { dirname, join } from "node:path";
|
|
6
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { execFileSync } from "node:child_process";
|
|
8
|
-
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
9
9
|
//#region host/constants.ts
|
|
10
10
|
/** 单阶段 token 熔断预算(官方口径总消耗:input+cacheRead+cacheWrite+output 累计)。 */
|
|
11
11
|
const STAGE_TOKEN_BUDGET = 6e4;
|
|
@@ -21,6 +21,7 @@ const REFUSAL_PATTERN = /(无法完成|不能完成|无法继续|抱歉|对不
|
|
|
21
21
|
const STAGE_MIN_LENGTH = {
|
|
22
22
|
prd: 400,
|
|
23
23
|
design: 250,
|
|
24
|
+
scaffold: 250,
|
|
24
25
|
arch: 250,
|
|
25
26
|
tech: 350,
|
|
26
27
|
dev: 60,
|
|
@@ -57,16 +58,30 @@ const STATUS = {
|
|
|
57
58
|
"needs-human"
|
|
58
59
|
]
|
|
59
60
|
};
|
|
60
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* 流水线阶段(2026-09-06 英文化改造):内部一律英文键(journal.stage.phase / 代码判断 / 状态机)。
|
|
63
|
+
* 中文阶段名只作为展示 label(client UI 映射,未来 i18n 与 dsh 中英对齐)。
|
|
64
|
+
*/
|
|
61
65
|
const PHASE_ORDER = [
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
66
|
+
"prd",
|
|
67
|
+
"design",
|
|
68
|
+
"scaffold",
|
|
69
|
+
"tech",
|
|
70
|
+
"dev",
|
|
71
|
+
"qa",
|
|
72
|
+
"acceptance"
|
|
69
73
|
];
|
|
74
|
+
/** 英文键 → 中文展示名(仅 UI/label/日志文案使用,不得用于代码判断)。 */
|
|
75
|
+
const PHASE_KEY_OF = {
|
|
76
|
+
prd: "PRD 产品需求",
|
|
77
|
+
design: "UI/UX 设计",
|
|
78
|
+
scaffold: "架构规划",
|
|
79
|
+
tech: "技术方案",
|
|
80
|
+
dev: "开发",
|
|
81
|
+
qa: "QA 测试",
|
|
82
|
+
acceptance: "产品验收"
|
|
83
|
+
};
|
|
84
|
+
/** 中文阶段名 → 英文键(存量 journal/backlog 兼容映射;迁移脚本执行后仅防御性保留)。 */
|
|
70
85
|
const PHASE_KEY_BY_NAME = {
|
|
71
86
|
"PRD 产品需求": "prd",
|
|
72
87
|
"UI/UX 设计": "design",
|
|
@@ -76,15 +91,22 @@ const PHASE_KEY_BY_NAME = {
|
|
|
76
91
|
"QA 测试": "qa",
|
|
77
92
|
"产品验收": "acceptance"
|
|
78
93
|
};
|
|
79
|
-
/**
|
|
94
|
+
/** phase 归一:中文(存量)或英文(新数据)输入 → 英文键;未知回退原值小写化。 */
|
|
95
|
+
function phaseKeyOf(phase) {
|
|
96
|
+
const p = String(phase || "");
|
|
97
|
+
if (!p) return "";
|
|
98
|
+
if (PHASE_KEY_BY_NAME[p]) return PHASE_KEY_BY_NAME[p];
|
|
99
|
+
return p;
|
|
100
|
+
}
|
|
101
|
+
/** 阶段英文键 → 角色键(任务卡 byRole 累计用;未知阶段归 'other')。 */
|
|
80
102
|
const PHASE_ROLE = {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
103
|
+
prd: "pm",
|
|
104
|
+
design: "design",
|
|
105
|
+
scaffold: "arch",
|
|
106
|
+
tech: "tech",
|
|
107
|
+
dev: "dev",
|
|
108
|
+
qa: "qa",
|
|
109
|
+
acceptance: "acceptance"
|
|
88
110
|
};
|
|
89
111
|
const STAGE_POLICY = {
|
|
90
112
|
full: [
|
|
@@ -185,6 +207,17 @@ function extractText(blocks) {
|
|
|
185
207
|
if (!Array.isArray(blocks)) return "";
|
|
186
208
|
return blocks.filter((b) => b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
|
|
187
209
|
}
|
|
210
|
+
/** 从 dev/qaFix 回复中提取「验证证据」块(`[Verification evidence]` 行起,到 state 块/结尾止)。
|
|
211
|
+
* dev 阶段无独立对抗校验(QA 有 QA-REPORT.md 结构化证据,dev 只有自述)——证据块是「可审计的
|
|
212
|
+
* 具体自述」:命令+退出码+断言计数+失败行引用,可对照 logs/teamflow/<runId>/ 命令输出日志核实;
|
|
213
|
+
* 模型仍可伪造,但具体细节难编造一致(具体性压力)且伪造可发现(审计轨迹)。
|
|
214
|
+
* 找不到块(契约未兑现)→ null,host 记 warn 不中断(policy 级)。 */
|
|
215
|
+
function extractVerificationEvidence(text) {
|
|
216
|
+
const m = toText(text).match(/\[Verification evidence\]([\s\S]*?)(?=<!--\s*state|$)/);
|
|
217
|
+
if (!m || !m[1]) return null;
|
|
218
|
+
const ev = m[1].trim();
|
|
219
|
+
return ev.length > 0 ? ev : null;
|
|
220
|
+
}
|
|
188
221
|
/**
|
|
189
222
|
* ADR-0008 任务夹命名:<yyyyMMdd>-r<N>[-<slug>]。
|
|
190
223
|
* - date 用本地时区(用户在东八区晚上建的需求不能落到"明天")
|
|
@@ -253,10 +286,11 @@ function sanitizeSnapOptions(o) {
|
|
|
253
286
|
const SAFE_SIGNAL = {
|
|
254
287
|
aborted: false,
|
|
255
288
|
addEventListener: () => {},
|
|
256
|
-
removeEventListener: () => {}
|
|
289
|
+
removeEventListener: () => {},
|
|
290
|
+
throwIfAborted: () => {}
|
|
257
291
|
};
|
|
258
292
|
function normalizeSignal(s) {
|
|
259
|
-
return s && typeof s === "object" && typeof s.addEventListener === "function" && typeof s.aborted === "boolean" ? s : SAFE_SIGNAL;
|
|
293
|
+
return s && typeof s === "object" && typeof s.addEventListener === "function" && typeof s.aborted === "boolean" && typeof s.throwIfAborted === "function" ? s : SAFE_SIGNAL;
|
|
260
294
|
}
|
|
261
295
|
/** 分支 slug 派生(ADR-2026-08-27):branchName > triageSlug > 需求中的英文标识词 > reqId 数字 > 'feature'。
|
|
262
296
|
* 实锤 feat/feature:lite 显式时 triage 不跑(无 slug)+ 分支检查早于 reqId 生成 → fallback 'feature'。 */
|
|
@@ -305,6 +339,33 @@ function handoffBrief(text) {
|
|
|
305
339
|
const m = String(text).match(/<!--\s*handoff\s*-->([\s\S]*?)(?:<!--\s*\/handoff\s*-->|$)/);
|
|
306
340
|
return clip((m && m[1] ? m[1] : String(text)).trim(), 2e3);
|
|
307
341
|
}
|
|
342
|
+
/** 拒绝词命中点:返回命中的具体短语 + 原文上下文片段(供重试诊断回灌,比事后从截断尾巴重算可靠)。 */
|
|
343
|
+
function refusalHit(text) {
|
|
344
|
+
const s = String(text || "");
|
|
345
|
+
const m = REFUSAL_PATTERN.exec(s);
|
|
346
|
+
if (!m || m.index < 0) return null;
|
|
347
|
+
const start = Math.max(0, m.index - 40);
|
|
348
|
+
const end = Math.min(s.length, m.index + String(m[0]).length + 40);
|
|
349
|
+
return {
|
|
350
|
+
phrase: m[0],
|
|
351
|
+
context: s.slice(start, end).replace(/\s+/g, " ").trim()
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/** 重试诊断包:上一轮失败详情回灌进重试 prompt(盲试 → 带因重试)。
|
|
355
|
+
* 失败分类/详情/护栏原因取自 stage;产出尾部截断 1000 字符供自查修正。 */
|
|
356
|
+
function buildRetryDiagnostic(attempt, stage) {
|
|
357
|
+
const lines = [];
|
|
358
|
+
lines.push(`[重试诊断 · 第 ${attempt} 次尝试] 上一轮尝试未成功。这不是新任务——请先阅读以下失败详情,再执行原任务并修正上一轮的问题。`);
|
|
359
|
+
lines.push(`- 失败分类:${stage.outcome || "unknown"}`);
|
|
360
|
+
if (stage.guardReason) lines.push(`- 护栏中止原因:${stage.guardReason}`);
|
|
361
|
+
if (stage.summary) lines.push(`- 详情:${stage.summary}`);
|
|
362
|
+
const out = String(stage.output || "");
|
|
363
|
+
if (out) {
|
|
364
|
+
const tail = out.length > 1e3 ? `…${out.slice(-1e3)}` : out;
|
|
365
|
+
lines.push(`- 上一轮产出末尾(节选,供自查修正):\n${tail}`);
|
|
366
|
+
}
|
|
367
|
+
return `\n\n${lines.join("\n")}\n[/重试诊断结束]`;
|
|
368
|
+
}
|
|
308
369
|
/**
|
|
309
370
|
* 验收结论解析:只以显式「验收结论 / 整体结论」行为准(acceptancePrompt 强制 4 档固定话术),
|
|
310
371
|
* 不做正文散文朴素子串匹配。历史误报实锤(run tf-msytlok5):验收报告 ✅ 通过,其记忆回写段一句
|
|
@@ -313,8 +374,11 @@ function handoffBrief(text) {
|
|
|
313
374
|
* - 「📝 需求不适用」是验收负责人专用的强结论词,允许全文命中;
|
|
314
375
|
* - 其余 reject 词(需求与实际不符/站不住/无效/无需改动等)仅在结论行且该行不含「通过/✅/⚠️」时才算;
|
|
315
376
|
* - rework 词仅认结论行(且不与「✅ 通过」同现)。
|
|
377
|
+
* 反向护栏(漏报实锤 2026-09-03):模型写「❌ 不通过」但漏写「验收结论:」前缀 → accLine 为空 →
|
|
378
|
+
* 旧实现落回默认 accepted(最乐观默认值,质量门禁漏报=假交付)。现改为 **找不到结论行 → needs-human**
|
|
379
|
+
* (宁严勿松:误拦截=人工看一眼,误放行=假交付;📝 全文命中与架构红词仍优先于该默认)。
|
|
316
380
|
* @param {unknown} text 验收报告全文
|
|
317
|
-
* @returns {'accepted'|'rework'|'reject'}
|
|
381
|
+
* @returns {'accepted'|'rework'|'reject'|'needs-human'}
|
|
318
382
|
*/
|
|
319
383
|
function parseAcceptanceVerdict(text) {
|
|
320
384
|
const acc = String(text || "");
|
|
@@ -322,9 +386,14 @@ function parseAcceptanceVerdict(text) {
|
|
|
322
386
|
const hasArchRedFlag = /重复实现|重复适配|偏离蓝图|未按蓝图|该拆未拆|该抽象未抽象|破坏既有结构|结构性.*问题|架构(打回|需重构)|需.*返工|返工.*项.*(存在|仍)|仍.*(返工|重构)/.test(acc);
|
|
323
387
|
const archNegated = /无返工|无.*返工|不返工|无架构打回|无.*打回|非漂移|无.*重复|无.*偏离|无.*抽象.*问题|无.*蓝图.*问题|架构一致性.*(PASS|良好|达标|通过|无问题)|M3.*(PASS|通过|达标)|架构.*(达标|无问题|良好)/.test(acc);
|
|
324
388
|
if (hasArchRedFlag && !archNegated) return "rework";
|
|
325
|
-
if (/❌\s*不通过|需返工|未通过/.test(accLine) && !/✅\s*通过/.test(accLine)) return "rework";
|
|
326
389
|
if (/📝\s*需求不适用/.test(acc)) return "reject";
|
|
390
|
+
if (/不通过|需返工|未通过/.test(accLine) && !/无\s*不通过|未发现不通过|未出现不通过/.test(accLine)) {
|
|
391
|
+
if (/✅\s*通过/.test(accLine)) return "accepted";
|
|
392
|
+
return "rework";
|
|
393
|
+
}
|
|
327
394
|
if (!/通过|✅|⚠️/.test(accLine) && /需求不适用|需求与实际不符|需求站不住|需求无效|无需改动|无需修改/.test(accLine)) return "reject";
|
|
395
|
+
if (!accLine) return "needs-human";
|
|
396
|
+
if (!/通过|✅|⚠️|❌|📝/.test(accLine)) return "needs-human";
|
|
328
397
|
return "accepted";
|
|
329
398
|
}
|
|
330
399
|
const bdOpen = "<!-- blueprint -->";
|
|
@@ -834,6 +903,20 @@ function createSubtask(journal, title, spec) {
|
|
|
834
903
|
const store = storeFor(journal.workspace || "default");
|
|
835
904
|
const mainTask = journal.taskId ? store.find("task", journal.taskId) : null;
|
|
836
905
|
if (!mainTask) return null;
|
|
906
|
+
const fullTitle = `开发 · ${title}`;
|
|
907
|
+
const existing = store.tasks.find((t) => t.reqId === journal.reqId && t.parentId === journal.taskId && (t.taskKey && t.taskKey === title || !t.taskKey && t.title === fullTitle));
|
|
908
|
+
if (existing) {
|
|
909
|
+
existing.status = "pending";
|
|
910
|
+
existing.failed = false;
|
|
911
|
+
existing.summary = null;
|
|
912
|
+
existing.endedAt = null;
|
|
913
|
+
existing.retries = (existing.retries || 0) + 1;
|
|
914
|
+
existing.taskKey = existing.taskKey || title;
|
|
915
|
+
existing.updatedAt = Date.now();
|
|
916
|
+
store.persist();
|
|
917
|
+
persistJournal(journal);
|
|
918
|
+
return existing;
|
|
919
|
+
}
|
|
837
920
|
const id = store.nextId("dev");
|
|
838
921
|
const sub = {
|
|
839
922
|
id,
|
|
@@ -841,7 +924,8 @@ function createSubtask(journal, title, spec) {
|
|
|
841
924
|
parentId: journal.taskId,
|
|
842
925
|
product: journal.workspace || "default",
|
|
843
926
|
type: "subtask",
|
|
844
|
-
title:
|
|
927
|
+
title: fullTitle,
|
|
928
|
+
taskKey: title,
|
|
845
929
|
spec: spec || "",
|
|
846
930
|
status: "pending",
|
|
847
931
|
devAssign: mainTask && mainTask.devAssign || null,
|
|
@@ -1170,16 +1254,59 @@ function runSanityCheck(path) {
|
|
|
1170
1254
|
//#endregion
|
|
1171
1255
|
//#region host/core/metering.ts
|
|
1172
1256
|
/**
|
|
1257
|
+
* 采集子代理会话事件(多源回退——2026-09-07 实锤 r38 usage 全空):
|
|
1258
|
+
* 宿主新版 Session(session v2)已无 `events` 属性/getter(仅私有 eventsSnapshot 缓存 +
|
|
1259
|
+
* 官方 snapshotEvents()/ownEvents() 方法),旧实现读 session.events = undefined → usage 全 null。
|
|
1260
|
+
* 回退链(与 guard.eventsOf 同款语义):events(老宿主快照,兼容)→ snapshotEvents()(官方完整日志)
|
|
1261
|
+
* → ownEvents()(fork 后本 agent 自己的事件)。取信息最多(含 usage 事件数最多)的源。
|
|
1262
|
+
*/
|
|
1263
|
+
function sessionEventsOf(run) {
|
|
1264
|
+
try {
|
|
1265
|
+
const local = run && run.localAgent;
|
|
1266
|
+
const session = local && local.session;
|
|
1267
|
+
if (!session) return [];
|
|
1268
|
+
const candidates = [];
|
|
1269
|
+
try {
|
|
1270
|
+
const raw = session.events;
|
|
1271
|
+
if (Array.isArray(raw)) candidates.push(raw);
|
|
1272
|
+
else if (typeof raw === "function") candidates.push(raw());
|
|
1273
|
+
} catch (e) {}
|
|
1274
|
+
try {
|
|
1275
|
+
if (typeof session.snapshotEvents === "function") candidates.push(session.snapshotEvents());
|
|
1276
|
+
} catch (e) {}
|
|
1277
|
+
try {
|
|
1278
|
+
if (typeof session.ownEvents === "function") candidates.push(session.ownEvents());
|
|
1279
|
+
} catch (e) {}
|
|
1280
|
+
const valid = candidates.filter((c) => Array.isArray(c));
|
|
1281
|
+
if (valid.length === 0) return [];
|
|
1282
|
+
const countUsage = (arr) => arr.filter((ev) => {
|
|
1283
|
+
const e = ev;
|
|
1284
|
+
return e && e.type === "assistant/message" && e.data && typeof e.data.usage === "object" && e.data.usage !== null;
|
|
1285
|
+
}).length;
|
|
1286
|
+
valid.sort((a, b) => countUsage(b) - countUsage(a));
|
|
1287
|
+
return valid[0];
|
|
1288
|
+
} catch (e) {
|
|
1289
|
+
return [];
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
/** 从单个 assistant/message 事件取 usage(宿主 usageOf 同款双路径:data.usage 优先,
|
|
1293
|
+
* 缺失时从 data.stream 的 usage chunk 取——v2 事件 usage 可能只在 stream 里)。 */
|
|
1294
|
+
function usageOfEvent(e) {
|
|
1295
|
+
if (!e || e.type !== "assistant/message") return void 0;
|
|
1296
|
+
const d = e.data || {};
|
|
1297
|
+
if (d.usage && typeof d.usage === "object") return d.usage;
|
|
1298
|
+
if (Array.isArray(d.stream)) for (const member of [...d.stream].reverse()) {
|
|
1299
|
+
const chunk = member?.chunk;
|
|
1300
|
+
if (chunk && chunk.type === "usage" && chunk.usage && typeof chunk.usage === "object") return chunk.usage;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1173
1304
|
* 累计子代理会话中所有 LLM 调用的真实 usage(官方三桶 + 调用数)。
|
|
1174
1305
|
* 返回 null 表示拿不到 usage(会话未暴露 events / 无数据)。
|
|
1175
1306
|
*/
|
|
1176
1307
|
function accumulateSessionUsage(run) {
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
if (!session) return null;
|
|
1180
|
-
const rawEvents = session.events;
|
|
1181
|
-
const events = Array.isArray(rawEvents) ? rawEvents : typeof rawEvents === "function" ? rawEvents() : null;
|
|
1182
|
-
if (!Array.isArray(events)) return null;
|
|
1308
|
+
const events = sessionEventsOf(run);
|
|
1309
|
+
if (events.length === 0) return null;
|
|
1183
1310
|
const buckets = {
|
|
1184
1311
|
input: 0,
|
|
1185
1312
|
cacheRead: 0,
|
|
@@ -1193,7 +1320,7 @@ function accumulateSessionUsage(run) {
|
|
|
1193
1320
|
if (!e || e.type !== "assistant/message") continue;
|
|
1194
1321
|
const d = e.data || {};
|
|
1195
1322
|
if (typeof d.turn === "number" && typeof d.step === "number") seen.add(`${d.turn}.${d.step}`);
|
|
1196
|
-
const u =
|
|
1323
|
+
const u = usageOfEvent(e);
|
|
1197
1324
|
if (u) {
|
|
1198
1325
|
buckets.input += u.inputTokens || 0;
|
|
1199
1326
|
buckets.cacheRead += u.cacheReadTokens || 0;
|
|
@@ -1240,14 +1367,48 @@ function totalTokensOf(usage) {
|
|
|
1240
1367
|
* 纯 read 循环(反复整读同一文件却无变更/无脚本执行)= 真退化。实锤 run tf-mte906e9:QA 重跑
|
|
1241
1368
|
* 只读分析(不 edit)→ 旧判定「零变更进展」误杀,第 2 次 provider error 后 450k 熔断。 */
|
|
1242
1369
|
const PROGRESS_TOOLS = /^(edit|write|create|apply_patch|patch|remove|delete|rm|mkdir|move|rename|append|bash|pwsh|shell|powershell)$/i;
|
|
1243
|
-
/**
|
|
1370
|
+
/** Agent 活动守卫(2026-09-06 实锤 r1):QA 子代理正常干活却被判「10 分钟无事件」——
|
|
1371
|
+
* 事件视图可能失明(session.events 缓存快照不增长)。若 agent 仍非 idle(phase 在跑)
|
|
1372
|
+
* 且本会话动过手(lastMutationAt>0)→ 不是挂死,跳过本次判定(不中止)。
|
|
1373
|
+
* 纯启动静默挂死(未动手)不受影响——照常 B 触发。 */
|
|
1374
|
+
function isAgentBusy(run) {
|
|
1375
|
+
try {
|
|
1376
|
+
const agent = run && run.localAgent;
|
|
1377
|
+
const kind = agent && agent.phase && agent.phase.kind;
|
|
1378
|
+
return !!kind && kind !== "idle";
|
|
1379
|
+
} catch (e) {
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
/** 与 metering 同款事件访问器(session.events 可能是数组或返回数组的函数)。
|
|
1384
|
+
* 2026-09-06 多源回退(实锤 json-parse r1:QA 子代理正常干活 254 事件 43 step 却被判「10 分钟
|
|
1385
|
+
* 无任何新事件」——session.events 缓存快照视图对某些子代理不增长)。回退链:
|
|
1386
|
+
* events(快照 getter)→ snapshotEvents()(宿主官方 API)→ ownEvents()(fork 后事件)——
|
|
1387
|
+
* 取信息最多(最长)的源;全部失效返回 [](stalled 触发前会记录诊断,见 fire())。 */
|
|
1244
1388
|
function eventsOf(run) {
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1389
|
+
try {
|
|
1390
|
+
const local = run && run.localAgent;
|
|
1391
|
+
const session = local && local.session;
|
|
1392
|
+
if (!session) return [];
|
|
1393
|
+
const candidates = [];
|
|
1394
|
+
try {
|
|
1395
|
+
const raw = session.events;
|
|
1396
|
+
if (Array.isArray(raw)) candidates.push(raw);
|
|
1397
|
+
else if (typeof raw === "function") candidates.push(raw());
|
|
1398
|
+
} catch (e) {}
|
|
1399
|
+
try {
|
|
1400
|
+
if (typeof session.snapshotEvents === "function") candidates.push(session.snapshotEvents());
|
|
1401
|
+
} catch (e) {}
|
|
1402
|
+
try {
|
|
1403
|
+
if (typeof session.ownEvents === "function") candidates.push(session.ownEvents());
|
|
1404
|
+
} catch (e) {}
|
|
1405
|
+
const valid = candidates.filter((c) => Array.isArray(c));
|
|
1406
|
+
if (valid.length === 0) return [];
|
|
1407
|
+
valid.sort((a, b) => b.length - a.length);
|
|
1408
|
+
return valid[0];
|
|
1409
|
+
} catch (e) {
|
|
1410
|
+
return [];
|
|
1411
|
+
}
|
|
1251
1412
|
}
|
|
1252
1413
|
/** 规范化文本片段:小写 + 仅保留字母数字/CJK,供逐字重复比对。 */
|
|
1253
1414
|
function normalizeFragment(s) {
|
|
@@ -1304,6 +1465,7 @@ function startStageGuard(opts) {
|
|
|
1304
1465
|
const warnedScripts = /* @__PURE__ */ new Set();
|
|
1305
1466
|
let lastMutationAt = 0;
|
|
1306
1467
|
let repeatWarned = false;
|
|
1468
|
+
let busyWarned = false;
|
|
1307
1469
|
function warnOnce(key, set, message, hint) {
|
|
1308
1470
|
if (set.has(key)) return;
|
|
1309
1471
|
set.add(key);
|
|
@@ -1322,6 +1484,34 @@ function startStageGuard(opts) {
|
|
|
1322
1484
|
clearInterval(timer);
|
|
1323
1485
|
stage.guardReason = reason;
|
|
1324
1486
|
stage.guardOutcome = outcome;
|
|
1487
|
+
if (outcome === "stalled") try {
|
|
1488
|
+
const local = run.localAgent;
|
|
1489
|
+
const session = local && local.session;
|
|
1490
|
+
const lens = [];
|
|
1491
|
+
if (session) {
|
|
1492
|
+
try {
|
|
1493
|
+
const r = session.events;
|
|
1494
|
+
lens.push(`events=${Array.isArray(r) ? r.length : typeof r === "function" ? r().length : "?"}`);
|
|
1495
|
+
} catch (e) {
|
|
1496
|
+
lens.push("events=err");
|
|
1497
|
+
}
|
|
1498
|
+
try {
|
|
1499
|
+
lens.push(`snap=${typeof session.snapshotEvents === "function" ? session.snapshotEvents().length : "-"}`);
|
|
1500
|
+
} catch (e) {
|
|
1501
|
+
lens.push("snap=err");
|
|
1502
|
+
}
|
|
1503
|
+
try {
|
|
1504
|
+
lens.push(`own=${typeof session.ownEvents === "function" ? session.ownEvents().length : "-"}`);
|
|
1505
|
+
} catch (e) {
|
|
1506
|
+
lens.push("own=err");
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
journal.logs.push({
|
|
1510
|
+
t: Date.now(),
|
|
1511
|
+
level: "warn",
|
|
1512
|
+
message: `${label} 挂死诊断:${lens.join(" / ") || "session 不可访问"}`
|
|
1513
|
+
});
|
|
1514
|
+
} catch (e) {}
|
|
1325
1515
|
try {
|
|
1326
1516
|
journal.logs.push({
|
|
1327
1517
|
t: Date.now(),
|
|
@@ -1395,8 +1585,22 @@ function startStageGuard(opts) {
|
|
|
1395
1585
|
lastEventCount = events.length;
|
|
1396
1586
|
lastGrowthAt = Date.now();
|
|
1397
1587
|
} else if (Date.now() - lastGrowthAt > 6e5) {
|
|
1398
|
-
|
|
1399
|
-
|
|
1588
|
+
if (lastMutationAt > 0 && isAgentBusy(run)) {
|
|
1589
|
+
if (!busyWarned) {
|
|
1590
|
+
busyWarned = true;
|
|
1591
|
+
try {
|
|
1592
|
+
journal.logs.push({
|
|
1593
|
+
t: Date.now(),
|
|
1594
|
+
level: "warn",
|
|
1595
|
+
message: `${label} 事件视图零增长但 agent 仍活动(会话已动手)——视为视图失明而非挂死,继续观察(护栏诊断见 stall 分支)`
|
|
1596
|
+
});
|
|
1597
|
+
} catch (e) {}
|
|
1598
|
+
}
|
|
1599
|
+
lastGrowthAt = Date.now();
|
|
1600
|
+
} else {
|
|
1601
|
+
fire(`挂死(${Math.round(GUARD_SILENCE_MS / 6e4)} 分钟无任何新事件)`, "stalled");
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1400
1604
|
}
|
|
1401
1605
|
if (seenToolCall && Date.now() - lastToolSignalAt > 9e5) {
|
|
1402
1606
|
fire(`空转(${Math.round(GUARD_NO_TOOL_MS / 6e4)} 分钟内无任何工具调用,但会话仍在产出)`, "stalled");
|
|
@@ -1498,7 +1702,7 @@ function resolveChildRoute(parent) {
|
|
|
1498
1702
|
return out;
|
|
1499
1703
|
}
|
|
1500
1704
|
/** 运行单个阶段子代理:执行 + 产出实质校验 + token 双口径计量 + stage 状态流转。 */
|
|
1501
|
-
async function runAgent(journal, parent, label, phase, prompt, signal) {
|
|
1705
|
+
async function runAgent(journal, parent, label, phase, prompt, signal, taskKey) {
|
|
1502
1706
|
const maxSeq = journal.stages.length ? Math.max(...journal.stages.map((s) => s.seq)) : 0;
|
|
1503
1707
|
let stageText = null;
|
|
1504
1708
|
const stage = {
|
|
@@ -1507,6 +1711,7 @@ async function runAgent(journal, parent, label, phase, prompt, signal) {
|
|
|
1507
1711
|
phase,
|
|
1508
1712
|
status: "running",
|
|
1509
1713
|
outcome: null,
|
|
1714
|
+
taskKey: taskKey || null,
|
|
1510
1715
|
childId: null,
|
|
1511
1716
|
startedAt: Date.now(),
|
|
1512
1717
|
endedAt: null,
|
|
@@ -1575,6 +1780,7 @@ async function runAgent(journal, parent, label, phase, prompt, signal) {
|
|
|
1575
1780
|
stage.status = "failed";
|
|
1576
1781
|
stage.outcome = stage.guardOutcome || "degenerated";
|
|
1577
1782
|
stage.summary = `进行中护栏中止(${stage.guardReason}),本次尝试无有效产出`;
|
|
1783
|
+
if (text) stage.output = clip(text, 4e3);
|
|
1578
1784
|
journal.logs.push({
|
|
1579
1785
|
t: Date.now(),
|
|
1580
1786
|
level: "warn",
|
|
@@ -1586,12 +1792,23 @@ async function runAgent(journal, parent, label, phase, prompt, signal) {
|
|
|
1586
1792
|
stage.outcome = stop === "completed" && text ? "insubstantial" : stop || "error";
|
|
1587
1793
|
const errDetail = result && result.error;
|
|
1588
1794
|
if (stage.outcome === "insubstantial") {
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1795
|
+
const hit = refusalHit(text);
|
|
1796
|
+
if (hit) {
|
|
1797
|
+
stage.summary = `产出未通过实质校验:命中拒绝词「${hit.phrase}」(原文:${hit.context}),视为未交付`;
|
|
1798
|
+
journal.logs.push({
|
|
1799
|
+
t: Date.now(),
|
|
1800
|
+
level: "warn",
|
|
1801
|
+
message: `${label} 产出命中拒绝词「${hit.phrase}」`
|
|
1802
|
+
});
|
|
1803
|
+
} else {
|
|
1804
|
+
stage.summary = `产出未通过实质校验:内容过短(${text.trim().length} 字符 < ${STAGE_MIN_LENGTH[phase] ?? 100} 下限),视为未交付`;
|
|
1805
|
+
journal.logs.push({
|
|
1806
|
+
t: Date.now(),
|
|
1807
|
+
level: "warn",
|
|
1808
|
+
message: `${label} 产出过短(${text.trim().length} 字符),未通过实质校验`
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
if (text) stage.output = clip(text, 4e3);
|
|
1595
1812
|
} else {
|
|
1596
1813
|
stage.summary = `未产出有效结果(stopReason=${stop || "unknown"}${errDetail ? `,error=${String(errDetail).slice(0, 200)}` : ""})`;
|
|
1597
1814
|
journal.logs.push({
|
|
@@ -1599,6 +1816,7 @@ async function runAgent(journal, parent, label, phase, prompt, signal) {
|
|
|
1599
1816
|
level: "error",
|
|
1600
1817
|
message: `${label} ${stage.summary}`
|
|
1601
1818
|
});
|
|
1819
|
+
if (text) stage.output = clip(text, 4e3);
|
|
1602
1820
|
}
|
|
1603
1821
|
return null;
|
|
1604
1822
|
} catch (e) {
|
|
@@ -1628,23 +1846,29 @@ async function runAgent(journal, parent, label, phase, prompt, signal) {
|
|
|
1628
1846
|
}
|
|
1629
1847
|
}
|
|
1630
1848
|
/** 单阶段重试 + token 熔断(官方口径:input+cacheRead+cacheWrite+output 累计)。 */
|
|
1631
|
-
async function withRetry(journal, parent, label, phase, prompt, signal) {
|
|
1849
|
+
async function withRetry(journal, parent, label, phase, prompt, signal, taskKey) {
|
|
1632
1850
|
let attempts = 0;
|
|
1633
1851
|
let stageTokens = 0;
|
|
1852
|
+
let lastStage = null;
|
|
1634
1853
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
1635
1854
|
attempts = attempt;
|
|
1636
|
-
const
|
|
1637
|
-
const
|
|
1855
|
+
const labelNow = attempt > 1 ? `${label}(第 ${attempt} 次重试)` : label;
|
|
1856
|
+
const promptNow = attempt > 1 && lastStage ? prompt + buildRetryDiagnostic(attempt, lastStage) : prompt;
|
|
1857
|
+
const beforeLen = journal.stages.length;
|
|
1858
|
+
const result = await runAgent(journal, parent, labelNow, phase, promptNow, signal, taskKey);
|
|
1859
|
+
lastStage = journal.stages[beforeLen] || null;
|
|
1638
1860
|
if (lastStage && lastStage.phase === phase) stageTokens += totalTokensOf(lastStage.usage);
|
|
1639
1861
|
if (result) return {
|
|
1640
1862
|
text: result,
|
|
1641
1863
|
attempts,
|
|
1642
|
-
stageTokens
|
|
1864
|
+
stageTokens,
|
|
1865
|
+
stage: lastStage
|
|
1643
1866
|
};
|
|
1644
1867
|
if (journal.cancelled) return {
|
|
1645
1868
|
text: null,
|
|
1646
1869
|
attempts,
|
|
1647
|
-
stageTokens
|
|
1870
|
+
stageTokens,
|
|
1871
|
+
stage: lastStage
|
|
1648
1872
|
};
|
|
1649
1873
|
if (lastStage && isUnretryable(lastStage.outcome, lastStage.outcome)) {
|
|
1650
1874
|
journal.logs.push({
|
|
@@ -1656,7 +1880,8 @@ async function withRetry(journal, parent, label, phase, prompt, signal) {
|
|
|
1656
1880
|
return {
|
|
1657
1881
|
text: null,
|
|
1658
1882
|
attempts,
|
|
1659
|
-
stageTokens
|
|
1883
|
+
stageTokens,
|
|
1884
|
+
stage: lastStage
|
|
1660
1885
|
};
|
|
1661
1886
|
}
|
|
1662
1887
|
if (lastStage && lastStage.outcome === "aborted") {
|
|
@@ -1669,7 +1894,8 @@ async function withRetry(journal, parent, label, phase, prompt, signal) {
|
|
|
1669
1894
|
return {
|
|
1670
1895
|
text: null,
|
|
1671
1896
|
attempts,
|
|
1672
|
-
stageTokens
|
|
1897
|
+
stageTokens,
|
|
1898
|
+
stage: lastStage
|
|
1673
1899
|
};
|
|
1674
1900
|
}
|
|
1675
1901
|
if (lastStage && lastStage.outcome === "degenerated") {
|
|
@@ -1682,7 +1908,22 @@ async function withRetry(journal, parent, label, phase, prompt, signal) {
|
|
|
1682
1908
|
return {
|
|
1683
1909
|
text: null,
|
|
1684
1910
|
attempts,
|
|
1685
|
-
stageTokens
|
|
1911
|
+
stageTokens,
|
|
1912
|
+
stage: lastStage
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
if (lastStage && lastStage.outcome === "stalled") {
|
|
1916
|
+
journal.logs.push({
|
|
1917
|
+
t: Date.now(),
|
|
1918
|
+
level: "warn",
|
|
1919
|
+
message: `${label} 进行中护栏中止(挂死/空转),不再自动重试(会话已无有效产出);可 teamflow_resume 以全新会话续跑`
|
|
1920
|
+
});
|
|
1921
|
+
journal.humanIntervention = true;
|
|
1922
|
+
return {
|
|
1923
|
+
text: null,
|
|
1924
|
+
attempts,
|
|
1925
|
+
stageTokens,
|
|
1926
|
+
stage: lastStage
|
|
1686
1927
|
};
|
|
1687
1928
|
}
|
|
1688
1929
|
if (stageTokens >= 6e4) {
|
|
@@ -1695,13 +1936,14 @@ async function withRetry(journal, parent, label, phase, prompt, signal) {
|
|
|
1695
1936
|
return {
|
|
1696
1937
|
text: null,
|
|
1697
1938
|
attempts,
|
|
1698
|
-
stageTokens
|
|
1939
|
+
stageTokens,
|
|
1940
|
+
stage: lastStage
|
|
1699
1941
|
};
|
|
1700
1942
|
}
|
|
1701
1943
|
if (attempt < 2) journal.logs.push({
|
|
1702
1944
|
t: Date.now(),
|
|
1703
1945
|
level: "warn",
|
|
1704
|
-
message: `${label} 第 ${attempt}
|
|
1946
|
+
message: `${label} 第 ${attempt} 次尝试未成功(${lastStage ? lastStage.outcome || "unknown" : "unknown"}),自动重试(重试 prompt 已附上一轮失败诊断)…`
|
|
1705
1947
|
});
|
|
1706
1948
|
else {
|
|
1707
1949
|
journal.logs.push({
|
|
@@ -1715,7 +1957,8 @@ async function withRetry(journal, parent, label, phase, prompt, signal) {
|
|
|
1715
1957
|
return {
|
|
1716
1958
|
text: null,
|
|
1717
1959
|
attempts,
|
|
1718
|
-
stageTokens
|
|
1960
|
+
stageTokens,
|
|
1961
|
+
stage: lastStage
|
|
1719
1962
|
};
|
|
1720
1963
|
}
|
|
1721
1964
|
//#endregion
|
|
@@ -1897,6 +2140,17 @@ const STATE_BLOCK_INSTRUCTION = `\n\n[STATE BLOCK · mandatory at the end] Appen
|
|
|
1897
2140
|
* - 回归基线:新 PRD 头部「基线依赖:<其他任务夹>」声明;跨代变更用「取代:<夹>#AC-n」;
|
|
1898
2141
|
* 硬保障在项目 verify-* 可执行套件。
|
|
1899
2142
|
* - 命令输出日志照旧收口 logs/teamflow/<runId>/。
|
|
2143
|
+
*
|
|
2144
|
+
* 【约束分级约定】(2026-09-03,防 high-signal 词脱敏)
|
|
2145
|
+
* - [HOST-ENFORCED]:host 有真实校验/解析/硬失败后果(如单轨产物文件缺失→needs-human 停线、
|
|
2146
|
+
* 验收结论行缺失→需人工确认)。标注后**必须**在同一句内描述真实后果(缺失=停线),
|
|
2147
|
+
* 不得只堆措辞。新增此类约束 = 先加 host 代码再标词。
|
|
2148
|
+
* - [policy]:无 host 强制,靠模型自律 + guard 观测注入(warn + 轻提醒,从不中断)。
|
|
2149
|
+
* 标注时描述真实机制(warn-only / cache 重放费),不声称「hard constraint」。
|
|
2150
|
+
* - 禁止:prompt 内自称 hard constraint——措辞层面「hard」与 enforcement 脱节会训练模型
|
|
2151
|
+
* 对 high-signal 词脱敏(实证:17 条 warn 零削减,guard 注入闭环后才见效)。
|
|
2152
|
+
* 【中英混排纪律】约束句/标签用英文(模型对英文指令注意力高)、被约束对象/内容用中文;
|
|
2153
|
+
* 列表分隔符随内容语言(文件路径等 ASCII 内容用英文逗号),不混用中文标点。
|
|
1900
2154
|
*/
|
|
1901
2155
|
/** 产品层文档根(memory.md 等跨任务资产;任务产物在其中的任务夹内)。 */
|
|
1902
2156
|
const TF_DOCS = "docs/teamflow";
|
|
@@ -1984,8 +2238,8 @@ function productCtx(root) {
|
|
|
1984
2238
|
Before starting: read ${base}/AGENTS.md (team rules & doc index — read the summary first, then details on demand; no aimless full reads).
|
|
1985
2239
|
[Task-folder docs · ADR-0008] Each requirement gets a self-contained task folder docs/teamflow/<yyyyMMdd-rN-slug>/ (folder path given per stage below); ALL artifacts of this requirement (PRD/DESIGN/TECHNICAL/QA-REPORT/ACCEPTANCE) live inside it. The folder is immutable after creation — **no archiving, no versioning** — retries/resumes write to the same folder. Cross-requirement product docs only: ${TF_DOCS}/memory.md (conventions/todos) and architecture/.
|
|
1986
2240
|
[Baseline] New PRD declares "基线依赖:<prior task folder>" at top; cross-generation behavior changes are explicitly marked "取代:<folder>#AC-n" — historical folders are never modified.
|
|
1987
|
-
[Doc boundary ·
|
|
1988
|
-
[AGENTS.md boundary ·
|
|
2241
|
+
[Doc boundary · policy] TeamFlow contract docs are written ONLY under ${base}/${TF_DOCS}/ (create dirs if missing); **never write host docs/<role>/ and never scatter log files at project root**; command output logs go to logs/teamflow/<runId>/.
|
|
2242
|
+
[AGENTS.md boundary · policy] AGENTS.md is team property (injected unconditionally — consensus/index/managed zone only): **do NOT append changelog-style sections during iterations (product memory / todos / change log)** — such data belongs in ${TF_DOCS}/memory.md and task folders; besides the <!-- teamflow:begin/end --> managed zone, no stage may rewrite, reorder, or overwrite any other part of AGENTS.md.
|
|
1989
2243
|
Backlog (req/task/bug) source of truth is the persisted mirror $DSH_HOME/teamflow/<workspace>/ under ${base}/backlog/: single rotating task card model (待办→开发中→待测试→测试中→待验收→已验收); devAssign/qaAssign live on the task card.
|
|
1990
2244
|
`;
|
|
1991
2245
|
}
|
|
@@ -1995,7 +2249,7 @@ function headTailClip(text, head, tail) {
|
|
|
1995
2249
|
if (s.length <= head + tail) return s;
|
|
1996
2250
|
return s.slice(0, head) + "\n...\n[CHANGED SECTION]\n" + s.slice(-tail);
|
|
1997
2251
|
}
|
|
1998
|
-
const TOKEN_HYGIENE = (runId) => `[TOKEN HYGIENE ·
|
|
2252
|
+
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:
|
|
1999
2253
|
- [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.
|
|
2000
2254
|
- [No duplicate reads] Same file: read ≤1 times. To verify a change, grep the change point instead of re-reading the whole file.
|
|
2001
2255
|
- [grep first] Before writing code, locate with one comprehensive grep pass, then batch-read in segments; avoid repeated small read/grep passes on the same file.
|
|
@@ -2007,7 +2261,7 @@ const TOKEN_HYGIENE = (runId) => `[TOKEN HYGIENE · hard constraint] Context is
|
|
|
2007
2261
|
- 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.
|
|
2008
2262
|
`;
|
|
2009
2263
|
/** 一次成型纪律:目标文档 write ≤1 次 + read ≤2 次,严禁 read→edit→read 循环。 */
|
|
2010
|
-
const ONCE_DISCIPLINE = `[ONE-SHOT WRITE ·
|
|
2264
|
+
const ONCE_DISCIPLINE = `[ONE-SHOT WRITE · policy] The most important efficiency rule; repeated write/read cycles pay cache replay fees (warn + reminder at 3rd read, never interrupt):
|
|
2011
2265
|
- The target delivery doc (PRD/DESIGN/TECHNICAL/QA-REPORT/ACCEPTANCE/memory) allows only **1 write of the complete new version** + **at most 2 reads** (1 to confirm structure before writing, ≤1 to verify format after).
|
|
2012
2266
|
- **No read→edit→read loops**: never reopen the same document to "tweak"; never re-read the whole file just to confirm a change.
|
|
2013
2267
|
- Use grep + limited segments for details; never whole-file read big documents.
|
|
@@ -2110,7 +2364,7 @@ ${clip(prd, 12e3)}
|
|
|
2110
2364
|
const devPrompt = (task, tech, prd, root, runId, state) => `You are a senior full-stack engineer (implementation executor). The current workspace IS the target project — actually implement the following task.
|
|
2111
2365
|
${productCtx(root)}${stateSliceFor(state, "dev")}${TOKEN_HYGIENE(runId)}[CONTEXT PACK]
|
|
2112
2366
|
[TASK TITLE] ${task.title}
|
|
2113
|
-
${task.files && task.files.length ? `[TASK TARGET FILES] ${task.files.join("
|
|
2367
|
+
${task.files && task.files.length ? `[TASK TARGET FILES] ${task.files.join(", ")}` : ""}
|
|
2114
2368
|
[TASK BRIEF] ${task.spec || "(see technical design)"}
|
|
2115
2369
|
${tech && String(tech).trim() ? `[TECH DESIGN SUMMARY (grep details on demand, don't full re-read)]
|
|
2116
2370
|
${clip(tech, 12e3)}` : ""}
|
|
@@ -2121,9 +2375,14 @@ ${clip(tech, 12e3)}` : ""}
|
|
|
2121
2375
|
3. If spec contradicts reality, explain with evidence in the summary instead of claiming completion or expanding scope on your own.
|
|
2122
2376
|
4. Actually write/modify code (grep + segmented reads to locate; no repeated whole-file reads), then run relevant build/verification to ensure green.
|
|
2123
2377
|
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.
|
|
2124
|
-
5b. [Git discipline ·
|
|
2378
|
+
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.
|
|
2125
2379
|
6. [Log discipline] Redirect command output to logs/teamflow/${runId || "<runId>"}/.
|
|
2126
|
-
7. Output an implementation summary (≤40 lines): changed files, key implementation points,
|
|
2380
|
+
7. Output an implementation summary (≤40 lines): changed files, key implementation points, leftovers. No big code pastes.
|
|
2381
|
+
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
|
+
[Verification evidence]
|
|
2383
|
+
- cmd: <exact command> → exit <code>, <passed>/<failed> asserts (<file>:<line> for failures)
|
|
2384
|
+
- ...(one line per verification run)
|
|
2385
|
+
- N/A: <explicit reason>(when nothing runnable — pure config/docs change, no test suite, etc.)
|
|
2127
2386
|
8. [State] End with a state block (phase="dev"), touched = array of changed files, summary = implementation conclusion.${STATE_BLOCK_INSTRUCTION}`;
|
|
2128
2387
|
/** 视觉验证能力条款(ADR-2026-08-27,解锁 browser-use 视觉验证):
|
|
2129
2388
|
* 按当前模型多模态能力动态生成——vision=true 允许截图看图(人眼类项),精确值仍走 DOM 计算断言;
|
|
@@ -2153,11 +2412,11 @@ ${clip(devSummary, 15e3)}
|
|
|
2153
2412
|
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.
|
|
2154
2413
|
3. Read AGENTS.md §4 engineering conventions (verify commands) and the code changes first, then actually run those sandbox-legal verifications.
|
|
2155
2414
|
4. [Log discipline] Redirect command output to logs/teamflow/${runId || "<runId>"}/ (e.g. qa-out.log); no scatter at project root.
|
|
2156
|
-
5. Output
|
|
2157
|
-
6. [Defect format] Report found defects as the structured table below (for direct import by the defect tracker):
|
|
2415
|
+
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
|
+
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:
|
|
2158
2417
|
| 编号 | 严重级(P0/P1/P2/P3) | 功能模块 | 复现步骤 | 期望行为 | 实际行为 | 关联验收项 |
|
|
2159
2418
|
If no defects: explicitly output 「未发现缺陷」.
|
|
2160
|
-
7. Chinese Markdown, concrete & executable; write the report to ${RUN(state)}/QA-REPORT.md (write once, tight body). [Boundary] only under ${TF_DOCS}/.
|
|
2419
|
+
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}/.
|
|
2161
2420
|
8. [State] End with a state block (phase="qa"), summary = test conclusion / blocked items, extra = { "verifyScripts": [...] }.${STATE_BLOCK_INSTRUCTION}`;
|
|
2162
2421
|
/** QA 打回后的开发修复 prompt:确认缺陷是否属实 → 修复 → 复验交接(QA→dev 打回闭环用)。 */
|
|
2163
2422
|
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.
|
|
@@ -2174,7 +2433,12 @@ ${tech && String(tech).trim() ? clip(tech, 12e3) : ""}
|
|
|
2174
2433
|
— confirmed → fix it directly; QA false positive / contradicts reality → state evidence explicit in the summary (no fabricated changes, and no ignoring real defects either).
|
|
2175
2434
|
2. Touch ONLY defect-related files (grep to locate; no whole-file reads of irrelevant big files); respect existing architecture & code style.
|
|
2176
2435
|
3. After fixing, run relevant verification to ensure green (regression floor: existing verify suites pass untouched); redirect output to logs/teamflow/${runId || "<runId>"}/.
|
|
2177
|
-
4. Output a fix summary (≤40 lines, Chinese): per defect —「truth judgment + fix」or「false-positive evidence」, changed files,
|
|
2436
|
+
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
|
+
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
|
+
[Verification evidence]
|
|
2439
|
+
- cmd: <exact command> → exit <code>, <passed>/<failed> asserts (<file>:<line> for failures)
|
|
2440
|
+
- ...(one line per verification run; the re-verified defect cases must be listed)
|
|
2441
|
+
- N/A: <explicit reason>(when nothing runnable — pure config/docs change, no test suite, etc.)
|
|
2178
2442
|
5. [State] End with a state block (phase="dev"), touched = changed files array, summary = fix conclusion.${STATE_BLOCK_INSTRUCTION}`;
|
|
2179
2443
|
const acceptancePrompt = (prd, qa, devSummary, root, runId, state, vision) => `You are the product manager (acceptance lead). Do a final acceptance of this delivery against the PRD acceptance criteria.
|
|
2180
2444
|
${productCtx(root)}${stateSliceFor(state, "acceptance")}${TOKEN_HYGIENE(runId)}
|
|
@@ -2191,9 +2455,9 @@ ${vision ? "[Visual re-check] If QA saved screenshots under the task folder, spo
|
|
|
2191
2455
|
- Any obvious **duplicated implementation / adapter drift / broken existing structure** (this is a code-quality floor, not optional).
|
|
2192
2456
|
- **Verdict impact**: only functionally green but with 「deviates from blueprint / duplicated impl / should-have-extracted」 → verdict should be **⚠️ 有条件通过** (architecture rework items listed, re-accept after rework); **significant deviation / broken structure → ❌ 不通过**. Never treat "verify all green" as the sole evidence of "no rework needed".
|
|
2193
2457
|
1. Verify each PRD acceptance criterion one by one.
|
|
2194
|
-
2. Output
|
|
2458
|
+
2. [Reply = brief summary only · HOST-ENFORCED] Output a short reply (≤10 lines, Chinese): **verdict line — verbatim: 验收结论:✅ 通过 / ⚠️ 有条件通过 / ❌ 不通过 / 📝 需求不适用**(pick one)+ the acceptance report path docs/teamflow/.../ACCEPTANCE.md. **Do NOT repeat the report body in the reply** — the host imports ACCEPTANCE.md as the single source of truth.
|
|
2195
2459
|
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".
|
|
2196
|
-
4. [Acceptance report] Write to ${RUN(state)}/ACCEPTANCE.md (write once,
|
|
2460
|
+
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.
|
|
2197
2461
|
5. Chinese Markdown.
|
|
2198
2462
|
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}`;
|
|
2199
2463
|
/** 需求分诊模型 prompt(模型驱动 triage;供 core/triage.runTriage 使用)。 */
|
|
@@ -2580,10 +2844,10 @@ function buildResumeProducts(journal) {
|
|
|
2580
2844
|
const products = {};
|
|
2581
2845
|
for (const s of journal.stages) {
|
|
2582
2846
|
if (s.status !== "done" || !s.output) continue;
|
|
2583
|
-
const key =
|
|
2847
|
+
const key = phaseKeyOf(s.phase);
|
|
2584
2848
|
if (!key) continue;
|
|
2585
|
-
if (key === "dev") products.dev = journal.stages.filter((x) => x.phase === "
|
|
2586
|
-
title: x.label.replace(/^开发 · /, ""),
|
|
2849
|
+
if (key === "dev") products.dev = journal.stages.filter((x) => phaseKeyOf(x.phase) === "dev" && x.status === "done" && x.output).map((x) => ({
|
|
2850
|
+
title: x.taskKey || x.label.replace(/^开发 · /, ""),
|
|
2587
2851
|
failed: false,
|
|
2588
2852
|
output: x.output
|
|
2589
2853
|
}));
|
|
@@ -2591,6 +2855,19 @@ function buildResumeProducts(journal) {
|
|
|
2591
2855
|
}
|
|
2592
2856
|
return products;
|
|
2593
2857
|
}
|
|
2858
|
+
/** 任务夹产物读取(单轨契约:文件即产物——QA/验收 host 只读文件,回复仅摘要)。
|
|
2859
|
+
* 缺失/空/读取异常返回 null(调用方决定硬失败或 journal 兜底)。 */
|
|
2860
|
+
function artifactText(journal, fileName) {
|
|
2861
|
+
const path = journal && journal.workspacePath && journal.runDocs ? `${journal.workspacePath}/${journal.runDocs}/${fileName}` : null;
|
|
2862
|
+
if (!path) return null;
|
|
2863
|
+
try {
|
|
2864
|
+
if (!existsSync(path)) return null;
|
|
2865
|
+
const t = readFileSync(path, "utf8").trim();
|
|
2866
|
+
return t ? t : null;
|
|
2867
|
+
} catch (e) {
|
|
2868
|
+
return null;
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2594
2871
|
/**
|
|
2595
2872
|
* 断点续跑起点:第一个「没有任意 done 尝试」的阶段。
|
|
2596
2873
|
* ⚠️ 按阶段而非尝试判断(实锤 tf-mtcomxpq):PRD 第 1 次尝试 failed(护栏退化)但第 2 次重试 done——
|
|
@@ -2598,13 +2875,37 @@ function buildResumeProducts(journal) {
|
|
|
2598
2875
|
* 全部完成仍被中断(理论极端)→ 从产品验收继续。
|
|
2599
2876
|
*/
|
|
2600
2877
|
function interruptedPhaseOf(journal) {
|
|
2601
|
-
if (hasOpenBlockingBugs(journal)) return "
|
|
2878
|
+
if (hasOpenBlockingBugs(journal)) return "qa";
|
|
2602
2879
|
for (const phase of PHASE_ORDER) {
|
|
2603
|
-
const phaseStages = (journal.stages || []).filter((s) => s.phase === phase);
|
|
2880
|
+
const phaseStages = (journal.stages || []).filter((s) => phaseKeyOf(s.phase) === phase);
|
|
2604
2881
|
if (phaseStages.length === 0) continue;
|
|
2605
|
-
if (
|
|
2882
|
+
if (phase === "dev") {
|
|
2883
|
+
if ([...devTaskStatuses(phaseStages).values()].some((st) => !st.done)) return phase;
|
|
2884
|
+
} else if (!phaseStages.some((s) => s.status === "done")) return phase;
|
|
2885
|
+
}
|
|
2886
|
+
return "acceptance";
|
|
2887
|
+
}
|
|
2888
|
+
/** 任务级聚合(journal 驱动,2026-09-06 状态机化):按 stage.taskKey(旧数据 label 兜底)分组——
|
|
2889
|
+
* 有 done stage = 任务已成功(历史失败尝试不算失败)。
|
|
2890
|
+
* resume 补跑判定/阶段完成判定共用;不读 backlog(两块业务线解耦——残留失败卡污染判定实锤 json-parse r1)。 */
|
|
2891
|
+
function devTaskStatuses(stages) {
|
|
2892
|
+
const m = /* @__PURE__ */ new Map();
|
|
2893
|
+
for (const s of stages || []) {
|
|
2894
|
+
const title = String(s.taskKey || String(s.label || "").replace(/^开发 · /, "").replace(/((?:第 \d+ 次重试|补跑))$/, "").trim());
|
|
2895
|
+
if (!title) continue;
|
|
2896
|
+
const cur = m.get(title) || {
|
|
2897
|
+
done: false,
|
|
2898
|
+
lastStatus: null,
|
|
2899
|
+
lastSeq: -1
|
|
2900
|
+
};
|
|
2901
|
+
if ((s.seq || 0) > cur.lastSeq) {
|
|
2902
|
+
cur.lastSeq = s.seq || 0;
|
|
2903
|
+
cur.lastStatus = s.status || null;
|
|
2904
|
+
}
|
|
2905
|
+
if (s.status === "done") cur.done = true;
|
|
2906
|
+
m.set(title, cur);
|
|
2606
2907
|
}
|
|
2607
|
-
return
|
|
2908
|
+
return m;
|
|
2608
2909
|
}
|
|
2609
2910
|
/** 开发任务定义(单一来源):架构蓝图自动拆 > 调用方显式 tasks > 整体开发兜底。
|
|
2610
2911
|
* resume 补跑与正常执行共用(defByTitle 按 title 匹配失败子卡)。 */
|
|
@@ -2698,18 +2999,18 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2698
2999
|
});
|
|
2699
3000
|
/** 阶段失败错误:带真实尝试次数/末次结果/累计消耗与熔断语义(取代千篇一律的「重试 N 次后仍无产出」)。 */
|
|
2700
3001
|
const stageFailError = (label, r) => {
|
|
2701
|
-
const last = [...journal.stages || []].reverse().find((s) => s.phase === label);
|
|
3002
|
+
const last = [...journal.stages || []].reverse().find((s) => phaseKeyOf(s.phase) === label);
|
|
2702
3003
|
const attempts = r && r.attempts ? r.attempts : 2;
|
|
2703
3004
|
const burnt = Math.round((r && r.stageTokens || 0) / 1e3);
|
|
2704
3005
|
const breaker = (r && r.stageTokens || 0) >= 6e4 ? ",超出阶段预算熔断" : "";
|
|
2705
3006
|
const detail = last ? `末次 ${last.outcome || "unknown"}${last.summary ? `(${last.summary})` : ""}` : "无阶段记录";
|
|
2706
|
-
return /* @__PURE__ */ new Error(`${label} 阶段失败:${attempts} 次尝试未交付,${detail},累计消耗 ${burnt}k token${breaker},需人工介入`);
|
|
3007
|
+
return /* @__PURE__ */ new Error(`${PHASE_KEY_OF[label] || label} 阶段失败:${attempts} 次尝试未交付,${detail},累计消耗 ${burnt}k token${breaker},需人工介入`);
|
|
2707
3008
|
};
|
|
2708
3009
|
try {
|
|
2709
3010
|
if (resume) journal.logs.push({
|
|
2710
3011
|
t: Date.now(),
|
|
2711
3012
|
level: "info",
|
|
2712
|
-
message: `断点续跑:复用 backlog(req=${journal.reqId}),从「${resume.phase}」继续`
|
|
3013
|
+
message: `断点续跑:复用 backlog(req=${journal.reqId}),从「${PHASE_KEY_OF[resume.phase] || resume.phase}」继续`
|
|
2713
3014
|
});
|
|
2714
3015
|
else {
|
|
2715
3016
|
const init = initPipelineBacklog(journal, requirement, options);
|
|
@@ -2872,11 +3173,25 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2872
3173
|
const block = extractStateBlock(output);
|
|
2873
3174
|
if (block) mergeStateBlock(journal.workspace || "default", block, phaseKey);
|
|
2874
3175
|
};
|
|
3176
|
+
const noteVerifyEvidence = (stage, output) => {
|
|
3177
|
+
try {
|
|
3178
|
+
const ev = extractVerificationEvidence(output);
|
|
3179
|
+
if (!ev) {
|
|
3180
|
+
journal.logs.push({
|
|
3181
|
+
t: Date.now(),
|
|
3182
|
+
level: "warn",
|
|
3183
|
+
message: `${stage ? PHASE_KEY_OF[phaseKeyOf(stage.phase)] || stage.phase : "开发"} 回复缺少 [Verification evidence] 块(契约未兑现,已记录不中断)`
|
|
3184
|
+
});
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
if (stage) stage.verifyEvidence = ev;
|
|
3188
|
+
} catch (e) {}
|
|
3189
|
+
};
|
|
2875
3190
|
let prd = null;
|
|
2876
|
-
if (resumed("
|
|
3191
|
+
if (resumed("prd")) {
|
|
2877
3192
|
prd = resume.products.prd;
|
|
2878
3193
|
timeline.prd = prd;
|
|
2879
|
-
logSkip(
|
|
3194
|
+
logSkip(PHASE_KEY_OF.prd);
|
|
2880
3195
|
} else {
|
|
2881
3196
|
journal.logs.push({
|
|
2882
3197
|
t: Date.now(),
|
|
@@ -2893,8 +3208,8 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2893
3208
|
label: "产品经理 · 梳理 PRD",
|
|
2894
3209
|
fn: prdPrompt
|
|
2895
3210
|
};
|
|
2896
|
-
const prdR = await withRetry(journal, parent, pForm.label, "
|
|
2897
|
-
if (!prdR.text) throw stageFailError("
|
|
3211
|
+
const prdR = await withRetry(journal, parent, pForm.label, "prd", pForm.fn(requirement, root, journal.id, state), signal);
|
|
3212
|
+
if (!prdR.text) throw stageFailError("prd", prdR);
|
|
2898
3213
|
prd = prdR.text;
|
|
2899
3214
|
timeline.prd = prd;
|
|
2900
3215
|
mergeStageState("prd", prd);
|
|
@@ -2903,18 +3218,18 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2903
3218
|
}
|
|
2904
3219
|
let design = null;
|
|
2905
3220
|
if (enabled("design")) {
|
|
2906
|
-
if (resumed("
|
|
3221
|
+
if (resumed("design")) {
|
|
2907
3222
|
design = resume.products.design;
|
|
2908
3223
|
timeline.design = design;
|
|
2909
|
-
logSkip(
|
|
3224
|
+
logSkip(PHASE_KEY_OF.design);
|
|
2910
3225
|
} else {
|
|
2911
3226
|
journal.logs.push({
|
|
2912
3227
|
t: Date.now(),
|
|
2913
3228
|
level: "phase",
|
|
2914
3229
|
message: "进入阶段:UI/UX 设计"
|
|
2915
3230
|
});
|
|
2916
|
-
const designR = await withRetry(journal, parent, "UI/UX 设计师 · 设计说明", "
|
|
2917
|
-
if (!designR.text) throw stageFailError("
|
|
3231
|
+
const designR = await withRetry(journal, parent, "UI/UX 设计师 · 设计说明", "design", designPrompt(prd, root, journal.id, state), signal);
|
|
3232
|
+
if (!designR.text) throw stageFailError("design", designR);
|
|
2918
3233
|
design = designR.text;
|
|
2919
3234
|
timeline.design = design;
|
|
2920
3235
|
mergeStageState("design", design);
|
|
@@ -2924,18 +3239,18 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2924
3239
|
}
|
|
2925
3240
|
let scaffold = null;
|
|
2926
3241
|
if (enabled("scaffold")) {
|
|
2927
|
-
if (resumed("
|
|
3242
|
+
if (resumed("scaffold")) {
|
|
2928
3243
|
scaffold = resume.products.scaffold;
|
|
2929
3244
|
timeline.scaffold = scaffold;
|
|
2930
|
-
logSkip(
|
|
3245
|
+
logSkip(PHASE_KEY_OF.scaffold);
|
|
2931
3246
|
} else {
|
|
2932
3247
|
journal.logs.push({
|
|
2933
3248
|
t: Date.now(),
|
|
2934
3249
|
level: "phase",
|
|
2935
3250
|
message: "进入阶段:架构规划"
|
|
2936
3251
|
});
|
|
2937
|
-
const scR = await withRetry(journal, parent, "架构师 · 脚手架规划与落地", "
|
|
2938
|
-
if (!scR.text) throw stageFailError("
|
|
3252
|
+
const scR = await withRetry(journal, parent, "架构师 · 脚手架规划与落地", "scaffold", scaffoldPrompt(requirement, design, root, journal.id, state), signal);
|
|
3253
|
+
if (!scR.text) throw stageFailError("scaffold", scR);
|
|
2939
3254
|
scaffold = scR.text;
|
|
2940
3255
|
timeline.scaffold = scaffold;
|
|
2941
3256
|
mergeStageState("scaffold", scaffold);
|
|
@@ -2945,10 +3260,10 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2945
3260
|
}
|
|
2946
3261
|
let tech = null;
|
|
2947
3262
|
if (enabled("tech")) {
|
|
2948
|
-
if (resumed("
|
|
3263
|
+
if (resumed("tech")) {
|
|
2949
3264
|
tech = resume.products.tech;
|
|
2950
3265
|
timeline.tech = tech;
|
|
2951
|
-
logSkip(
|
|
3266
|
+
logSkip(PHASE_KEY_OF.tech);
|
|
2952
3267
|
} else {
|
|
2953
3268
|
const isHeavy = !options.lite && options.mode !== "tech" && options.mode !== "patch";
|
|
2954
3269
|
journal.logs.push({
|
|
@@ -2957,7 +3272,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2957
3272
|
message: isHeavy ? "进入阶段:技术方案" : "进入阶段:架构蓝图"
|
|
2958
3273
|
});
|
|
2959
3274
|
const label = isHeavy ? "高级全栈工程师 · 技术方案" : "架构师 · 架构蓝图";
|
|
2960
|
-
const techR = await withRetry(journal, parent, label, "
|
|
3275
|
+
const techR = await withRetry(journal, parent, label, "tech", isHeavy ? techPrompt(prd, design, scaffold, tasks, root, journal.id, state) : architectPrompt(prd, root, journal.id, state), signal);
|
|
2961
3276
|
if (!techR.text) throw stageFailError(label, techR);
|
|
2962
3277
|
tech = techR.text;
|
|
2963
3278
|
timeline.tech = tech;
|
|
@@ -2990,28 +3305,28 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2990
3305
|
}
|
|
2991
3306
|
}
|
|
2992
3307
|
let devResults = null;
|
|
2993
|
-
if (
|
|
3308
|
+
if (resume) {
|
|
2994
3309
|
devResults = resume.products.dev || [];
|
|
2995
|
-
const
|
|
2996
|
-
|
|
2997
|
-
const
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
};
|
|
3006
|
-
}).filter(Boolean);
|
|
3007
|
-
const reused = devResults.filter((r) => r && !rerunDefs.some((d) => d.title === r.title));
|
|
3310
|
+
const taskStatuses = devTaskStatuses(journal.stages || []);
|
|
3311
|
+
const todo = buildDevTaskDefs(journal, tasks).filter((d) => {
|
|
3312
|
+
const st = taskStatuses.get(String(d.title || "").trim());
|
|
3313
|
+
return !st || !st.done;
|
|
3314
|
+
});
|
|
3315
|
+
if (todo.length === 0) {
|
|
3316
|
+
timeline.dev = devResults;
|
|
3317
|
+
logSkip("开发");
|
|
3318
|
+
} else {
|
|
3319
|
+
const reused = devResults.filter((r) => r && !todo.some((d) => d.title === r.title));
|
|
3008
3320
|
journal.logs.push({
|
|
3009
3321
|
t: Date.now(),
|
|
3010
3322
|
level: "warn",
|
|
3011
|
-
message: `断点续跑开发:复用 ${reused.length} 个已完成任务,补跑 ${
|
|
3323
|
+
message: `断点续跑开发:复用 ${reused.length} 个已完成任务,补跑 ${todo.length} 个失败任务`
|
|
3012
3324
|
});
|
|
3013
|
-
const rerun = await runPool(
|
|
3014
|
-
const
|
|
3325
|
+
const rerun = await runPool(todo, maxConcurrency, async (task) => {
|
|
3326
|
+
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
|
+
const resumePrompt = devPrompt(task, tech, prd, root, journal.id, state) + (prevStage ? buildRetryDiagnostic(2, prevStage) : "");
|
|
3328
|
+
const devR = await withRetry(journal, parent, `开发 · ${task.title}(补跑)`, "dev", resumePrompt, signal, task.title);
|
|
3329
|
+
noteVerifyEvidence(devR.stage, devR.text);
|
|
3015
3330
|
const ok = !!devR.text;
|
|
3016
3331
|
return {
|
|
3017
3332
|
title: task.title,
|
|
@@ -3020,14 +3335,11 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3020
3335
|
};
|
|
3021
3336
|
});
|
|
3022
3337
|
for (const t of rerun) {
|
|
3023
|
-
const sub =
|
|
3338
|
+
const sub = createSubtask(journal, t.title, t.spec || "");
|
|
3024
3339
|
if (sub) completeSubtask(journal, sub.id, t.failed, t.output ? snippet(t.output, 1e3) : null, null);
|
|
3025
3340
|
}
|
|
3026
3341
|
devResults = [...reused, ...rerun];
|
|
3027
3342
|
timeline.dev = devResults;
|
|
3028
|
-
} else {
|
|
3029
|
-
timeline.dev = devResults;
|
|
3030
|
-
logSkip("开发");
|
|
3031
3343
|
}
|
|
3032
3344
|
} else {
|
|
3033
3345
|
journal.logs.push({
|
|
@@ -3069,12 +3381,12 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3069
3381
|
persistJournal(journal);
|
|
3070
3382
|
}
|
|
3071
3383
|
}
|
|
3072
|
-
const devR = await withRetry(journal, parent, `开发 · ${task.title}`, "
|
|
3384
|
+
const devR = await withRetry(journal, parent, `开发 · ${task.title}`, "dev", devPrompt(task, tech, prd, root, journal.id, state), signal, task.title);
|
|
3385
|
+
noteVerifyEvidence(devR.stage, devR.text);
|
|
3073
3386
|
const ok = !!devR.text;
|
|
3074
3387
|
if (sub) {
|
|
3075
3388
|
completeSubtask(journal, sub.id, !ok, devR.text ? snippet(devR.text, 1e3) : null, null);
|
|
3076
|
-
|
|
3077
|
-
if (devStage) noteSubtaskUsage(journal, sub.id, devStage);
|
|
3389
|
+
if (devR.stage) noteSubtaskUsage(journal, sub.id, devR.stage);
|
|
3078
3390
|
}
|
|
3079
3391
|
return {
|
|
3080
3392
|
title: task.title,
|
|
@@ -3085,7 +3397,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3085
3397
|
timeline.dev = devResults;
|
|
3086
3398
|
for (const r of devResults) if (r && r.output) mergeStageState("dev", r.output);
|
|
3087
3399
|
noteTaskStageUsage(journal);
|
|
3088
|
-
noteTaskAssign(journal, "dev", journal.stages.filter((s) => s.phase === "
|
|
3400
|
+
noteTaskAssign(journal, "dev", journal.stages.filter((s) => phaseKeyOf(s.phase) === "dev").map((s) => (s.childId || "").slice(0, 8)).filter(Boolean).join(",") || "开发组");
|
|
3089
3401
|
const failedCount = devResults.filter((r) => r && r.failed).length;
|
|
3090
3402
|
if (failedCount > 0) {
|
|
3091
3403
|
advanceTask(journal, "needs-human", null, "开发失败,需人工介入", { by: "dev" });
|
|
@@ -3120,10 +3432,10 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3120
3432
|
message: "当前档位阶段集不含独立 QA:跳过(单点修复,开发自测兜底)"
|
|
3121
3433
|
});
|
|
3122
3434
|
qa = "(独立 QA 跳过:当前档位由开发自测兜底)";
|
|
3123
|
-
} else if (resumed("
|
|
3124
|
-
qa = resume.products.qa;
|
|
3435
|
+
} else if (resumed("qa") && !hasOpenBlockingBugs(journal)) {
|
|
3436
|
+
qa = artifactText(journal, "QA-REPORT.md") || resume.products.qa;
|
|
3125
3437
|
timeline.qa = qa;
|
|
3126
|
-
logSkip(
|
|
3438
|
+
logSkip(PHASE_KEY_OF.qa);
|
|
3127
3439
|
} else {
|
|
3128
3440
|
journal.logs.push({
|
|
3129
3441
|
t: Date.now(),
|
|
@@ -3132,7 +3444,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3132
3444
|
});
|
|
3133
3445
|
advanceTask(journal, "testing", null, "QA 开始(待测试 → 测试中)", { by: "qa" });
|
|
3134
3446
|
const store = storeFor(scopeKey);
|
|
3135
|
-
const qaStageChildren = () => journal.stages.filter((s) => s.phase === "
|
|
3447
|
+
const qaStageChildren = () => journal.stages.filter((s) => phaseKeyOf(s.phase) === "qa").map((s) => (s.childId || "").slice(0, 8)).filter(Boolean).join(",") || "测试组";
|
|
3136
3448
|
let round = 0;
|
|
3137
3449
|
let qaClean = false;
|
|
3138
3450
|
const devFixRounds = [];
|
|
@@ -3144,14 +3456,26 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3144
3456
|
do {
|
|
3145
3457
|
round += 1;
|
|
3146
3458
|
const isReverify = round > 1;
|
|
3147
|
-
const qaR = await withRetry(journal, parent, isReverify ? `QA 复验 · 第${round - 1}轮修复后` : "QA 测试工程师 · 功能测试", "
|
|
3459
|
+
const qaR = await withRetry(journal, parent, isReverify ? `QA 复验 · 第${round - 1}轮修复后` : "QA 测试工程师 · 功能测试", "qa", qaPrompt(prd, qaDevSummary(), root, journal.id, state, await currentModelSupportsVision(resolveChildRoute(parent).provider, resolveChildRoute(parent).model)), signal);
|
|
3148
3460
|
if (!qaR.text) {
|
|
3149
3461
|
advanceTask(journal, "needs-human", null, isReverify ? `QA 复验失败(第 ${round - 1} 轮修复后)` : "QA 失败", { by: "qa" });
|
|
3150
|
-
throw stageFailError(
|
|
3462
|
+
throw stageFailError("qa", qaR);
|
|
3463
|
+
}
|
|
3464
|
+
mergeStageState("qa", qaR.text);
|
|
3465
|
+
qa = artifactText(journal, "QA-REPORT.md");
|
|
3466
|
+
if (!qa) {
|
|
3467
|
+
journal.logs.push({
|
|
3468
|
+
t: Date.now(),
|
|
3469
|
+
level: "error",
|
|
3470
|
+
message: `QA 子代理回复成功但 ${journal.runDocs ? journal.runDocs + "/" : ""}QA-REPORT.md 未落盘/为空——单轨契约(文件即产物)未兑现,需人工介入`
|
|
3471
|
+
});
|
|
3472
|
+
advanceTask(journal, "needs-human", null, "QA-REPORT.md 未落盘(单轨契约未兑现)", { by: "qa" });
|
|
3473
|
+
throw stageFailError("qa", {
|
|
3474
|
+
attempts: qaR.attempts,
|
|
3475
|
+
stageTokens: qaR.stageTokens
|
|
3476
|
+
});
|
|
3151
3477
|
}
|
|
3152
|
-
qa = qaR.text;
|
|
3153
3478
|
timeline.qa = qa;
|
|
3154
|
-
mergeStageState("qa", qa);
|
|
3155
3479
|
noteTaskStageUsage(journal);
|
|
3156
3480
|
noteTaskAssign(journal, "qa", qaStageChildren());
|
|
3157
3481
|
defects = parseDefects(qa);
|
|
@@ -3189,7 +3513,8 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3189
3513
|
message: `QA 发现 ${blocking.length} 个阻断缺陷(第 ${round} 轮),打回开发确认修复后复验`
|
|
3190
3514
|
});
|
|
3191
3515
|
advanceTask(journal, "rework", snippet(qa, 3e3), `QA 打回开发修复(第 ${round}/3 轮)`, { by: "qa" });
|
|
3192
|
-
const fixR = await withRetry(journal, parent, `开发 · QA 缺陷修复(第 ${round} 轮)`, "
|
|
3516
|
+
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.text);
|
|
3193
3518
|
if (!fixR.text) {
|
|
3194
3519
|
advanceTask(journal, "needs-human", null, "QA 打回后开发修复失败", { by: "qa" });
|
|
3195
3520
|
throw stageFailError("开发(QA 打回修复)", fixR);
|
|
@@ -3239,18 +3564,47 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3239
3564
|
const curTask = storeFor(scopeKey).find("task", journal.taskId);
|
|
3240
3565
|
if (curTask && curTask.status !== "pending-acceptance" && curTask.status !== "needs-human" && curTask.status !== "rework") advanceTask(journal, "pending-acceptance", null, "进入验收(待验收)", { by: "pm" });
|
|
3241
3566
|
}
|
|
3242
|
-
const accR = await withRetry(journal, parent, "产品经理 · 最终验收", "
|
|
3567
|
+
const accR = await withRetry(journal, parent, "产品经理 · 最终验收", "acceptance", acceptancePrompt(prd, qa, JSON.stringify(timeline.dev), root, journal.id, state, await currentModelSupportsVision(resolveChildRoute(parent).provider, resolveChildRoute(parent).model)), signal);
|
|
3243
3568
|
if (!accR.text) {
|
|
3244
3569
|
advanceTask(journal, "needs-human", null, "验收失败", { by: "pm" });
|
|
3245
|
-
throw stageFailError("
|
|
3570
|
+
throw stageFailError("acceptance", accR);
|
|
3571
|
+
}
|
|
3572
|
+
mergeStageState("acceptance", accR.text);
|
|
3573
|
+
const acceptance = artifactText(journal, "ACCEPTANCE.md");
|
|
3574
|
+
if (!acceptance) {
|
|
3575
|
+
journal.logs.push({
|
|
3576
|
+
t: Date.now(),
|
|
3577
|
+
level: "error",
|
|
3578
|
+
message: `验收子代理回复成功但 ${journal.runDocs ? journal.runDocs + "/" : ""}ACCEPTANCE.md 未落盘/为空——单轨契约(文件即产物)未兑现,需人工介入`
|
|
3579
|
+
});
|
|
3580
|
+
advanceTask(journal, "needs-human", null, "ACCEPTANCE.md 未落盘(单轨契约未兑现)", { by: "pm" });
|
|
3581
|
+
throw stageFailError("acceptance", {
|
|
3582
|
+
attempts: accR.attempts,
|
|
3583
|
+
stageTokens: accR.stageTokens
|
|
3584
|
+
});
|
|
3246
3585
|
}
|
|
3247
|
-
const acceptance = accR.text;
|
|
3248
3586
|
timeline.acceptance = acceptance;
|
|
3249
3587
|
noteTaskStageUsage(journal);
|
|
3250
|
-
const accStage = journal.stages.find((s) => s.phase === "
|
|
3588
|
+
const accStage = journal.stages.find((s) => phaseKeyOf(s.phase) === "acceptance" && s.childId);
|
|
3251
3589
|
noteTaskAssign(journal, "accept", accStage ? String(accStage.childId).slice(0, 8) : "验收组");
|
|
3252
|
-
mergeStageState("acceptance", acceptance);
|
|
3253
3590
|
const accVerdict = parseAcceptanceVerdict(acceptance);
|
|
3591
|
+
if (accVerdict === "needs-human") {
|
|
3592
|
+
journal.logs.push({
|
|
3593
|
+
t: Date.now(),
|
|
3594
|
+
level: "error",
|
|
3595
|
+
message: "ACCEPTANCE.md 缺少「验收结论:」行(字面量模板未兑现)——不自动判通过,需人工确认结论"
|
|
3596
|
+
});
|
|
3597
|
+
advanceTask(journal, "needs-human", snippet(acceptance, 3e3), "ACCEPTANCE.md 缺少验收结论行(契约未兑现),需人工确认", { by: "pm" });
|
|
3598
|
+
const store = storeFor(scopeKey);
|
|
3599
|
+
const req = store.find("req", journal.reqId);
|
|
3600
|
+
if (req) {
|
|
3601
|
+
req.humanIntervention = true;
|
|
3602
|
+
store.pushEvent(req, req.status, "needs-human", "验收结论行缺失,需人工确认");
|
|
3603
|
+
}
|
|
3604
|
+
journal.humanIntervention = true;
|
|
3605
|
+
persistJournal(journal);
|
|
3606
|
+
throw new Error("ACCEPTANCE.md 缺少验收结论行,需人工确认结论");
|
|
3607
|
+
}
|
|
3254
3608
|
if (accVerdict === "reject") {
|
|
3255
3609
|
advanceTask(journal, "needs-human", snippet(acceptance, 3e3), "需求与现状不符(无需改动),需人工决定调整或取消需求", { by: "pm" });
|
|
3256
3610
|
const store = storeFor(scopeKey);
|
|
@@ -4295,8 +4649,7 @@ function tryFlushPendingInjections(sessionId) {
|
|
|
4295
4649
|
const agent = runtime.agents ? runtime.agents.get(sessionId) : void 0;
|
|
4296
4650
|
if (!agent || typeof agent.inject !== "function") return;
|
|
4297
4651
|
try {
|
|
4298
|
-
agent.inject({
|
|
4299
|
-
type: "user",
|
|
4652
|
+
agent.inject(createUserMessage({
|
|
4300
4653
|
content: [{
|
|
4301
4654
|
type: "text",
|
|
4302
4655
|
text: teamflowContextText(pending.teamIcon, pending.teamName, pending.teamId)
|
|
@@ -4304,9 +4657,9 @@ function tryFlushPendingInjections(sessionId) {
|
|
|
4304
4657
|
source: {
|
|
4305
4658
|
kind: "plugin",
|
|
4306
4659
|
plugin: "dsh-plugin-teamflow",
|
|
4307
|
-
form: "
|
|
4660
|
+
form: "instructions"
|
|
4308
4661
|
}
|
|
4309
|
-
});
|
|
4662
|
+
}));
|
|
4310
4663
|
pendingInjections.delete(sessionId);
|
|
4311
4664
|
} catch (e) {}
|
|
4312
4665
|
}
|
|
@@ -4379,7 +4732,9 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4379
4732
|
const j = runs.get(latest.id);
|
|
4380
4733
|
return j ? snapshotOf(j) : null;
|
|
4381
4734
|
}
|
|
4382
|
-
/** 阶段详情:卡片点击查看 —— 状态/耗时/官方 usage + 产物全文(超 24k 截断)。
|
|
4735
|
+
/** 阶段详情:卡片点击查看 —— 状态/耗时/官方 usage + 产物全文(超 24k 截断)。
|
|
4736
|
+
* 2026-09-06 状态机化:返回同任务全部尝试(attempts 聚合——按 stage.taskKey(旧数据 label 兜底),
|
|
4737
|
+
* 按 seq 排序)——client 弹窗单次渲染现状、多次渲染时间线。 */
|
|
4383
4738
|
stageDetail(runId, seq, sessionId) {
|
|
4384
4739
|
if (typeof runId !== "string" || !runId || seq === void 0 || seq === null) return null;
|
|
4385
4740
|
const sc = sessionScope(sessionId);
|
|
@@ -4388,6 +4743,21 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4388
4743
|
if (j.workspace && sc.projectKey && j.workspace !== sc.projectKey && sc.projectKey !== "default") return null;
|
|
4389
4744
|
const s = (j.stages || []).find((st) => Number(st.seq) === Number(seq));
|
|
4390
4745
|
if (!s) return null;
|
|
4746
|
+
const taskKeyOf = (x) => String(x.taskKey || String(x.label || "").replace(/^开发 · /, "").replace(/((?:第 \d+ 次重试|补跑))$/, "").trim());
|
|
4747
|
+
const taskKey = taskKeyOf(s);
|
|
4748
|
+
const attempts = taskKey ? (j.stages || []).filter((x) => phaseKeyOf(x.phase) === phaseKeyOf(s.phase) && taskKeyOf(x) === taskKey).sort((a, b) => Number(a.seq) - Number(b.seq)).map((x) => ({
|
|
4749
|
+
seq: x.seq,
|
|
4750
|
+
label: x.label,
|
|
4751
|
+
status: x.status,
|
|
4752
|
+
outcome: x.outcome || null,
|
|
4753
|
+
summary: clip(x.summary || "", 1500),
|
|
4754
|
+
output: clip(toText(x.output) || toText(x.handoff) || "", 12e3),
|
|
4755
|
+
usage: x.usage || null,
|
|
4756
|
+
verifyEvidence: x.verifyEvidence || null,
|
|
4757
|
+
childId: x.childId || null,
|
|
4758
|
+
startedAt: x.startedAt,
|
|
4759
|
+
endedAt: x.endedAt
|
|
4760
|
+
})) : null;
|
|
4391
4761
|
return {
|
|
4392
4762
|
seq: s.seq,
|
|
4393
4763
|
label: s.label,
|
|
@@ -4399,8 +4769,10 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4399
4769
|
endedAt: s.endedAt,
|
|
4400
4770
|
ownerSession: j.ownerSession || null,
|
|
4401
4771
|
usage: s.usage || null,
|
|
4772
|
+
verifyEvidence: s.verifyEvidence || null,
|
|
4402
4773
|
summary: clip(s.summary || "", 3e3),
|
|
4403
|
-
output: clip(toText(s.output) || toText(s.handoff) || "", 24e3)
|
|
4774
|
+
output: clip(toText(s.output) || toText(s.handoff) || "", 24e3),
|
|
4775
|
+
attempts
|
|
4404
4776
|
};
|
|
4405
4777
|
}
|
|
4406
4778
|
/** Backlog 条目详情:卡片点击查看 —— 完整字段 + 流转时间线 + 关联(子卡/缺陷)+ 任务夹路径。 */
|
|
@@ -4625,8 +4997,7 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4625
4997
|
activeTeams.set(sid, tid);
|
|
4626
4998
|
saveActiveTeams();
|
|
4627
4999
|
const agent = runtime.agents && runtime.agents.get(sid);
|
|
4628
|
-
const injectPayload = {
|
|
4629
|
-
type: "user",
|
|
5000
|
+
const injectPayload = createUserMessage({
|
|
4630
5001
|
content: [{
|
|
4631
5002
|
type: "text",
|
|
4632
5003
|
text: teamflowContextText(team.icon, team.name, tid)
|
|
@@ -4634,9 +5005,9 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4634
5005
|
source: {
|
|
4635
5006
|
kind: "plugin",
|
|
4636
5007
|
plugin: "dsh-plugin-teamflow",
|
|
4637
|
-
form: "
|
|
5008
|
+
form: "instructions"
|
|
4638
5009
|
}
|
|
4639
|
-
};
|
|
5010
|
+
});
|
|
4640
5011
|
if (agent && typeof agent.inject === "function") try {
|
|
4641
5012
|
agent.inject(injectPayload);
|
|
4642
5013
|
} catch (e) {}
|