dsh-plugin-teamflow 0.1.4 → 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 +39 -0
- package/README.md +10 -0
- package/lib/client.js +173 -18
- package/lib/host.mjs +693 -337
- 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: [
|
|
@@ -157,9 +179,7 @@ const STAGE_POLICY = {
|
|
|
157
179
|
key: "scaffold",
|
|
158
180
|
when: (o) => !!o.needScaffold
|
|
159
181
|
},
|
|
160
|
-
{ key: "
|
|
161
|
-
{ key: "dev" },
|
|
162
|
-
{ key: "acceptance" }
|
|
182
|
+
{ key: "dev" }
|
|
163
183
|
]
|
|
164
184
|
};
|
|
165
185
|
/** 按档位 + 条件展开实际执行阶段集(纯函数;未知档回退 full)。 */
|
|
@@ -187,6 +207,17 @@ function extractText(blocks) {
|
|
|
187
207
|
if (!Array.isArray(blocks)) return "";
|
|
188
208
|
return blocks.filter((b) => b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
|
|
189
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
|
+
}
|
|
190
221
|
/**
|
|
191
222
|
* ADR-0008 任务夹命名:<yyyyMMdd>-r<N>[-<slug>]。
|
|
192
223
|
* - date 用本地时区(用户在东八区晚上建的需求不能落到"明天")
|
|
@@ -255,10 +286,11 @@ function sanitizeSnapOptions(o) {
|
|
|
255
286
|
const SAFE_SIGNAL = {
|
|
256
287
|
aborted: false,
|
|
257
288
|
addEventListener: () => {},
|
|
258
|
-
removeEventListener: () => {}
|
|
289
|
+
removeEventListener: () => {},
|
|
290
|
+
throwIfAborted: () => {}
|
|
259
291
|
};
|
|
260
292
|
function normalizeSignal(s) {
|
|
261
|
-
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;
|
|
262
294
|
}
|
|
263
295
|
/** 分支 slug 派生(ADR-2026-08-27):branchName > triageSlug > 需求中的英文标识词 > reqId 数字 > 'feature'。
|
|
264
296
|
* 实锤 feat/feature:lite 显式时 triage 不跑(无 slug)+ 分支检查早于 reqId 生成 → fallback 'feature'。 */
|
|
@@ -266,7 +298,24 @@ function deriveBranchSlug(requirement, reqId, triageSlug, branchName) {
|
|
|
266
298
|
if (branchName && /^[a-z0-9][a-z0-9-_]*$/i.test(branchName)) return String(branchName).replace(/[^a-z0-9-]/gi, "-").toLowerCase().slice(0, 40);
|
|
267
299
|
if (triageSlug && /^[a-z0-9-]{3,24}$/i.test(triageSlug)) return triageSlug;
|
|
268
300
|
const en = String(requirement || "").match(/[a-zA-Z][a-zA-Z0-9-]{2,23}/g);
|
|
269
|
-
if (en && en.length)
|
|
301
|
+
if (en && en.length) {
|
|
302
|
+
const NOISE = /* @__PURE__ */ new Set([
|
|
303
|
+
"patch",
|
|
304
|
+
"lite",
|
|
305
|
+
"tech",
|
|
306
|
+
"full",
|
|
307
|
+
"medium",
|
|
308
|
+
"mode",
|
|
309
|
+
"the",
|
|
310
|
+
"and",
|
|
311
|
+
"for",
|
|
312
|
+
"with",
|
|
313
|
+
"use",
|
|
314
|
+
"using"
|
|
315
|
+
]);
|
|
316
|
+
const hit = en.find((w) => !NOISE.has(w.toLowerCase()));
|
|
317
|
+
if (hit) return hit.toLowerCase().slice(0, 40);
|
|
318
|
+
}
|
|
270
319
|
const num = String(reqId || "").match(/\d+/);
|
|
271
320
|
if (num) return `r${num[0]}`;
|
|
272
321
|
return "feature";
|
|
@@ -290,6 +339,33 @@ function handoffBrief(text) {
|
|
|
290
339
|
const m = String(text).match(/<!--\s*handoff\s*-->([\s\S]*?)(?:<!--\s*\/handoff\s*-->|$)/);
|
|
291
340
|
return clip((m && m[1] ? m[1] : String(text)).trim(), 2e3);
|
|
292
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
|
+
}
|
|
293
369
|
/**
|
|
294
370
|
* 验收结论解析:只以显式「验收结论 / 整体结论」行为准(acceptancePrompt 强制 4 档固定话术),
|
|
295
371
|
* 不做正文散文朴素子串匹配。历史误报实锤(run tf-msytlok5):验收报告 ✅ 通过,其记忆回写段一句
|
|
@@ -298,8 +374,11 @@ function handoffBrief(text) {
|
|
|
298
374
|
* - 「📝 需求不适用」是验收负责人专用的强结论词,允许全文命中;
|
|
299
375
|
* - 其余 reject 词(需求与实际不符/站不住/无效/无需改动等)仅在结论行且该行不含「通过/✅/⚠️」时才算;
|
|
300
376
|
* - rework 词仅认结论行(且不与「✅ 通过」同现)。
|
|
377
|
+
* 反向护栏(漏报实锤 2026-09-03):模型写「❌ 不通过」但漏写「验收结论:」前缀 → accLine 为空 →
|
|
378
|
+
* 旧实现落回默认 accepted(最乐观默认值,质量门禁漏报=假交付)。现改为 **找不到结论行 → needs-human**
|
|
379
|
+
* (宁严勿松:误拦截=人工看一眼,误放行=假交付;📝 全文命中与架构红词仍优先于该默认)。
|
|
301
380
|
* @param {unknown} text 验收报告全文
|
|
302
|
-
* @returns {'accepted'|'rework'|'reject'}
|
|
381
|
+
* @returns {'accepted'|'rework'|'reject'|'needs-human'}
|
|
303
382
|
*/
|
|
304
383
|
function parseAcceptanceVerdict(text) {
|
|
305
384
|
const acc = String(text || "");
|
|
@@ -307,9 +386,14 @@ function parseAcceptanceVerdict(text) {
|
|
|
307
386
|
const hasArchRedFlag = /重复实现|重复适配|偏离蓝图|未按蓝图|该拆未拆|该抽象未抽象|破坏既有结构|结构性.*问题|架构(打回|需重构)|需.*返工|返工.*项.*(存在|仍)|仍.*(返工|重构)/.test(acc);
|
|
308
387
|
const archNegated = /无返工|无.*返工|不返工|无架构打回|无.*打回|非漂移|无.*重复|无.*偏离|无.*抽象.*问题|无.*蓝图.*问题|架构一致性.*(PASS|良好|达标|通过|无问题)|M3.*(PASS|通过|达标)|架构.*(达标|无问题|良好)/.test(acc);
|
|
309
388
|
if (hasArchRedFlag && !archNegated) return "rework";
|
|
310
|
-
if (/❌\s*不通过|需返工|未通过/.test(accLine) && !/✅\s*通过/.test(accLine)) return "rework";
|
|
311
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
|
+
}
|
|
312
394
|
if (!/通过|✅|⚠️/.test(accLine) && /需求不适用|需求与实际不符|需求站不住|需求无效|无需改动|无需修改/.test(accLine)) return "reject";
|
|
395
|
+
if (!accLine) return "needs-human";
|
|
396
|
+
if (!/通过|✅|⚠️|❌|📝/.test(accLine)) return "needs-human";
|
|
313
397
|
return "accepted";
|
|
314
398
|
}
|
|
315
399
|
const bdOpen = "<!-- blueprint -->";
|
|
@@ -819,6 +903,20 @@ function createSubtask(journal, title, spec) {
|
|
|
819
903
|
const store = storeFor(journal.workspace || "default");
|
|
820
904
|
const mainTask = journal.taskId ? store.find("task", journal.taskId) : null;
|
|
821
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
|
+
}
|
|
822
920
|
const id = store.nextId("dev");
|
|
823
921
|
const sub = {
|
|
824
922
|
id,
|
|
@@ -826,7 +924,8 @@ function createSubtask(journal, title, spec) {
|
|
|
826
924
|
parentId: journal.taskId,
|
|
827
925
|
product: journal.workspace || "default",
|
|
828
926
|
type: "subtask",
|
|
829
|
-
title:
|
|
927
|
+
title: fullTitle,
|
|
928
|
+
taskKey: title,
|
|
830
929
|
spec: spec || "",
|
|
831
930
|
status: "pending",
|
|
832
931
|
devAssign: mainTask && mainTask.devAssign || null,
|
|
@@ -1155,16 +1254,59 @@ function runSanityCheck(path) {
|
|
|
1155
1254
|
//#endregion
|
|
1156
1255
|
//#region host/core/metering.ts
|
|
1157
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
|
+
/**
|
|
1158
1304
|
* 累计子代理会话中所有 LLM 调用的真实 usage(官方三桶 + 调用数)。
|
|
1159
1305
|
* 返回 null 表示拿不到 usage(会话未暴露 events / 无数据)。
|
|
1160
1306
|
*/
|
|
1161
1307
|
function accumulateSessionUsage(run) {
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
if (!session) return null;
|
|
1165
|
-
const rawEvents = session.events;
|
|
1166
|
-
const events = Array.isArray(rawEvents) ? rawEvents : typeof rawEvents === "function" ? rawEvents() : null;
|
|
1167
|
-
if (!Array.isArray(events)) return null;
|
|
1308
|
+
const events = sessionEventsOf(run);
|
|
1309
|
+
if (events.length === 0) return null;
|
|
1168
1310
|
const buckets = {
|
|
1169
1311
|
input: 0,
|
|
1170
1312
|
cacheRead: 0,
|
|
@@ -1178,7 +1320,7 @@ function accumulateSessionUsage(run) {
|
|
|
1178
1320
|
if (!e || e.type !== "assistant/message") continue;
|
|
1179
1321
|
const d = e.data || {};
|
|
1180
1322
|
if (typeof d.turn === "number" && typeof d.step === "number") seen.add(`${d.turn}.${d.step}`);
|
|
1181
|
-
const u =
|
|
1323
|
+
const u = usageOfEvent(e);
|
|
1182
1324
|
if (u) {
|
|
1183
1325
|
buckets.input += u.inputTokens || 0;
|
|
1184
1326
|
buckets.cacheRead += u.cacheReadTokens || 0;
|
|
@@ -1225,14 +1367,48 @@ function totalTokensOf(usage) {
|
|
|
1225
1367
|
* 纯 read 循环(反复整读同一文件却无变更/无脚本执行)= 真退化。实锤 run tf-mte906e9:QA 重跑
|
|
1226
1368
|
* 只读分析(不 edit)→ 旧判定「零变更进展」误杀,第 2 次 provider error 后 450k 熔断。 */
|
|
1227
1369
|
const PROGRESS_TOOLS = /^(edit|write|create|apply_patch|patch|remove|delete|rm|mkdir|move|rename|append|bash|pwsh|shell|powershell)$/i;
|
|
1228
|
-
/**
|
|
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())。 */
|
|
1229
1388
|
function eventsOf(run) {
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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
|
+
}
|
|
1236
1412
|
}
|
|
1237
1413
|
/** 规范化文本片段:小写 + 仅保留字母数字/CJK,供逐字重复比对。 */
|
|
1238
1414
|
function normalizeFragment(s) {
|
|
@@ -1289,6 +1465,7 @@ function startStageGuard(opts) {
|
|
|
1289
1465
|
const warnedScripts = /* @__PURE__ */ new Set();
|
|
1290
1466
|
let lastMutationAt = 0;
|
|
1291
1467
|
let repeatWarned = false;
|
|
1468
|
+
let busyWarned = false;
|
|
1292
1469
|
function warnOnce(key, set, message, hint) {
|
|
1293
1470
|
if (set.has(key)) return;
|
|
1294
1471
|
set.add(key);
|
|
@@ -1307,6 +1484,34 @@ function startStageGuard(opts) {
|
|
|
1307
1484
|
clearInterval(timer);
|
|
1308
1485
|
stage.guardReason = reason;
|
|
1309
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) {}
|
|
1310
1515
|
try {
|
|
1311
1516
|
journal.logs.push({
|
|
1312
1517
|
t: Date.now(),
|
|
@@ -1380,8 +1585,22 @@ function startStageGuard(opts) {
|
|
|
1380
1585
|
lastEventCount = events.length;
|
|
1381
1586
|
lastGrowthAt = Date.now();
|
|
1382
1587
|
} else if (Date.now() - lastGrowthAt > 6e5) {
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
+
}
|
|
1385
1604
|
}
|
|
1386
1605
|
if (seenToolCall && Date.now() - lastToolSignalAt > 9e5) {
|
|
1387
1606
|
fire(`空转(${Math.round(GUARD_NO_TOOL_MS / 6e4)} 分钟内无任何工具调用,但会话仍在产出)`, "stalled");
|
|
@@ -1483,7 +1702,7 @@ function resolveChildRoute(parent) {
|
|
|
1483
1702
|
return out;
|
|
1484
1703
|
}
|
|
1485
1704
|
/** 运行单个阶段子代理:执行 + 产出实质校验 + token 双口径计量 + stage 状态流转。 */
|
|
1486
|
-
async function runAgent(journal, parent, label, phase, prompt, signal) {
|
|
1705
|
+
async function runAgent(journal, parent, label, phase, prompt, signal, taskKey) {
|
|
1487
1706
|
const maxSeq = journal.stages.length ? Math.max(...journal.stages.map((s) => s.seq)) : 0;
|
|
1488
1707
|
let stageText = null;
|
|
1489
1708
|
const stage = {
|
|
@@ -1492,6 +1711,7 @@ async function runAgent(journal, parent, label, phase, prompt, signal) {
|
|
|
1492
1711
|
phase,
|
|
1493
1712
|
status: "running",
|
|
1494
1713
|
outcome: null,
|
|
1714
|
+
taskKey: taskKey || null,
|
|
1495
1715
|
childId: null,
|
|
1496
1716
|
startedAt: Date.now(),
|
|
1497
1717
|
endedAt: null,
|
|
@@ -1560,6 +1780,7 @@ async function runAgent(journal, parent, label, phase, prompt, signal) {
|
|
|
1560
1780
|
stage.status = "failed";
|
|
1561
1781
|
stage.outcome = stage.guardOutcome || "degenerated";
|
|
1562
1782
|
stage.summary = `进行中护栏中止(${stage.guardReason}),本次尝试无有效产出`;
|
|
1783
|
+
if (text) stage.output = clip(text, 4e3);
|
|
1563
1784
|
journal.logs.push({
|
|
1564
1785
|
t: Date.now(),
|
|
1565
1786
|
level: "warn",
|
|
@@ -1569,20 +1790,33 @@ async function runAgent(journal, parent, label, phase, prompt, signal) {
|
|
|
1569
1790
|
}
|
|
1570
1791
|
stage.status = "failed";
|
|
1571
1792
|
stage.outcome = stop === "completed" && text ? "insubstantial" : stop || "error";
|
|
1793
|
+
const errDetail = result && result.error;
|
|
1572
1794
|
if (stage.outcome === "insubstantial") {
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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);
|
|
1579
1812
|
} else {
|
|
1580
|
-
stage.summary = `未产出有效结果(stopReason=${stop || "unknown"})`;
|
|
1813
|
+
stage.summary = `未产出有效结果(stopReason=${stop || "unknown"}${errDetail ? `,error=${String(errDetail).slice(0, 200)}` : ""})`;
|
|
1581
1814
|
journal.logs.push({
|
|
1582
1815
|
t: Date.now(),
|
|
1583
1816
|
level: "error",
|
|
1584
|
-
message: `${label}
|
|
1817
|
+
message: `${label} ${stage.summary}`
|
|
1585
1818
|
});
|
|
1819
|
+
if (text) stage.output = clip(text, 4e3);
|
|
1586
1820
|
}
|
|
1587
1821
|
return null;
|
|
1588
1822
|
} catch (e) {
|
|
@@ -1612,23 +1846,29 @@ async function runAgent(journal, parent, label, phase, prompt, signal) {
|
|
|
1612
1846
|
}
|
|
1613
1847
|
}
|
|
1614
1848
|
/** 单阶段重试 + token 熔断(官方口径:input+cacheRead+cacheWrite+output 累计)。 */
|
|
1615
|
-
async function withRetry(journal, parent, label, phase, prompt, signal) {
|
|
1849
|
+
async function withRetry(journal, parent, label, phase, prompt, signal, taskKey) {
|
|
1616
1850
|
let attempts = 0;
|
|
1617
1851
|
let stageTokens = 0;
|
|
1852
|
+
let lastStage = null;
|
|
1618
1853
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
1619
1854
|
attempts = attempt;
|
|
1620
|
-
const
|
|
1621
|
-
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;
|
|
1622
1860
|
if (lastStage && lastStage.phase === phase) stageTokens += totalTokensOf(lastStage.usage);
|
|
1623
1861
|
if (result) return {
|
|
1624
1862
|
text: result,
|
|
1625
1863
|
attempts,
|
|
1626
|
-
stageTokens
|
|
1864
|
+
stageTokens,
|
|
1865
|
+
stage: lastStage
|
|
1627
1866
|
};
|
|
1628
1867
|
if (journal.cancelled) return {
|
|
1629
1868
|
text: null,
|
|
1630
1869
|
attempts,
|
|
1631
|
-
stageTokens
|
|
1870
|
+
stageTokens,
|
|
1871
|
+
stage: lastStage
|
|
1632
1872
|
};
|
|
1633
1873
|
if (lastStage && isUnretryable(lastStage.outcome, lastStage.outcome)) {
|
|
1634
1874
|
journal.logs.push({
|
|
@@ -1640,7 +1880,22 @@ async function withRetry(journal, parent, label, phase, prompt, signal) {
|
|
|
1640
1880
|
return {
|
|
1641
1881
|
text: null,
|
|
1642
1882
|
attempts,
|
|
1643
|
-
stageTokens
|
|
1883
|
+
stageTokens,
|
|
1884
|
+
stage: lastStage
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
if (lastStage && lastStage.outcome === "aborted") {
|
|
1888
|
+
journal.logs.push({
|
|
1889
|
+
t: Date.now(),
|
|
1890
|
+
level: "warn",
|
|
1891
|
+
message: `${label} 被外部中止(aborted),未正常产出——非预算问题;可 teamflow_resume 续跑(补跑失败任务,已完成任务复用)`
|
|
1892
|
+
});
|
|
1893
|
+
journal.humanIntervention = true;
|
|
1894
|
+
return {
|
|
1895
|
+
text: null,
|
|
1896
|
+
attempts,
|
|
1897
|
+
stageTokens,
|
|
1898
|
+
stage: lastStage
|
|
1644
1899
|
};
|
|
1645
1900
|
}
|
|
1646
1901
|
if (lastStage && lastStage.outcome === "degenerated") {
|
|
@@ -1653,7 +1908,22 @@ async function withRetry(journal, parent, label, phase, prompt, signal) {
|
|
|
1653
1908
|
return {
|
|
1654
1909
|
text: null,
|
|
1655
1910
|
attempts,
|
|
1656
|
-
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
|
|
1657
1927
|
};
|
|
1658
1928
|
}
|
|
1659
1929
|
if (stageTokens >= 6e4) {
|
|
@@ -1666,13 +1936,14 @@ async function withRetry(journal, parent, label, phase, prompt, signal) {
|
|
|
1666
1936
|
return {
|
|
1667
1937
|
text: null,
|
|
1668
1938
|
attempts,
|
|
1669
|
-
stageTokens
|
|
1939
|
+
stageTokens,
|
|
1940
|
+
stage: lastStage
|
|
1670
1941
|
};
|
|
1671
1942
|
}
|
|
1672
1943
|
if (attempt < 2) journal.logs.push({
|
|
1673
1944
|
t: Date.now(),
|
|
1674
1945
|
level: "warn",
|
|
1675
|
-
message: `${label} 第 ${attempt}
|
|
1946
|
+
message: `${label} 第 ${attempt} 次尝试未成功(${lastStage ? lastStage.outcome || "unknown" : "unknown"}),自动重试(重试 prompt 已附上一轮失败诊断)…`
|
|
1676
1947
|
});
|
|
1677
1948
|
else {
|
|
1678
1949
|
journal.logs.push({
|
|
@@ -1686,7 +1957,8 @@ async function withRetry(journal, parent, label, phase, prompt, signal) {
|
|
|
1686
1957
|
return {
|
|
1687
1958
|
text: null,
|
|
1688
1959
|
attempts,
|
|
1689
|
-
stageTokens
|
|
1960
|
+
stageTokens,
|
|
1961
|
+
stage: lastStage
|
|
1690
1962
|
};
|
|
1691
1963
|
}
|
|
1692
1964
|
//#endregion
|
|
@@ -1868,6 +2140,17 @@ const STATE_BLOCK_INSTRUCTION = `\n\n[STATE BLOCK · mandatory at the end] Appen
|
|
|
1868
2140
|
* - 回归基线:新 PRD 头部「基线依赖:<其他任务夹>」声明;跨代变更用「取代:<夹>#AC-n」;
|
|
1869
2141
|
* 硬保障在项目 verify-* 可执行套件。
|
|
1870
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 内容用英文逗号),不混用中文标点。
|
|
1871
2154
|
*/
|
|
1872
2155
|
/** 产品层文档根(memory.md 等跨任务资产;任务产物在其中的任务夹内)。 */
|
|
1873
2156
|
const TF_DOCS = "docs/teamflow";
|
|
@@ -1955,8 +2238,8 @@ function productCtx(root) {
|
|
|
1955
2238
|
Before starting: read ${base}/AGENTS.md (team rules & doc index — read the summary first, then details on demand; no aimless full reads).
|
|
1956
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/.
|
|
1957
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.
|
|
1958
|
-
[Doc boundary ·
|
|
1959
|
-
[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.
|
|
1960
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.
|
|
1961
2244
|
`;
|
|
1962
2245
|
}
|
|
@@ -1966,7 +2249,7 @@ function headTailClip(text, head, tail) {
|
|
|
1966
2249
|
if (s.length <= head + tail) return s;
|
|
1967
2250
|
return s.slice(0, head) + "\n...\n[CHANGED SECTION]\n" + s.slice(-tail);
|
|
1968
2251
|
}
|
|
1969
|
-
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:
|
|
1970
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.
|
|
1971
2254
|
- [No duplicate reads] Same file: read ≤1 times. To verify a change, grep the change point instead of re-reading the whole file.
|
|
1972
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.
|
|
@@ -1978,7 +2261,7 @@ const TOKEN_HYGIENE = (runId) => `[TOKEN HYGIENE · hard constraint] Context is
|
|
|
1978
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.
|
|
1979
2262
|
`;
|
|
1980
2263
|
/** 一次成型纪律:目标文档 write ≤1 次 + read ≤2 次,严禁 read→edit→read 循环。 */
|
|
1981
|
-
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):
|
|
1982
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).
|
|
1983
2266
|
- **No read→edit→read loops**: never reopen the same document to "tweak"; never re-read the whole file just to confirm a change.
|
|
1984
2267
|
- Use grep + limited segments for details; never whole-file read big documents.
|
|
@@ -2081,7 +2364,7 @@ ${clip(prd, 12e3)}
|
|
|
2081
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.
|
|
2082
2365
|
${productCtx(root)}${stateSliceFor(state, "dev")}${TOKEN_HYGIENE(runId)}[CONTEXT PACK]
|
|
2083
2366
|
[TASK TITLE] ${task.title}
|
|
2084
|
-
${task.files && task.files.length ? `[TASK TARGET FILES] ${task.files.join("
|
|
2367
|
+
${task.files && task.files.length ? `[TASK TARGET FILES] ${task.files.join(", ")}` : ""}
|
|
2085
2368
|
[TASK BRIEF] ${task.spec || "(see technical design)"}
|
|
2086
2369
|
${tech && String(tech).trim() ? `[TECH DESIGN SUMMARY (grep details on demand, don't full re-read)]
|
|
2087
2370
|
${clip(tech, 12e3)}` : ""}
|
|
@@ -2092,9 +2375,14 @@ ${clip(tech, 12e3)}` : ""}
|
|
|
2092
2375
|
3. If spec contradicts reality, explain with evidence in the summary instead of claiming completion or expanding scope on your own.
|
|
2093
2376
|
4. Actually write/modify code (grep + segmented reads to locate; no repeated whole-file reads), then run relevant build/verification to ensure green.
|
|
2094
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.
|
|
2095
|
-
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.
|
|
2096
2379
|
6. [Log discipline] Redirect command output to logs/teamflow/${runId || "<runId>"}/.
|
|
2097
|
-
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.)
|
|
2098
2386
|
8. [State] End with a state block (phase="dev"), touched = array of changed files, summary = implementation conclusion.${STATE_BLOCK_INSTRUCTION}`;
|
|
2099
2387
|
/** 视觉验证能力条款(ADR-2026-08-27,解锁 browser-use 视觉验证):
|
|
2100
2388
|
* 按当前模型多模态能力动态生成——vision=true 允许截图看图(人眼类项),精确值仍走 DOM 计算断言;
|
|
@@ -2124,11 +2412,11 @@ ${clip(devSummary, 15e3)}
|
|
|
2124
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.
|
|
2125
2413
|
3. Read AGENTS.md §4 engineering conventions (verify commands) and the code changes first, then actually run those sandbox-legal verifications.
|
|
2126
2414
|
4. [Log discipline] Redirect command output to logs/teamflow/${runId || "<runId>"}/ (e.g. qa-out.log); no scatter at project root.
|
|
2127
|
-
5. Output
|
|
2128
|
-
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:
|
|
2129
2417
|
| 编号 | 严重级(P0/P1/P2/P3) | 功能模块 | 复现步骤 | 期望行为 | 实际行为 | 关联验收项 |
|
|
2130
2418
|
If no defects: explicitly output 「未发现缺陷」.
|
|
2131
|
-
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}/.
|
|
2132
2420
|
8. [State] End with a state block (phase="qa"), summary = test conclusion / blocked items, extra = { "verifyScripts": [...] }.${STATE_BLOCK_INSTRUCTION}`;
|
|
2133
2421
|
/** QA 打回后的开发修复 prompt:确认缺陷是否属实 → 修复 → 复验交接(QA→dev 打回闭环用)。 */
|
|
2134
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.
|
|
@@ -2145,7 +2433,12 @@ ${tech && String(tech).trim() ? clip(tech, 12e3) : ""}
|
|
|
2145
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).
|
|
2146
2434
|
2. Touch ONLY defect-related files (grep to locate; no whole-file reads of irrelevant big files); respect existing architecture & code style.
|
|
2147
2435
|
3. After fixing, run relevant verification to ensure green (regression floor: existing verify suites pass untouched); redirect output to logs/teamflow/${runId || "<runId>"}/.
|
|
2148
|
-
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.)
|
|
2149
2442
|
5. [State] End with a state block (phase="dev"), touched = changed files array, summary = fix conclusion.${STATE_BLOCK_INSTRUCTION}`;
|
|
2150
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.
|
|
2151
2444
|
${productCtx(root)}${stateSliceFor(state, "acceptance")}${TOKEN_HYGIENE(runId)}
|
|
@@ -2162,14 +2455,14 @@ ${vision ? "[Visual re-check] If QA saved screenshots under the task folder, spo
|
|
|
2162
2455
|
- Any obvious **duplicated implementation / adapter drift / broken existing structure** (this is a code-quality floor, not optional).
|
|
2163
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".
|
|
2164
2457
|
1. Verify each PRD acceptance criterion one by one.
|
|
2165
|
-
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.
|
|
2166
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".
|
|
2167
|
-
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.
|
|
2168
2461
|
5. Chinese Markdown.
|
|
2169
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}`;
|
|
2170
2463
|
/** 需求分诊模型 prompt(模型驱动 triage;供 core/triage.runTriage 使用)。 */
|
|
2171
|
-
const TRIAGE_PROMPT = (requirement, opts, pre) => `You are a senior research-dev triage analyst. Do ONE thing: analyze which pipeline mode this dev requirement fits, then give the conclusion. No code, no scope speculation.
|
|
2172
|
-
[RAW REQUIREMENT]
|
|
2464
|
+
const TRIAGE_PROMPT = (requirement, opts, pre, retryHint) => `You are a senior research-dev triage analyst. Do ONE thing: analyze which pipeline mode this dev requirement fits, then give the conclusion. No code, no scope speculation.
|
|
2465
|
+
${retryHint ? `[RETRY — YOUR LAST REPLY FAILED]\n${retryHint}\n` : ""}[RAW REQUIREMENT]
|
|
2173
2466
|
${requirement}
|
|
2174
2467
|
${pre.rationale.length ? `\n[REGEX PRE-FILTER SIGNALS (reference only; judge semantically, don't blindly follow)]\n${pre.rationale.join("\n")}` : ""}
|
|
2175
2468
|
\n[OPTIONAL SIGNAL] UI work needed: ${opts && opts.needDesign ? "yes" : "not flagged"}
|
|
@@ -2183,12 +2476,12 @@ ${pre.rationale.length ? `\n[REGEX PRE-FILTER SIGNALS (reference only; judge sem
|
|
|
2183
2476
|
|
|
2184
2477
|
[JUDGMENT POINTS]
|
|
2185
2478
|
1. Distinguish "user-visible functional change" vs "internal tech change": refactors/optimizations, even large code volume, usually go tech, not full.
|
|
2186
|
-
2. UI/
|
|
2479
|
+
2. **UI feature work** (new screens/components/interactions/pages) → at least medium (excludes patch/tech). **UI micro-adjustments** — moving a button, relocating a control, changing copy/labels, spacing/padding, color tweaks, "move the button", "switch the button position" — are patch-or-lite material, NOT medium: no new interaction logic, no design phase worth the token cost. The line: does it change behavior/interaction logic (medium) or just placement/appearance of existing elements (patch/lite)?
|
|
2187
2480
|
3. hotfix/single-point/pure numeric/pure docs → patch; clear "add feature X" → pick lite/medium/full by size.
|
|
2188
2481
|
4. Focused change (even with tests/regression) → lite/tech by nature; not necessarily full.
|
|
2189
2482
|
5. [M1 ARCHITECTURE CRITERION (important)] **Architecture-level changes** — persistence/localStorage/database/standalone module/abstraction/cross-many-files without an existing reusable wrapper (like a localStorage wrapper, storage layer, state management) — even if they look like "small features", go **at least medium** (must pass the architecture stage and produce a blueprint, avoiding scattered local implementations by dev); such changes collapse under a light "micro feature" tier. Tech-driven rework (refactor/optimize/arch upgrade) is itself tech (tech also runs the lightweight blueprint now).
|
|
2190
2483
|
|
|
2191
|
-
[OUTPUT]
|
|
2484
|
+
[OUTPUT] JSON object ONLY — no commentary, no preface, no closing text. The FIRST character of your reply must be '{'. Do NOT say anything like "here is the JSON" or "Let me output the JSON" — output the object itself:
|
|
2192
2485
|
{ "mode": "patch|lite|tech|medium|full", "slug": "<topic words> (3-24 lowercase letters/digits/hyphens, e.g. wallkick-toggle, 7bag-random; used to name the task folder)", "kind": "one-word nature", "needDesign": true|false, "complexity": "small|medium|large", "rationale": ["key argument 1","key argument 2"], "confidence": "high|medium|low" }`;
|
|
2193
2486
|
/** tech 档 PRD:技术变更单(无功能 AC,重范围/目标/改动面/回归)。 */
|
|
2194
2487
|
const techChangePrompt = (requirement, root, runId, state) => `You are the senior tech lead. The current workspace IS the target project. This is a **tech-driven rework** requirement (refactor/optimize/upgrade/architecture/dependencies/tech debt) — the product doesn't need a full feature PRD, but needs a **技术变更单** (tech change sheet) as the contract for dev/QA/acceptance and memory write-back.
|
|
@@ -2268,140 +2561,72 @@ const MODE_REGISTRY = {
|
|
|
2268
2561
|
desc: "单行/常量/版本号/hotfix:单 agent 直改+自测即交付(无独立 QA)"
|
|
2269
2562
|
}
|
|
2270
2563
|
};
|
|
2271
|
-
/**
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
"微功能",
|
|
2313
|
-
"小功能",
|
|
2314
|
-
"小增强",
|
|
2315
|
-
"加个",
|
|
2316
|
-
"补一个",
|
|
2317
|
-
"轻微",
|
|
2318
|
-
"顺手"
|
|
2319
|
-
]
|
|
2320
|
-
},
|
|
2321
|
-
{
|
|
2322
|
-
mode: "medium",
|
|
2323
|
-
words: [
|
|
2324
|
-
"界面",
|
|
2325
|
-
"UI",
|
|
2326
|
-
"视觉",
|
|
2327
|
-
"页面",
|
|
2328
|
-
"按钮",
|
|
2329
|
-
"样式",
|
|
2330
|
-
"交互",
|
|
2331
|
-
"布局",
|
|
2332
|
-
"组件",
|
|
2333
|
-
"持久化",
|
|
2334
|
-
"localStorage",
|
|
2335
|
-
"本地存储",
|
|
2336
|
-
"存储",
|
|
2337
|
-
"保存",
|
|
2338
|
-
"恢复",
|
|
2339
|
-
"存档",
|
|
2340
|
-
"独立模块",
|
|
2341
|
-
"抽象",
|
|
2342
|
-
"存储层",
|
|
2343
|
-
"sessionStorage",
|
|
2344
|
-
"IndexedDB",
|
|
2345
|
-
"跨模块",
|
|
2346
|
-
"数据层"
|
|
2347
|
-
]
|
|
2348
|
-
},
|
|
2349
|
-
{
|
|
2350
|
-
mode: "full",
|
|
2351
|
-
words: [
|
|
2352
|
-
"跨模块",
|
|
2353
|
-
"完整",
|
|
2354
|
-
"大型",
|
|
2355
|
-
"对接",
|
|
2356
|
-
"集成",
|
|
2357
|
-
"模块化",
|
|
2358
|
-
"重构为",
|
|
2359
|
-
"二期",
|
|
2360
|
-
"平台"
|
|
2361
|
-
]
|
|
2362
|
-
}
|
|
2564
|
+
/** 确定性护栏关键词(双语,仅 fallback 兜底用;主路由是模型——TRIAGE_PROMPT 语义判断)。
|
|
2565
|
+
* 架构信号:持久化/存储/独立模块/抽象/跨模块——防「轻档位局部实现塌方」(M1 架构护栏)。
|
|
2566
|
+
* UI 信号:UI 相关需求不得落 patch/tech(无设计/QA 的档位)——最低 lite。 */
|
|
2567
|
+
const ARCH_SIGNALS = [
|
|
2568
|
+
"持久化",
|
|
2569
|
+
"存储",
|
|
2570
|
+
"保存",
|
|
2571
|
+
"恢复",
|
|
2572
|
+
"存档",
|
|
2573
|
+
"独立模块",
|
|
2574
|
+
"抽象",
|
|
2575
|
+
"存储层",
|
|
2576
|
+
"localStorage",
|
|
2577
|
+
"sessionStorage",
|
|
2578
|
+
"IndexedDB",
|
|
2579
|
+
"跨模块",
|
|
2580
|
+
"数据层",
|
|
2581
|
+
"persistence",
|
|
2582
|
+
"storage",
|
|
2583
|
+
"database",
|
|
2584
|
+
"数据库",
|
|
2585
|
+
"standalone module",
|
|
2586
|
+
"abstraction"
|
|
2587
|
+
];
|
|
2588
|
+
const UI_SIGNALS = [
|
|
2589
|
+
"界面",
|
|
2590
|
+
"UI",
|
|
2591
|
+
"视觉",
|
|
2592
|
+
"页面",
|
|
2593
|
+
"按钮",
|
|
2594
|
+
"样式",
|
|
2595
|
+
"交互",
|
|
2596
|
+
"布局",
|
|
2597
|
+
"组件",
|
|
2598
|
+
"page",
|
|
2599
|
+
"button",
|
|
2600
|
+
"style",
|
|
2601
|
+
"layout",
|
|
2602
|
+
"component",
|
|
2603
|
+
"visual",
|
|
2604
|
+
"interaction"
|
|
2363
2605
|
];
|
|
2364
|
-
/**
|
|
2606
|
+
/** 对原始需求做启发式分诊(兜底路径专用)。返回建议 mode + 判定理由 + 置信。
|
|
2607
|
+
* 只做确定性护栏(架构强升/UI 禁轻档/needDesign 升档)——不再逐词匹配五档信号:
|
|
2608
|
+
* 主路由是模型(TRIAGE_PROMPT 语义判断,天然双语),正则兜底在模型不可用时宁重勿漏(默认 full)。 */
|
|
2365
2609
|
function suggestMode(requirement, opts) {
|
|
2366
2610
|
const text = String(requirement || "");
|
|
2367
|
-
const
|
|
2368
|
-
for (const sig of SIGNALS) for (const w of sig.words) if (text.includes(w)) hits.push({
|
|
2369
|
-
mode: sig.mode,
|
|
2370
|
-
word: w
|
|
2371
|
-
});
|
|
2372
|
-
const rationale = hits.map((h) => `命中「${h.word}」→ ${h.mode}`);
|
|
2373
|
-
const countOf = (m) => hits.filter((h) => h.mode === m).length;
|
|
2611
|
+
const rationale = [];
|
|
2374
2612
|
let mode = "full";
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
if (
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
"存储",
|
|
2387
|
-
"保存",
|
|
2388
|
-
"恢复",
|
|
2389
|
-
"存档",
|
|
2390
|
-
"独立模块",
|
|
2391
|
-
"抽象",
|
|
2392
|
-
"存储层",
|
|
2393
|
-
"sessionStorage",
|
|
2394
|
-
"IndexedDB",
|
|
2395
|
-
"跨模块",
|
|
2396
|
-
"数据层"
|
|
2397
|
-
];
|
|
2398
|
-
if ((mode === "lite" || mode === "tech" || mode === "patch") && architectureSignals.some((w) => text.includes(w))) {
|
|
2613
|
+
const archHit = ARCH_SIGNALS.find((w) => text.includes(w));
|
|
2614
|
+
if (archHit) {
|
|
2615
|
+
mode = "medium";
|
|
2616
|
+
rationale.push(`架构护栏:需求含「${archHit}」→ 强升 medium(需架构阶段产蓝图,防塌)`);
|
|
2617
|
+
}
|
|
2618
|
+
const uiHit = !archHit ? UI_SIGNALS.find((w) => text.includes(w)) : void 0;
|
|
2619
|
+
if (uiHit) {
|
|
2620
|
+
mode = mode === "full" ? "lite" : mode;
|
|
2621
|
+
rationale.push(`UI 护栏:需求含「${uiHit}」→ 不低于 lite(UI 改动需 QA/验收)`);
|
|
2622
|
+
}
|
|
2623
|
+
if (opts && opts.needDesign && mode !== "medium") {
|
|
2399
2624
|
mode = "medium";
|
|
2400
|
-
rationale.push(
|
|
2625
|
+
rationale.push("needDesign=true → 强升 medium(显式要求设计阶段)");
|
|
2401
2626
|
}
|
|
2402
|
-
|
|
2403
|
-
const
|
|
2404
|
-
|
|
2627
|
+
if (rationale.length === 0) rationale.push("无强护栏信号,默认 full(模型不可用时宁重勿漏)");
|
|
2628
|
+
const kind = mode === "medium" ? "标准功能(含UI)" : mode === "lite" ? "微功能" : "完整需求";
|
|
2629
|
+
const confidence = mode === "medium" && !opts?.needDesign ? "medium" : "low";
|
|
2405
2630
|
return {
|
|
2406
2631
|
mode,
|
|
2407
2632
|
kind,
|
|
@@ -2459,29 +2684,36 @@ function fallbackVerdict(requirement, opts) {
|
|
|
2459
2684
|
source: "fallback"
|
|
2460
2685
|
};
|
|
2461
2686
|
}
|
|
2462
|
-
/** 模型驱动分诊:spawn「分诊分析师」子代理思考一轮;子代理不可用/超时/解析失败 → 正则兜底。
|
|
2687
|
+
/** 模型驱动分诊:spawn「分诊分析师」子代理思考一轮;子代理不可用/超时/解析失败 → 正则兜底。
|
|
2688
|
+
* 解析失败先带纠错提示重试一次(实锤 run tf-mtfo8exi:模型输出「Let me output the JSON.」开场白
|
|
2689
|
+
* 后无 JSON——首轮输出预算被思考耗尽/模型停早;重试提示直接输出 JSON 对象本身)。 */
|
|
2463
2690
|
async function runTriage(requirement, opts, parent, signal) {
|
|
2464
2691
|
const subagents = runtime.subagents;
|
|
2465
2692
|
if (!subagents || typeof subagents.start !== "function") return fallbackVerdict(requirement, opts);
|
|
2466
2693
|
const pre = suggestMode(requirement, opts);
|
|
2467
|
-
let
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2694
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
2695
|
+
let run = null;
|
|
2696
|
+
try {
|
|
2697
|
+
const hint = attempt > 1 ? "Your previous reply contained only a preface (e.g. \"Let me output the JSON.\") with NO JSON object — that is a failed reply. Reply now with the JSON object ITSELF as the first and only content, starting with {." : "";
|
|
2698
|
+
run = await subagents.start(providerName(), {
|
|
2699
|
+
label: "需求分诊",
|
|
2700
|
+
prompt: [{
|
|
2701
|
+
type: "text",
|
|
2702
|
+
text: TRIAGE_PROMPT(requirement, opts, pre, hint)
|
|
2703
|
+
}],
|
|
2704
|
+
parent,
|
|
2705
|
+
signal
|
|
2706
|
+
});
|
|
2707
|
+
const result = await Promise.race([run.result, new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error("triage 超时(90s)")), 9e4))]);
|
|
2708
|
+
const parsed = parseVerdictText(extractText(result && result.output));
|
|
2709
|
+
if (parsed) return parsed;
|
|
2710
|
+
} catch (e) {
|
|
2711
|
+
if (attempt === 2) {}
|
|
2712
|
+
} finally {
|
|
2713
|
+
if (run && run.dispose) try {
|
|
2714
|
+
await run.dispose();
|
|
2715
|
+
} catch (e) {}
|
|
2716
|
+
}
|
|
2485
2717
|
}
|
|
2486
2718
|
return fallbackVerdict(requirement, opts);
|
|
2487
2719
|
}
|
|
@@ -2612,10 +2844,10 @@ function buildResumeProducts(journal) {
|
|
|
2612
2844
|
const products = {};
|
|
2613
2845
|
for (const s of journal.stages) {
|
|
2614
2846
|
if (s.status !== "done" || !s.output) continue;
|
|
2615
|
-
const key =
|
|
2847
|
+
const key = phaseKeyOf(s.phase);
|
|
2616
2848
|
if (!key) continue;
|
|
2617
|
-
if (key === "dev") products.dev = journal.stages.filter((x) => x.phase === "
|
|
2618
|
-
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(/^开发 · /, ""),
|
|
2619
2851
|
failed: false,
|
|
2620
2852
|
output: x.output
|
|
2621
2853
|
}));
|
|
@@ -2623,6 +2855,19 @@ function buildResumeProducts(journal) {
|
|
|
2623
2855
|
}
|
|
2624
2856
|
return products;
|
|
2625
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
|
+
}
|
|
2626
2871
|
/**
|
|
2627
2872
|
* 断点续跑起点:第一个「没有任意 done 尝试」的阶段。
|
|
2628
2873
|
* ⚠️ 按阶段而非尝试判断(实锤 tf-mtcomxpq):PRD 第 1 次尝试 failed(护栏退化)但第 2 次重试 done——
|
|
@@ -2630,13 +2875,37 @@ function buildResumeProducts(journal) {
|
|
|
2630
2875
|
* 全部完成仍被中断(理论极端)→ 从产品验收继续。
|
|
2631
2876
|
*/
|
|
2632
2877
|
function interruptedPhaseOf(journal) {
|
|
2633
|
-
if (hasOpenBlockingBugs(journal)) return "
|
|
2878
|
+
if (hasOpenBlockingBugs(journal)) return "qa";
|
|
2634
2879
|
for (const phase of PHASE_ORDER) {
|
|
2635
|
-
const phaseStages = (journal.stages || []).filter((s) => s.phase === phase);
|
|
2880
|
+
const phaseStages = (journal.stages || []).filter((s) => phaseKeyOf(s.phase) === phase);
|
|
2636
2881
|
if (phaseStages.length === 0) continue;
|
|
2637
|
-
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;
|
|
2638
2885
|
}
|
|
2639
|
-
return "
|
|
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);
|
|
2907
|
+
}
|
|
2908
|
+
return m;
|
|
2640
2909
|
}
|
|
2641
2910
|
/** 开发任务定义(单一来源):架构蓝图自动拆 > 调用方显式 tasks > 整体开发兜底。
|
|
2642
2911
|
* resume 补跑与正常执行共用(defByTitle 按 title 匹配失败子卡)。 */
|
|
@@ -2730,18 +2999,18 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2730
2999
|
});
|
|
2731
3000
|
/** 阶段失败错误:带真实尝试次数/末次结果/累计消耗与熔断语义(取代千篇一律的「重试 N 次后仍无产出」)。 */
|
|
2732
3001
|
const stageFailError = (label, r) => {
|
|
2733
|
-
const last = [...journal.stages || []].reverse().find((s) => s.phase === label);
|
|
3002
|
+
const last = [...journal.stages || []].reverse().find((s) => phaseKeyOf(s.phase) === label);
|
|
2734
3003
|
const attempts = r && r.attempts ? r.attempts : 2;
|
|
2735
3004
|
const burnt = Math.round((r && r.stageTokens || 0) / 1e3);
|
|
2736
3005
|
const breaker = (r && r.stageTokens || 0) >= 6e4 ? ",超出阶段预算熔断" : "";
|
|
2737
3006
|
const detail = last ? `末次 ${last.outcome || "unknown"}${last.summary ? `(${last.summary})` : ""}` : "无阶段记录";
|
|
2738
|
-
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},需人工介入`);
|
|
2739
3008
|
};
|
|
2740
3009
|
try {
|
|
2741
3010
|
if (resume) journal.logs.push({
|
|
2742
3011
|
t: Date.now(),
|
|
2743
3012
|
level: "info",
|
|
2744
|
-
message: `断点续跑:复用 backlog(req=${journal.reqId}),从「${resume.phase}」继续`
|
|
3013
|
+
message: `断点续跑:复用 backlog(req=${journal.reqId}),从「${PHASE_KEY_OF[resume.phase] || resume.phase}」继续`
|
|
2745
3014
|
});
|
|
2746
3015
|
else {
|
|
2747
3016
|
const init = initPipelineBacklog(journal, requirement, options);
|
|
@@ -2904,11 +3173,25 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2904
3173
|
const block = extractStateBlock(output);
|
|
2905
3174
|
if (block) mergeStateBlock(journal.workspace || "default", block, phaseKey);
|
|
2906
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
|
+
};
|
|
2907
3190
|
let prd = null;
|
|
2908
|
-
if (resumed("
|
|
3191
|
+
if (resumed("prd")) {
|
|
2909
3192
|
prd = resume.products.prd;
|
|
2910
3193
|
timeline.prd = prd;
|
|
2911
|
-
logSkip(
|
|
3194
|
+
logSkip(PHASE_KEY_OF.prd);
|
|
2912
3195
|
} else {
|
|
2913
3196
|
journal.logs.push({
|
|
2914
3197
|
t: Date.now(),
|
|
@@ -2925,8 +3208,8 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2925
3208
|
label: "产品经理 · 梳理 PRD",
|
|
2926
3209
|
fn: prdPrompt
|
|
2927
3210
|
};
|
|
2928
|
-
const prdR = await withRetry(journal, parent, pForm.label, "
|
|
2929
|
-
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);
|
|
2930
3213
|
prd = prdR.text;
|
|
2931
3214
|
timeline.prd = prd;
|
|
2932
3215
|
mergeStageState("prd", prd);
|
|
@@ -2935,18 +3218,18 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2935
3218
|
}
|
|
2936
3219
|
let design = null;
|
|
2937
3220
|
if (enabled("design")) {
|
|
2938
|
-
if (resumed("
|
|
3221
|
+
if (resumed("design")) {
|
|
2939
3222
|
design = resume.products.design;
|
|
2940
3223
|
timeline.design = design;
|
|
2941
|
-
logSkip(
|
|
3224
|
+
logSkip(PHASE_KEY_OF.design);
|
|
2942
3225
|
} else {
|
|
2943
3226
|
journal.logs.push({
|
|
2944
3227
|
t: Date.now(),
|
|
2945
3228
|
level: "phase",
|
|
2946
3229
|
message: "进入阶段:UI/UX 设计"
|
|
2947
3230
|
});
|
|
2948
|
-
const designR = await withRetry(journal, parent, "UI/UX 设计师 · 设计说明", "
|
|
2949
|
-
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);
|
|
2950
3233
|
design = designR.text;
|
|
2951
3234
|
timeline.design = design;
|
|
2952
3235
|
mergeStageState("design", design);
|
|
@@ -2956,18 +3239,18 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2956
3239
|
}
|
|
2957
3240
|
let scaffold = null;
|
|
2958
3241
|
if (enabled("scaffold")) {
|
|
2959
|
-
if (resumed("
|
|
3242
|
+
if (resumed("scaffold")) {
|
|
2960
3243
|
scaffold = resume.products.scaffold;
|
|
2961
3244
|
timeline.scaffold = scaffold;
|
|
2962
|
-
logSkip(
|
|
3245
|
+
logSkip(PHASE_KEY_OF.scaffold);
|
|
2963
3246
|
} else {
|
|
2964
3247
|
journal.logs.push({
|
|
2965
3248
|
t: Date.now(),
|
|
2966
3249
|
level: "phase",
|
|
2967
3250
|
message: "进入阶段:架构规划"
|
|
2968
3251
|
});
|
|
2969
|
-
const scR = await withRetry(journal, parent, "架构师 · 脚手架规划与落地", "
|
|
2970
|
-
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);
|
|
2971
3254
|
scaffold = scR.text;
|
|
2972
3255
|
timeline.scaffold = scaffold;
|
|
2973
3256
|
mergeStageState("scaffold", scaffold);
|
|
@@ -2976,72 +3259,74 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2976
3259
|
}
|
|
2977
3260
|
}
|
|
2978
3261
|
let tech = null;
|
|
2979
|
-
if (
|
|
2980
|
-
tech
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
level: "phase",
|
|
2988
|
-
message: isHeavy ? "进入阶段:技术方案" : "进入阶段:架构蓝图"
|
|
2989
|
-
});
|
|
2990
|
-
const label = isHeavy ? "高级全栈工程师 · 技术方案" : "架构师 · 架构蓝图";
|
|
2991
|
-
const techR = await withRetry(journal, parent, label, "技术方案", isHeavy ? techPrompt(prd, design, scaffold, tasks, root, journal.id, state) : architectPrompt(prd, root, journal.id, state), signal);
|
|
2992
|
-
if (!techR.text) throw stageFailError(label, techR);
|
|
2993
|
-
tech = techR.text;
|
|
2994
|
-
timeline.tech = tech;
|
|
2995
|
-
mergeStageState("tech", tech);
|
|
2996
|
-
let bd = extractBlueprint(tech);
|
|
2997
|
-
if (!bd || bd.summary === void 0) try {
|
|
2998
|
-
const techFile = journal.runDocs && journal.workspacePath ? `${journal.workspacePath}/${journal.runDocs}/TECHNICAL.md` : null;
|
|
2999
|
-
if (techFile && existsSync(techFile)) bd = extractBlueprint(readFileSync(techFile, "utf8"));
|
|
3000
|
-
if (bd) journal.logs.push({
|
|
3262
|
+
if (enabled("tech")) {
|
|
3263
|
+
if (resumed("tech")) {
|
|
3264
|
+
tech = resume.products.tech;
|
|
3265
|
+
timeline.tech = tech;
|
|
3266
|
+
logSkip(PHASE_KEY_OF.tech);
|
|
3267
|
+
} else {
|
|
3268
|
+
const isHeavy = !options.lite && options.mode !== "tech" && options.mode !== "patch";
|
|
3269
|
+
journal.logs.push({
|
|
3001
3270
|
t: Date.now(),
|
|
3002
|
-
level: "
|
|
3003
|
-
message: "
|
|
3271
|
+
level: "phase",
|
|
3272
|
+
message: isHeavy ? "进入阶段:技术方案" : "进入阶段:架构蓝图"
|
|
3004
3273
|
});
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3274
|
+
const label = isHeavy ? "高级全栈工程师 · 技术方案" : "架构师 · 架构蓝图";
|
|
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);
|
|
3276
|
+
if (!techR.text) throw stageFailError(label, techR);
|
|
3277
|
+
tech = techR.text;
|
|
3278
|
+
timeline.tech = tech;
|
|
3279
|
+
mergeStageState("tech", tech);
|
|
3280
|
+
let bd = extractBlueprint(tech);
|
|
3281
|
+
if (!bd || bd.summary === void 0) try {
|
|
3282
|
+
const techFile = journal.runDocs && journal.workspacePath ? `${journal.workspacePath}/${journal.runDocs}/TECHNICAL.md` : null;
|
|
3283
|
+
if (techFile && existsSync(techFile)) bd = extractBlueprint(readFileSync(techFile, "utf8"));
|
|
3284
|
+
if (bd) journal.logs.push({
|
|
3285
|
+
t: Date.now(),
|
|
3286
|
+
level: "info",
|
|
3287
|
+
message: "蓝图从任务夹 TECHNICAL.md 提取(模型把蓝图写进了文档而非回复输出)"
|
|
3288
|
+
});
|
|
3289
|
+
} catch (e) {}
|
|
3290
|
+
if (bd && bd.summary !== void 0) try {
|
|
3291
|
+
state.__runCtx = state.__runCtx || {};
|
|
3292
|
+
state.__runCtx.blueprint = bd.render;
|
|
3293
|
+
journal.blueprint = {
|
|
3294
|
+
modules: bd.modules,
|
|
3295
|
+
tasks: bd.tasks
|
|
3296
|
+
};
|
|
3297
|
+
} catch (e) {}
|
|
3298
|
+
else if (/<!-- blueprint -->/.test(String(tech))) journal.logs.push({
|
|
3299
|
+
t: Date.now(),
|
|
3300
|
+
level: "warn",
|
|
3301
|
+
message: "技术方案蓝图块解析失败(JSON 畸形),开发任务将回退整体开发——TECHNICAL.md 蓝图块需人工检查"
|
|
3302
|
+
});
|
|
3303
|
+
noteTaskStageUsage(journal);
|
|
3304
|
+
if (journal.cancelled) return;
|
|
3305
|
+
}
|
|
3021
3306
|
}
|
|
3022
3307
|
let devResults = null;
|
|
3023
|
-
if (
|
|
3308
|
+
if (resume) {
|
|
3024
3309
|
devResults = resume.products.dev || [];
|
|
3025
|
-
const
|
|
3026
|
-
|
|
3027
|
-
const
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
};
|
|
3036
|
-
}).filter(Boolean);
|
|
3037
|
-
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));
|
|
3038
3320
|
journal.logs.push({
|
|
3039
3321
|
t: Date.now(),
|
|
3040
3322
|
level: "warn",
|
|
3041
|
-
message: `断点续跑开发:复用 ${reused.length} 个已完成任务,补跑 ${
|
|
3323
|
+
message: `断点续跑开发:复用 ${reused.length} 个已完成任务,补跑 ${todo.length} 个失败任务`
|
|
3042
3324
|
});
|
|
3043
|
-
const rerun = await runPool(
|
|
3044
|
-
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);
|
|
3045
3330
|
const ok = !!devR.text;
|
|
3046
3331
|
return {
|
|
3047
3332
|
title: task.title,
|
|
@@ -3050,14 +3335,11 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3050
3335
|
};
|
|
3051
3336
|
});
|
|
3052
3337
|
for (const t of rerun) {
|
|
3053
|
-
const sub =
|
|
3338
|
+
const sub = createSubtask(journal, t.title, t.spec || "");
|
|
3054
3339
|
if (sub) completeSubtask(journal, sub.id, t.failed, t.output ? snippet(t.output, 1e3) : null, null);
|
|
3055
3340
|
}
|
|
3056
3341
|
devResults = [...reused, ...rerun];
|
|
3057
3342
|
timeline.dev = devResults;
|
|
3058
|
-
} else {
|
|
3059
|
-
timeline.dev = devResults;
|
|
3060
|
-
logSkip("开发");
|
|
3061
3343
|
}
|
|
3062
3344
|
} else {
|
|
3063
3345
|
journal.logs.push({
|
|
@@ -3099,12 +3381,12 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3099
3381
|
persistJournal(journal);
|
|
3100
3382
|
}
|
|
3101
3383
|
}
|
|
3102
|
-
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);
|
|
3103
3386
|
const ok = !!devR.text;
|
|
3104
3387
|
if (sub) {
|
|
3105
3388
|
completeSubtask(journal, sub.id, !ok, devR.text ? snippet(devR.text, 1e3) : null, null);
|
|
3106
|
-
|
|
3107
|
-
if (devStage) noteSubtaskUsage(journal, sub.id, devStage);
|
|
3389
|
+
if (devR.stage) noteSubtaskUsage(journal, sub.id, devR.stage);
|
|
3108
3390
|
}
|
|
3109
3391
|
return {
|
|
3110
3392
|
title: task.title,
|
|
@@ -3115,7 +3397,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3115
3397
|
timeline.dev = devResults;
|
|
3116
3398
|
for (const r of devResults) if (r && r.output) mergeStageState("dev", r.output);
|
|
3117
3399
|
noteTaskStageUsage(journal);
|
|
3118
|
-
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(",") || "开发组");
|
|
3119
3401
|
const failedCount = devResults.filter((r) => r && r.failed).length;
|
|
3120
3402
|
if (failedCount > 0) {
|
|
3121
3403
|
advanceTask(journal, "needs-human", null, "开发失败,需人工介入", { by: "dev" });
|
|
@@ -3150,10 +3432,10 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3150
3432
|
message: "当前档位阶段集不含独立 QA:跳过(单点修复,开发自测兜底)"
|
|
3151
3433
|
});
|
|
3152
3434
|
qa = "(独立 QA 跳过:当前档位由开发自测兜底)";
|
|
3153
|
-
} else if (resumed("
|
|
3154
|
-
qa = resume.products.qa;
|
|
3435
|
+
} else if (resumed("qa") && !hasOpenBlockingBugs(journal)) {
|
|
3436
|
+
qa = artifactText(journal, "QA-REPORT.md") || resume.products.qa;
|
|
3155
3437
|
timeline.qa = qa;
|
|
3156
|
-
logSkip(
|
|
3438
|
+
logSkip(PHASE_KEY_OF.qa);
|
|
3157
3439
|
} else {
|
|
3158
3440
|
journal.logs.push({
|
|
3159
3441
|
t: Date.now(),
|
|
@@ -3162,7 +3444,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3162
3444
|
});
|
|
3163
3445
|
advanceTask(journal, "testing", null, "QA 开始(待测试 → 测试中)", { by: "qa" });
|
|
3164
3446
|
const store = storeFor(scopeKey);
|
|
3165
|
-
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(",") || "测试组";
|
|
3166
3448
|
let round = 0;
|
|
3167
3449
|
let qaClean = false;
|
|
3168
3450
|
const devFixRounds = [];
|
|
@@ -3174,14 +3456,26 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3174
3456
|
do {
|
|
3175
3457
|
round += 1;
|
|
3176
3458
|
const isReverify = round > 1;
|
|
3177
|
-
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);
|
|
3178
3460
|
if (!qaR.text) {
|
|
3179
3461
|
advanceTask(journal, "needs-human", null, isReverify ? `QA 复验失败(第 ${round - 1} 轮修复后)` : "QA 失败", { by: "qa" });
|
|
3180
|
-
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
|
+
});
|
|
3181
3477
|
}
|
|
3182
|
-
qa = qaR.text;
|
|
3183
3478
|
timeline.qa = qa;
|
|
3184
|
-
mergeStageState("qa", qa);
|
|
3185
3479
|
noteTaskStageUsage(journal);
|
|
3186
3480
|
noteTaskAssign(journal, "qa", qaStageChildren());
|
|
3187
3481
|
defects = parseDefects(qa);
|
|
@@ -3219,7 +3513,8 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3219
3513
|
message: `QA 发现 ${blocking.length} 个阻断缺陷(第 ${round} 轮),打回开发确认修复后复验`
|
|
3220
3514
|
});
|
|
3221
3515
|
advanceTask(journal, "rework", snippet(qa, 3e3), `QA 打回开发修复(第 ${round}/3 轮)`, { by: "qa" });
|
|
3222
|
-
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);
|
|
3223
3518
|
if (!fixR.text) {
|
|
3224
3519
|
advanceTask(journal, "needs-human", null, "QA 打回后开发修复失败", { by: "qa" });
|
|
3225
3520
|
throw stageFailError("开发(QA 打回修复)", fixR);
|
|
@@ -3244,7 +3539,22 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3244
3539
|
if (journal.cancelled) return;
|
|
3245
3540
|
}
|
|
3246
3541
|
persistJournal(journal);
|
|
3247
|
-
if (!
|
|
3542
|
+
if (!enabled("acceptance")) {
|
|
3543
|
+
journal.logs.push({
|
|
3544
|
+
t: Date.now(),
|
|
3545
|
+
level: "info",
|
|
3546
|
+
message: "patch 档交付完成:单 agent 直改 + 自测即收口(无独立 QA/验收)"
|
|
3547
|
+
});
|
|
3548
|
+
advanceTask(journal, "accepted", null, "patch 直改交付(自测通过)", { by: "dev" });
|
|
3549
|
+
const store = storeFor(scopeKey);
|
|
3550
|
+
const req = store.find("req", journal.reqId);
|
|
3551
|
+
if (req && req.status !== "accepted") {
|
|
3552
|
+
store.pushEvent(req, req.status, "accepted", "patch 交付通过(自测)");
|
|
3553
|
+
req.status = "accepted";
|
|
3554
|
+
store.persist();
|
|
3555
|
+
}
|
|
3556
|
+
journal.status = "completed";
|
|
3557
|
+
} else if (!qaBlocked) {
|
|
3248
3558
|
journal.logs.push({
|
|
3249
3559
|
t: Date.now(),
|
|
3250
3560
|
level: "phase",
|
|
@@ -3254,18 +3564,47 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3254
3564
|
const curTask = storeFor(scopeKey).find("task", journal.taskId);
|
|
3255
3565
|
if (curTask && curTask.status !== "pending-acceptance" && curTask.status !== "needs-human" && curTask.status !== "rework") advanceTask(journal, "pending-acceptance", null, "进入验收(待验收)", { by: "pm" });
|
|
3256
3566
|
}
|
|
3257
|
-
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);
|
|
3258
3568
|
if (!accR.text) {
|
|
3259
3569
|
advanceTask(journal, "needs-human", null, "验收失败", { by: "pm" });
|
|
3260
|
-
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
|
+
});
|
|
3261
3585
|
}
|
|
3262
|
-
const acceptance = accR.text;
|
|
3263
3586
|
timeline.acceptance = acceptance;
|
|
3264
3587
|
noteTaskStageUsage(journal);
|
|
3265
|
-
const accStage = journal.stages.find((s) => s.phase === "
|
|
3588
|
+
const accStage = journal.stages.find((s) => phaseKeyOf(s.phase) === "acceptance" && s.childId);
|
|
3266
3589
|
noteTaskAssign(journal, "accept", accStage ? String(accStage.childId).slice(0, 8) : "验收组");
|
|
3267
|
-
mergeStageState("acceptance", acceptance);
|
|
3268
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
|
+
}
|
|
3269
3608
|
if (accVerdict === "reject") {
|
|
3270
3609
|
advanceTask(journal, "needs-human", snippet(acceptance, 3e3), "需求与现状不符(无需改动),需人工决定调整或取消需求", { by: "pm" });
|
|
3271
3610
|
const store = storeFor(scopeKey);
|
|
@@ -4310,8 +4649,7 @@ function tryFlushPendingInjections(sessionId) {
|
|
|
4310
4649
|
const agent = runtime.agents ? runtime.agents.get(sessionId) : void 0;
|
|
4311
4650
|
if (!agent || typeof agent.inject !== "function") return;
|
|
4312
4651
|
try {
|
|
4313
|
-
agent.inject({
|
|
4314
|
-
type: "user",
|
|
4652
|
+
agent.inject(createUserMessage({
|
|
4315
4653
|
content: [{
|
|
4316
4654
|
type: "text",
|
|
4317
4655
|
text: teamflowContextText(pending.teamIcon, pending.teamName, pending.teamId)
|
|
@@ -4319,9 +4657,9 @@ function tryFlushPendingInjections(sessionId) {
|
|
|
4319
4657
|
source: {
|
|
4320
4658
|
kind: "plugin",
|
|
4321
4659
|
plugin: "dsh-plugin-teamflow",
|
|
4322
|
-
form: "
|
|
4660
|
+
form: "instructions"
|
|
4323
4661
|
}
|
|
4324
|
-
});
|
|
4662
|
+
}));
|
|
4325
4663
|
pendingInjections.delete(sessionId);
|
|
4326
4664
|
} catch (e) {}
|
|
4327
4665
|
}
|
|
@@ -4394,7 +4732,9 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4394
4732
|
const j = runs.get(latest.id);
|
|
4395
4733
|
return j ? snapshotOf(j) : null;
|
|
4396
4734
|
}
|
|
4397
|
-
/** 阶段详情:卡片点击查看 —— 状态/耗时/官方 usage + 产物全文(超 24k 截断)。
|
|
4735
|
+
/** 阶段详情:卡片点击查看 —— 状态/耗时/官方 usage + 产物全文(超 24k 截断)。
|
|
4736
|
+
* 2026-09-06 状态机化:返回同任务全部尝试(attempts 聚合——按 stage.taskKey(旧数据 label 兜底),
|
|
4737
|
+
* 按 seq 排序)——client 弹窗单次渲染现状、多次渲染时间线。 */
|
|
4398
4738
|
stageDetail(runId, seq, sessionId) {
|
|
4399
4739
|
if (typeof runId !== "string" || !runId || seq === void 0 || seq === null) return null;
|
|
4400
4740
|
const sc = sessionScope(sessionId);
|
|
@@ -4403,6 +4743,21 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4403
4743
|
if (j.workspace && sc.projectKey && j.workspace !== sc.projectKey && sc.projectKey !== "default") return null;
|
|
4404
4744
|
const s = (j.stages || []).find((st) => Number(st.seq) === Number(seq));
|
|
4405
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;
|
|
4406
4761
|
return {
|
|
4407
4762
|
seq: s.seq,
|
|
4408
4763
|
label: s.label,
|
|
@@ -4414,8 +4769,10 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4414
4769
|
endedAt: s.endedAt,
|
|
4415
4770
|
ownerSession: j.ownerSession || null,
|
|
4416
4771
|
usage: s.usage || null,
|
|
4772
|
+
verifyEvidence: s.verifyEvidence || null,
|
|
4417
4773
|
summary: clip(s.summary || "", 3e3),
|
|
4418
|
-
output: clip(toText(s.output) || toText(s.handoff) || "", 24e3)
|
|
4774
|
+
output: clip(toText(s.output) || toText(s.handoff) || "", 24e3),
|
|
4775
|
+
attempts
|
|
4419
4776
|
};
|
|
4420
4777
|
}
|
|
4421
4778
|
/** Backlog 条目详情:卡片点击查看 —— 完整字段 + 流转时间线 + 关联(子卡/缺陷)+ 任务夹路径。 */
|
|
@@ -4640,8 +4997,7 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4640
4997
|
activeTeams.set(sid, tid);
|
|
4641
4998
|
saveActiveTeams();
|
|
4642
4999
|
const agent = runtime.agents && runtime.agents.get(sid);
|
|
4643
|
-
const injectPayload = {
|
|
4644
|
-
type: "user",
|
|
5000
|
+
const injectPayload = createUserMessage({
|
|
4645
5001
|
content: [{
|
|
4646
5002
|
type: "text",
|
|
4647
5003
|
text: teamflowContextText(team.icon, team.name, tid)
|
|
@@ -4649,9 +5005,9 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4649
5005
|
source: {
|
|
4650
5006
|
kind: "plugin",
|
|
4651
5007
|
plugin: "dsh-plugin-teamflow",
|
|
4652
|
-
form: "
|
|
5008
|
+
form: "instructions"
|
|
4653
5009
|
}
|
|
4654
|
-
};
|
|
5010
|
+
});
|
|
4655
5011
|
if (agent && typeof agent.inject === "function") try {
|
|
4656
5012
|
agent.inject(injectPayload);
|
|
4657
5013
|
} catch (e) {}
|