dsh-plugin-teamflow 0.1.2 → 0.1.4
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 +44 -7
- package/README.en.md +22 -0
- package/README.md +22 -0
- package/lib/host.mjs +713 -156
- package/package.json +25 -9
package/lib/host.mjs
CHANGED
|
@@ -4,8 +4,8 @@ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
|
4
4
|
import { parameterSchemaSpecToJsonSchema } from "@deepseek-ai/dsh-tools";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
8
7
|
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;
|
|
@@ -260,6 +260,17 @@ const SAFE_SIGNAL = {
|
|
|
260
260
|
function normalizeSignal(s) {
|
|
261
261
|
return s && typeof s === "object" && typeof s.addEventListener === "function" && typeof s.aborted === "boolean" ? s : SAFE_SIGNAL;
|
|
262
262
|
}
|
|
263
|
+
/** 分支 slug 派生(ADR-2026-08-27):branchName > triageSlug > 需求中的英文标识词 > reqId 数字 > 'feature'。
|
|
264
|
+
* 实锤 feat/feature:lite 显式时 triage 不跑(无 slug)+ 分支检查早于 reqId 生成 → fallback 'feature'。 */
|
|
265
|
+
function deriveBranchSlug(requirement, reqId, triageSlug, branchName) {
|
|
266
|
+
if (branchName && /^[a-z0-9][a-z0-9-_]*$/i.test(branchName)) return String(branchName).replace(/[^a-z0-9-]/gi, "-").toLowerCase().slice(0, 40);
|
|
267
|
+
if (triageSlug && /^[a-z0-9-]{3,24}$/i.test(triageSlug)) return triageSlug;
|
|
268
|
+
const en = String(requirement || "").match(/[a-zA-Z][a-zA-Z0-9-]{2,23}/g);
|
|
269
|
+
if (en && en.length) return en[0].toLowerCase().slice(0, 40);
|
|
270
|
+
const num = String(reqId || "").match(/\d+/);
|
|
271
|
+
if (num) return `r${num[0]}`;
|
|
272
|
+
return "feature";
|
|
273
|
+
}
|
|
263
274
|
/** 产出物实质校验:非空 + 无拒绝词 + 达到阶段长度下限。 */
|
|
264
275
|
function hasSubstance(phase, text) {
|
|
265
276
|
if (!text || !text.trim()) return false;
|
|
@@ -267,10 +278,11 @@ function hasSubstance(phase, text) {
|
|
|
267
278
|
const min = STAGE_MIN_LENGTH[phase] ?? 100;
|
|
268
279
|
return text.trim().length >= min;
|
|
269
280
|
}
|
|
270
|
-
/**
|
|
281
|
+
/** 不可重试的失败原因(上下文耗尽/超长/provider 客户端拒绝等——重试同一 prompt 大概率复现)。
|
|
282
|
+
* 实锤 tf-mtcnejqj:opencode-go 400 invalid_request_error(tool 消息序列非法)被当作可重试 → 烧 1.98M 熔断。 */
|
|
271
283
|
function isUnretryable(reason, outcome) {
|
|
272
284
|
const r = String(reason || outcome || "");
|
|
273
|
-
return /context|limit|max-token|token|tool-error/i.test(r);
|
|
285
|
+
return /context|limit|max-token|token|tool-error|400|invalid_request|INVALID_REQUEST/i.test(r);
|
|
274
286
|
}
|
|
275
287
|
/** 阶段产出 → 精简交接摘要(供审计/展示;产出含显式 <!-- handoff --> 块则优先取块内内容)。 */
|
|
276
288
|
function handoffBrief(text) {
|
|
@@ -381,12 +393,13 @@ function extractBlueprint(text) {
|
|
|
381
393
|
*/
|
|
382
394
|
/** 子代理/计量等宿主能力(由 TeamflowService 装配时 setRuntime 注入)。字段为鸭子类型:消费方自行窄化。 */
|
|
383
395
|
const runtime = {};
|
|
384
|
-
function setRuntime(agents, subagents, tokenMeter, workspaceRegistry, agentDefaultModel) {
|
|
396
|
+
function setRuntime(agents, subagents, tokenMeter, workspaceRegistry, agentDefaultModel, llm) {
|
|
385
397
|
runtime.agents = agents;
|
|
386
398
|
runtime.subagents = subagents;
|
|
387
399
|
runtime.tokenMeter = tokenMeter;
|
|
388
400
|
runtime.workspaceRegistry = workspaceRegistry;
|
|
389
401
|
runtime.agentDefaultModel = agentDefaultModel;
|
|
402
|
+
runtime.llm = llm;
|
|
390
403
|
}
|
|
391
404
|
/** 运行期 run 注册表(runId → Journal)。 */
|
|
392
405
|
const runs = /* @__PURE__ */ new Map();
|
|
@@ -405,6 +418,32 @@ function providerName() {
|
|
|
405
418
|
return names.length > 0 ? names[0] : null;
|
|
406
419
|
}
|
|
407
420
|
/**
|
|
421
|
+
* 探测指定 provider/model 是否支持图像输入(多模态)。
|
|
422
|
+
* 用途:QA/验收的视觉验证条款按能力条件化——不支持视觉的模型禁止「截图看图」(防幻觉/循环),
|
|
423
|
+
* 只走 DOM 计算断言(evaluate 返回文本);探测失败/未知 → false(安全侧)。
|
|
424
|
+
*/
|
|
425
|
+
async function currentModelSupportsVision(provider, model) {
|
|
426
|
+
try {
|
|
427
|
+
const llm = runtime.llm;
|
|
428
|
+
if (!llm || typeof llm.resolveModelInfo !== "function") return false;
|
|
429
|
+
const p = provider && provider.trim() || providerName();
|
|
430
|
+
const m = model && model.trim() || (() => {
|
|
431
|
+
const adm = runtime.agentDefaultModel;
|
|
432
|
+
if (adm && typeof adm.currentSelection === "function") try {
|
|
433
|
+
return adm.currentSelection()?.model;
|
|
434
|
+
} catch (e) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
return adm?.model;
|
|
438
|
+
})();
|
|
439
|
+
if (!p || !m) return false;
|
|
440
|
+
const info = await llm.resolveModelInfo(p, m);
|
|
441
|
+
return !!info && Array.isArray(info.inputModalities) && info.inputModalities.includes("image");
|
|
442
|
+
} catch (e) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
408
447
|
* 从发起会话推导工作区作用域。
|
|
409
448
|
*
|
|
410
449
|
* 优先级:
|
|
@@ -932,6 +971,17 @@ function verifyReqBugs(journal) {
|
|
|
932
971
|
}
|
|
933
972
|
if (touched) store.persist();
|
|
934
973
|
}
|
|
974
|
+
/** 该需求是否存在未闭环的阻断缺陷(P0/P1/P2 仍 open)——resume 断点定位与 QA 复用判定依赖。
|
|
975
|
+
* 实锤 run tf-mte906e9:QA 修复子代理失败 → run failed,但 QA 阶段本身 done(缺陷已登记),
|
|
976
|
+
* 旧 interruptedPhaseOf 直接定位产品验收 → 带缺陷代码进验收。
|
|
977
|
+
* ⚠️ store key 与 resumeRun/storeFor 解析一致(workspace || product || default),否则查错 store 误判无缺陷。 */
|
|
978
|
+
function hasOpenBlockingBugs(journal) {
|
|
979
|
+
try {
|
|
980
|
+
return storeFor(journal.workspace || journal.product || "default").bugs.some((b) => b.reqId === journal.reqId && b.status === "open" && b.severity !== "P3");
|
|
981
|
+
} catch (e) {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
935
985
|
/** 默认团队配置文件内容。 */
|
|
936
986
|
const DEFAULT_TEAMS_FILE = {
|
|
937
987
|
version: 1,
|
|
@@ -1030,6 +1080,79 @@ function getActiveStages(team, options) {
|
|
|
1030
1080
|
});
|
|
1031
1081
|
}
|
|
1032
1082
|
//#endregion
|
|
1083
|
+
//#region host/core/sanity.ts
|
|
1084
|
+
/**
|
|
1085
|
+
* dsh-plugin-teamflow core — M0 状态核对(sanity check)。
|
|
1086
|
+
*
|
|
1087
|
+
* 目的(三情况协议):
|
|
1088
|
+
* - 情况一(全新会话):0 认知 → 核对确认"从零开始"。
|
|
1089
|
+
* - 情况二(续会话):认知大概率过期(多人协作/场外提交/非流水线改动)→ 核对发现预期外变化。
|
|
1090
|
+
* - 情况三(新会话处理新需求):即使共用 state/记忆,仓库也可能被外部改动 → 核对现状。
|
|
1091
|
+
*
|
|
1092
|
+
* 核心原则:认知资产(索引/记忆/摘要)可复用"减量",但永不能替代"对代码库当前真实状态的核对"。
|
|
1093
|
+
* 本模块在 host 侧直接跑 git(零模型 token、轻量、失败优雅降级为"无法核对"),
|
|
1094
|
+
* 产出 externalDiffs 摘要注入到后续所有阶段 prompt。
|
|
1095
|
+
*/
|
|
1096
|
+
/** 单条 git 检查结果(失败返回 null,调用方降级)。 */
|
|
1097
|
+
function gitCmd(cwd, args, timeoutMs = 8e3) {
|
|
1098
|
+
try {
|
|
1099
|
+
return execFileSync("git", args, {
|
|
1100
|
+
cwd,
|
|
1101
|
+
encoding: "utf8",
|
|
1102
|
+
timeout: timeoutMs,
|
|
1103
|
+
windowsHide: true
|
|
1104
|
+
}).trim();
|
|
1105
|
+
} catch (e) {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* 跑一次状态核对。
|
|
1111
|
+
* @param path - 工作区绝对路径(workspaceScopeOf(agent).path)。
|
|
1112
|
+
*/
|
|
1113
|
+
function runSanityCheck(path) {
|
|
1114
|
+
const defaultOut = {
|
|
1115
|
+
ok: false,
|
|
1116
|
+
inRepo: false,
|
|
1117
|
+
branch: null,
|
|
1118
|
+
dirty: "",
|
|
1119
|
+
hasDirty: false,
|
|
1120
|
+
recentCommits: "",
|
|
1121
|
+
summary: ""
|
|
1122
|
+
};
|
|
1123
|
+
if (!path) return {
|
|
1124
|
+
...defaultOut,
|
|
1125
|
+
summary: "⚠ 无法确定工作区路径,未做状态核对。"
|
|
1126
|
+
};
|
|
1127
|
+
const branch = gitCmd(path, ["branch", "--show-current"]);
|
|
1128
|
+
const status = gitCmd(path, ["status", "--short"]);
|
|
1129
|
+
if (branch === null && status === null) return {
|
|
1130
|
+
...defaultOut,
|
|
1131
|
+
summary: "⚠ 状态核对不可用(非 git 仓库或 git 命令被拒),无法确认代码库真实状态。"
|
|
1132
|
+
};
|
|
1133
|
+
const recent = gitCmd(path, [
|
|
1134
|
+
"log",
|
|
1135
|
+
"--oneline",
|
|
1136
|
+
"-5"
|
|
1137
|
+
]);
|
|
1138
|
+
const dirty = status || "";
|
|
1139
|
+
const dirtyLines = dirty.split("\n").filter(Boolean);
|
|
1140
|
+
const summaryParts = [];
|
|
1141
|
+
summaryParts.push(`分支:${branch && branch !== "HEAD" ? branch : "(detached HEAD / 非分支)"}`);
|
|
1142
|
+
if (dirtyLines.length) summaryParts.push(`未提交改动 ${dirtyLines.length} 处:${dirtyLines.slice(0, 8).join(";")}${dirtyLines.length > 8 ? "…" : ""}`);
|
|
1143
|
+
else summaryParts.push("工作区干净(无未提交改动)");
|
|
1144
|
+
if (recent) summaryParts.push(`近期提交(可能含场外改动):${recent.split("\n").slice(0, 3).join(";")}`);
|
|
1145
|
+
return {
|
|
1146
|
+
ok: true,
|
|
1147
|
+
inRepo: true,
|
|
1148
|
+
branch: branch && branch !== "HEAD" ? branch : null,
|
|
1149
|
+
dirty,
|
|
1150
|
+
hasDirty: dirtyLines.length > 0,
|
|
1151
|
+
recentCommits: recent || "",
|
|
1152
|
+
summary: `【状态核对】${summaryParts.join("。")}`
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
//#endregion
|
|
1033
1156
|
//#region host/core/metering.ts
|
|
1034
1157
|
/**
|
|
1035
1158
|
* 累计子代理会话中所有 LLM 调用的真实 usage(官方三桶 + 调用数)。
|
|
@@ -1083,16 +1206,25 @@ function totalTokensOf(usage) {
|
|
|
1083
1206
|
* 的健康节奏工作到第 25 分钟被击落(tf-mt8fbavd dev 尝试一)。
|
|
1084
1207
|
*
|
|
1085
1208
|
* 设计原则:【只看进度信号,无任何时间配额】慢吞吐的合法任务永远不该被打断:
|
|
1086
|
-
* A. 复读检测:滑动窗口内同一规范化流式片段出现 ≥ GUARD_REPEAT_LIMIT
|
|
1087
|
-
*
|
|
1209
|
+
* A. 复读检测:滑动窗口内同一规范化流式片段出现 ≥ GUARD_REPEAT_LIMIT 次,且窗口内零变更进展
|
|
1210
|
+
* (无 edit/write 等写操作)→ 真退化(纯推理打转)→ outcome='degenerated'(豁免预算门,允许一次干净重试)。
|
|
1211
|
+
* ⚠️ 状态判定(实锤 run tf-mte906e9):大文件 read-edit 循环是正常模式——模型反复 read 同一大文件
|
|
1212
|
+
* (每次 edit 后内容已变,必须重读确认)、输出高度相似的「读后分析」,逐字片段在 400 条窗口内
|
|
1213
|
+
* 可累积 ≥12 次——伴随 edit/write 变更调用时只记录观察,不中止(否则大文件修改任务全被误杀)。
|
|
1088
1214
|
* B. 挂死检测:连续 GUARD_SILENCE_MS 一个新事件都没有(provider 层挂起/连接静默死亡)
|
|
1089
1215
|
* → outcome='stalled'(走正常预算门 → 熔断转人工,不自动重试烧钱)。
|
|
1090
1216
|
* C. 空转检测:会话仍在产出事件,但连续 GUARD_NO_TOOL_MS 没有任何工具调用
|
|
1091
1217
|
* (纯推理打转/改写式循环;正常 agent 每分钟都在调工具)→ outcome='stalled'。
|
|
1218
|
+
* 兜底关系:复读判定放宽后,edit 后陷入死循环的漏网场景由 C(长时间无工具调用)兜住。
|
|
1092
1219
|
*
|
|
1093
1220
|
* 中止方式:run.dispose() → run.result 结算。outcome 命名刻意避开 isUnretryable 的
|
|
1094
1221
|
* /token|context|limit/ 正则;只有 'degenerated' 享受干净重试豁免(runner.withRetry)。
|
|
1095
1222
|
*/
|
|
1223
|
+
/** 进展工具(复读状态判定):变更类写操作 + 脚本执行。
|
|
1224
|
+
* 「有进展」= 大文件 read-edit 循环(dev)或只读分析任务的 read+跑脚本循环(QA/验收)均属正常模式;
|
|
1225
|
+
* 纯 read 循环(反复整读同一文件却无变更/无脚本执行)= 真退化。实锤 run tf-mte906e9:QA 重跑
|
|
1226
|
+
* 只读分析(不 edit)→ 旧判定「零变更进展」误杀,第 2 次 provider error 后 450k 熔断。 */
|
|
1227
|
+
const PROGRESS_TOOLS = /^(edit|write|create|apply_patch|patch|remove|delete|rm|mkdir|move|rename|append|bash|pwsh|shell|powershell)$/i;
|
|
1096
1228
|
/** 与 metering 同款事件访问器(session.events 可能是数组或返回数组的函数)。 */
|
|
1097
1229
|
function eventsOf(run) {
|
|
1098
1230
|
const local = run && run.localAgent;
|
|
@@ -1106,6 +1238,38 @@ function eventsOf(run) {
|
|
|
1106
1238
|
function normalizeFragment(s) {
|
|
1107
1239
|
return String(s || "").toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "");
|
|
1108
1240
|
}
|
|
1241
|
+
/** 观测→执行闭环:向运行中的子代理注入轻提醒(不打断,下一轮 step 可见)。
|
|
1242
|
+
* 通道:subagents.start 句柄无 inject——用 DSH 官方 session.append('user/message')(in-process driver 同款用法)。
|
|
1243
|
+
* ⚠️ 安全窗口(实锤 tf-mtcnejqj):绝不能插在 assistant(tool_calls) → tool/result 之间——
|
|
1244
|
+
* provider 校验「tool 消息必须响应前序 tool_calls」,插入 user 消息会 400 invalid_request_error。
|
|
1245
|
+
* 因此只入队(pendingInjects),在观察到 step/end(该 step 的 tool/result 已写入)后统一 flush。 */
|
|
1246
|
+
function injectReminder(run, text) {
|
|
1247
|
+
const queue = run;
|
|
1248
|
+
try {
|
|
1249
|
+
if (!Array.isArray(queue.__teamflowPending)) queue.__teamflowPending = [];
|
|
1250
|
+
queue.__teamflowPending.push(text);
|
|
1251
|
+
} catch (e) {}
|
|
1252
|
+
}
|
|
1253
|
+
function flushReminders(run) {
|
|
1254
|
+
try {
|
|
1255
|
+
const queue = run.__teamflowPending;
|
|
1256
|
+
if (!queue || queue.length === 0) return;
|
|
1257
|
+
const local = run.localAgent;
|
|
1258
|
+
if (!local || typeof local.session?.append !== "function") return;
|
|
1259
|
+
for (const text of queue.splice(0)) local.session.append("user/message", {
|
|
1260
|
+
id: crypto.randomUUID(),
|
|
1261
|
+
role: "user",
|
|
1262
|
+
content: [{
|
|
1263
|
+
type: "text",
|
|
1264
|
+
text
|
|
1265
|
+
}],
|
|
1266
|
+
source: {
|
|
1267
|
+
kind: "plugin",
|
|
1268
|
+
plugin: "teamflow"
|
|
1269
|
+
}
|
|
1270
|
+
}, { surfaceOp: "append" });
|
|
1271
|
+
} catch (e) {}
|
|
1272
|
+
}
|
|
1109
1273
|
/**
|
|
1110
1274
|
* 启动单调用护栏轮询,返回取消函数(runAgent finally 必须调用)。
|
|
1111
1275
|
* 触发时:stage.guardReason/guardOutcome 记录原因 + journal 落日志 + dispose 中止本次尝试。
|
|
@@ -1123,7 +1287,9 @@ function startStageGuard(opts) {
|
|
|
1123
1287
|
const warnedReads = /* @__PURE__ */ new Set();
|
|
1124
1288
|
const scriptCounts = /* @__PURE__ */ new Map();
|
|
1125
1289
|
const warnedScripts = /* @__PURE__ */ new Set();
|
|
1126
|
-
|
|
1290
|
+
let lastMutationAt = 0;
|
|
1291
|
+
let repeatWarned = false;
|
|
1292
|
+
function warnOnce(key, set, message, hint) {
|
|
1127
1293
|
if (set.has(key)) return;
|
|
1128
1294
|
set.add(key);
|
|
1129
1295
|
try {
|
|
@@ -1133,6 +1299,7 @@ function startStageGuard(opts) {
|
|
|
1133
1299
|
message: `${label} [token 观测] ${message}`
|
|
1134
1300
|
});
|
|
1135
1301
|
} catch (e) {}
|
|
1302
|
+
if (hint) injectReminder(run, `[TOKEN GUARD · reminder] ${hint}`);
|
|
1136
1303
|
}
|
|
1137
1304
|
function fire(reason, outcome) {
|
|
1138
1305
|
if (fired) return;
|
|
@@ -1155,37 +1322,60 @@ function startStageGuard(opts) {
|
|
|
1155
1322
|
if (fired || journal.cancelled) return;
|
|
1156
1323
|
try {
|
|
1157
1324
|
const events = eventsOf(run);
|
|
1158
|
-
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1325
|
+
if (events.length > processed) {
|
|
1326
|
+
const newEvents = events.slice(processed);
|
|
1327
|
+
for (const ev of newEvents) {
|
|
1328
|
+
const e = ev;
|
|
1329
|
+
if (!e) continue;
|
|
1330
|
+
let streamText = null;
|
|
1331
|
+
if (e.type === "text-chunks" || e.type === "reasoning-chunks") {
|
|
1332
|
+
const arr = e.data && e.data.texts || e.texts;
|
|
1333
|
+
if (Array.isArray(arr)) streamText = arr.map(String).join("");
|
|
1334
|
+
} else if (e.type === "assistant/chunk") {
|
|
1335
|
+
const chunk = e.data && e.data.chunk || null;
|
|
1336
|
+
if (chunk && (chunk.type === "text-delta" || chunk.type === "reasoning-delta") && typeof chunk.text === "string") streamText = chunk.text;
|
|
1337
|
+
}
|
|
1338
|
+
if (streamText !== null && streamText.length > 0) {
|
|
1339
|
+
const s = normalizeFragment(streamText);
|
|
1340
|
+
if (s.length >= 12) window.push(s);
|
|
1341
|
+
lastGrowthAt = Date.now();
|
|
1342
|
+
}
|
|
1167
1343
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1344
|
+
if (window.length > 400) window.splice(0, window.length - 400);
|
|
1345
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1346
|
+
for (const w of window) counts.set(w, (counts.get(w) || 0) + 1);
|
|
1347
|
+
for (const [, n] of counts) if (n >= 12) {
|
|
1348
|
+
if (lastMutationAt > 0) {
|
|
1349
|
+
if (!repeatWarned) {
|
|
1350
|
+
repeatWarned = true;
|
|
1351
|
+
try {
|
|
1352
|
+
journal.logs.push({
|
|
1353
|
+
t: Date.now(),
|
|
1354
|
+
level: "warn",
|
|
1355
|
+
message: `${label} 复读计数达阈值但检测到变更进展(read-edit 循环属正常模式),不中止;仅零变更进展的纯复读才中止`
|
|
1356
|
+
});
|
|
1357
|
+
} catch (e) {}
|
|
1358
|
+
}
|
|
1359
|
+
} else {
|
|
1360
|
+
fire(`推理复读(同一片段在近 ${window.length} 条流式片段中出现 ${n} 次,且窗口内零变更进展)`, "degenerated");
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
observeToolCalls(newEvents);
|
|
1365
|
+
if (newEvents.some((ev) => ev?.type === "step/end")) flushReminders(run);
|
|
1366
|
+
processed = events.length;
|
|
1176
1367
|
}
|
|
1177
1368
|
for (const ev of events.slice(-200)) {
|
|
1178
1369
|
const e = ev;
|
|
1179
|
-
if (e
|
|
1370
|
+
if (!e) continue;
|
|
1371
|
+
if (e.type === "tool-call-chunks" || e.type === "tool/call") {
|
|
1180
1372
|
seenToolCall = true;
|
|
1181
1373
|
lastToolSignalAt = Date.now();
|
|
1374
|
+
const d = e.data || e;
|
|
1375
|
+
if (d && typeof d.name === "string" && PROGRESS_TOOLS.test(d.name)) lastMutationAt = Date.now();
|
|
1182
1376
|
break;
|
|
1183
1377
|
}
|
|
1184
1378
|
}
|
|
1185
|
-
if (events.length > processed) {
|
|
1186
|
-
observeToolCalls(events.slice(processed));
|
|
1187
|
-
processed = events.length;
|
|
1188
|
-
}
|
|
1189
1379
|
if (events.length !== lastEventCount) {
|
|
1190
1380
|
lastEventCount = events.length;
|
|
1191
1381
|
lastGrowthAt = Date.now();
|
|
@@ -1213,14 +1403,14 @@ function startStageGuard(opts) {
|
|
|
1213
1403
|
const key = m[1].replace(/\\\\/g, "\\").toLowerCase();
|
|
1214
1404
|
const n = (readCounts.get(key) || 0) + 1;
|
|
1215
1405
|
readCounts.set(key, n);
|
|
1216
|
-
if (n === 3) warnOnce(key, warnedReads, `同一文件重复 read ${n} 次:${m[1].split(/[\\\\/]/).pop()}(TOKEN_HYGIENE 上限 1 次,多余读取在为后续每一步付 cache
|
|
1406
|
+
if (n === 3) warnOnce(key, warnedReads, `同一文件重复 read ${n} 次:${m[1].split(/[\\\\/]/).pop()}(TOKEN_HYGIENE 上限 1 次,多余读取在为后续每一步付 cache 重放费)`, `你已整读 ${m[1].split(/[\\\\/]/).pop()} 第 3 次(TOKEN_HYGIENE 上限 1 次)。停止整读:需要确认细节时用 grep 定位行号 + 限量片段读取。`);
|
|
1217
1407
|
} else if (/bash|pwsh|shell|powershell/i.test(name || "")) {
|
|
1218
1408
|
const sm = String(args).match(/(verify-[a-z0-9-]+\.cjs|assembly-check\.cjs|qa-e2e-jsdom\.cjs)/);
|
|
1219
1409
|
if (!sm) continue;
|
|
1220
1410
|
const key = sm[1];
|
|
1221
1411
|
const n = (scriptCounts.get(key) || 0) + 1;
|
|
1222
1412
|
scriptCounts.set(key, n);
|
|
1223
|
-
if (n === 3) warnOnce(key, warnedScripts, `验证脚本重复执行 ${n} 次:${key}(批量修复纪律:一次修完所有失败再跑,≤3
|
|
1413
|
+
if (n === 3) warnOnce(key, warnedScripts, `验证脚本重复执行 ${n} 次:${key}(批量修复纪律:一次修完所有失败再跑,≤3 轮)`, `验证脚本 ${key} 已重复执行 3 次。遵守批量修复纪律:一次读完所有失败用例 → 一次全修 → 再跑一次;超出 3 轮请输出诊断摘要并停止。`);
|
|
1224
1414
|
}
|
|
1225
1415
|
}
|
|
1226
1416
|
}
|
|
@@ -1453,7 +1643,20 @@ async function withRetry(journal, parent, label, phase, prompt, signal) {
|
|
|
1453
1643
|
stageTokens
|
|
1454
1644
|
};
|
|
1455
1645
|
}
|
|
1456
|
-
if (
|
|
1646
|
+
if (lastStage && lastStage.outcome === "degenerated") {
|
|
1647
|
+
journal.logs.push({
|
|
1648
|
+
t: Date.now(),
|
|
1649
|
+
level: "warn",
|
|
1650
|
+
message: `${label} 进行中护栏中止(退化/推理复读),不再自动重试(污染会话内重试大概率复现且烧钱);可 teamflow_resume 以全新会话续跑`
|
|
1651
|
+
});
|
|
1652
|
+
journal.humanIntervention = true;
|
|
1653
|
+
return {
|
|
1654
|
+
text: null,
|
|
1655
|
+
attempts,
|
|
1656
|
+
stageTokens
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
if (stageTokens >= 6e4) {
|
|
1457
1660
|
journal.logs.push({
|
|
1458
1661
|
t: Date.now(),
|
|
1459
1662
|
level: "error",
|
|
@@ -1889,10 +2092,24 @@ ${clip(tech, 12e3)}` : ""}
|
|
|
1889
2092
|
3. If spec contradicts reality, explain with evidence in the summary instead of claiming completion or expanding scope on your own.
|
|
1890
2093
|
4. Actually write/modify code (grep + segmented reads to locate; no repeated whole-file reads), then run relevant build/verification to ensure green.
|
|
1891
2094
|
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 · hard (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.
|
|
1892
2096
|
6. [Log discipline] Redirect command output to logs/teamflow/${runId || "<runId>"}/.
|
|
1893
2097
|
7. Output an implementation summary (≤40 lines): changed files, key implementation points, how verified, leftovers. No big code pastes.
|
|
1894
2098
|
8. [State] End with a state block (phase="dev"), touched = array of changed files, summary = implementation conclusion.${STATE_BLOCK_INSTRUCTION}`;
|
|
1895
|
-
|
|
2099
|
+
/** 视觉验证能力条款(ADR-2026-08-27,解锁 browser-use 视觉验证):
|
|
2100
|
+
* 按当前模型多模态能力动态生成——vision=true 允许截图看图(人眼类项),精确值仍走 DOM 计算断言;
|
|
2101
|
+
* vision=false 禁截图看图(防幻觉/循环,历史禁令动机=模型不识图),只走 DOM 计算断言(evaluate 返回文本)。
|
|
2102
|
+
* 两者都要求:浏览器失败降级不重试。 */
|
|
2103
|
+
const VISUAL_POLICY = (vision) => vision ? `[Visual verification · enabled (model supports image input)]
|
|
2104
|
+
- Real-browser visual verification IS allowed: launch the page (headless browser / browser-use) and verify layout/pixel/overlay/occlusion items by screenshot + reading the image.
|
|
2105
|
+
- **Scripted assertions first** (exact values must come from DOM computation, not eyeballing): evaluate offsetWidth/scrollWidth/clientHeight for overflow, getComputedStyle for exact colors/visibility, element sizes & ratios (e.g. 1:2). Assert on those numbers/strings.
|
|
2106
|
+
- Screenshot checks are for human-eye items only: overlay occlusion, animation feel, layout reasonableness. Save screenshots under the task folder (docs/teamflow/.../qa/) for acceptance & human review.
|
|
2107
|
+
- On browser launch failure: degrade to jsdom/scripted checks + list remaining items in 「人工补测清单」; do NOT retry more than once.` : `[Visual verification · limited (current model has NO image input)]
|
|
2108
|
+
- You CANNOT interpret screenshots (no image input) — do NOT take screenshots to "look" at them (waste loop); do NOT guess layout/pixel state from screenshots.
|
|
2109
|
+
- Scripted DOM assertions ARE allowed and preferred: launch the page headless and evaluate TEXT values only — overflow (scrollWidth <= clientWidth), exact colors via getComputedStyle, visibility, element sizes/ratios. Assert on those numbers/strings.
|
|
2110
|
+
- Visual-judgment items (occlusion, animation feel, layout aesthetics) that cannot be asserted via DOM values: list them in 「人工补测清单」 (acceptance criterion + method + tool), note「环境限制,非交付缺陷」; do NOT guess, do NOT retry.
|
|
2111
|
+
- On any browser failure: degrade to jsdom + 「人工补测清单」, do not retry.`;
|
|
2112
|
+
const qaPrompt = (prd, devSummary, root, runId, state, vision) => `You are a senior QA test engineer. The current workspace IS the target project — functionally test this delivery.
|
|
1896
2113
|
${productCtx(root)}${stateSliceFor(state, "qa")}${TOKEN_HYGIENE(runId)}[PRD (this change & relevant ACs)]
|
|
1897
2114
|
${headTailClip(prd, 5e3, 7e3)}
|
|
1898
2115
|
[DEV RESULT SUMMARY]
|
|
@@ -1902,8 +2119,9 @@ ${clip(devSummary, 15e3)}
|
|
|
1902
2119
|
- If the injected blueprint JSON ("<!-- blueprint -->") is present, verify the implementation follows it (was the to-be-extracted module extracted? deps/assembly per blueprint? any deviations?).
|
|
1903
2120
|
- Check for **duplicated implementations** (e.g. multiple security wrappers/storage/adapter utilities drifting), **abstraction not extracted where it should be**, **obviously broken existing structure**.
|
|
1904
2121
|
- Report architecture findings in the defect table format (severity P1, module =「架构」). This is part of the delivery quality gate, not just functional bugs.
|
|
1905
|
-
1. [Environment limits ·
|
|
1906
|
-
|
|
2122
|
+
1. [Environment limits · dynamic by model capability]${VISUAL_POLICY(!!vision)}
|
|
2123
|
+
- Always-available sandbox-legal paths: build/assembly checks, unit tests, DOM-level E2E (jsdom or equivalent), static audit, adversarial spot-checks.
|
|
2124
|
+
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.
|
|
1907
2125
|
3. Read AGENTS.md §4 engineering conventions (verify commands) and the code changes first, then actually run those sandbox-legal verifications.
|
|
1908
2126
|
4. [Log discipline] Redirect command output to logs/teamflow/${runId || "<runId>"}/ (e.g. qa-out.log); no scatter at project root.
|
|
1909
2127
|
5. Output the test report (body ≤150 lines, verdict first): scope & environment, cases & results (pass/fail/blocked), conclusion (whether acceptance-ready).
|
|
@@ -1929,7 +2147,7 @@ ${tech && String(tech).trim() ? clip(tech, 12e3) : ""}
|
|
|
1929
2147
|
3. After fixing, run relevant verification to ensure green (regression floor: existing verify suites pass untouched); redirect output to logs/teamflow/${runId || "<runId>"}/.
|
|
1930
2148
|
4. Output a fix summary (≤40 lines, Chinese): per defect —「truth judgment + fix」or「false-positive evidence」, changed files, how verified, leftovers. No big code pastes.
|
|
1931
2149
|
5. [State] End with a state block (phase="dev"), touched = changed files array, summary = fix conclusion.${STATE_BLOCK_INSTRUCTION}`;
|
|
1932
|
-
const acceptancePrompt = (prd, qa, devSummary, root, runId, state) => `You are the product manager (acceptance lead). Do a final acceptance of this delivery against the PRD acceptance criteria.
|
|
2150
|
+
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.
|
|
1933
2151
|
${productCtx(root)}${stateSliceFor(state, "acceptance")}${TOKEN_HYGIENE(runId)}
|
|
1934
2152
|
${ONCE_DISCIPLINE}[PRD (revision log + this run's new ACs)]
|
|
1935
2153
|
${headTailClip(prd, 4e3, 5e3)}
|
|
@@ -1937,6 +2155,7 @@ ${headTailClip(prd, 4e3, 5e3)}
|
|
|
1937
2155
|
${clip(qa, 1e4)}
|
|
1938
2156
|
[DEV RESULT SUMMARY]
|
|
1939
2157
|
${clip(devSummary, 8e3)}
|
|
2158
|
+
${vision ? "[Visual re-check] If QA saved screenshots under the task folder, spot-check the human-eye items visually (occlusion / layout reasonableness / animation feel) before concluding; otherwise rely on the QA report + 人工补测清单." : ""}
|
|
1940
2159
|
[REQUIREMENTS]
|
|
1941
2160
|
0. [Architecture consistency check · mandatory (M3 quality gate)] Beyond functional ACs, check structural quality:
|
|
1942
2161
|
- If the injected blueprint JSON ("<!-- blueprint -->") is present: does the implementation follow it (module extracted as planned? assembly order correct? abstraction missing where required?).
|
|
@@ -2321,6 +2540,29 @@ function deliverCompletion(journal, parent) {
|
|
|
2321
2540
|
interrupted: "⚠ 中断(可用 teamflow_resume 从断点重跑)"
|
|
2322
2541
|
}[journal.status] || journal.status;
|
|
2323
2542
|
const stagesLine = stages.length === 0 ? "尚未进入任何阶段" : `${stages.length} 个阶段 · ${done} 完成${failed > 0 ? ` · ${failed} 失败` : ""}${cancelledStages > 0 ? ` · ${cancelledStages} 取消` : ""}`;
|
|
2543
|
+
let mergeHint = "";
|
|
2544
|
+
if (journal.status === "completed" && !journal.error && journal.workspacePath) try {
|
|
2545
|
+
const branch = gitCmd(journal.workspacePath, [
|
|
2546
|
+
"rev-parse",
|
|
2547
|
+
"--abbrev-ref",
|
|
2548
|
+
"HEAD"
|
|
2549
|
+
]);
|
|
2550
|
+
if (branch && branch !== "main") {
|
|
2551
|
+
const ahead = gitCmd(journal.workspacePath, [
|
|
2552
|
+
"rev-list",
|
|
2553
|
+
"--count",
|
|
2554
|
+
"main..HEAD"
|
|
2555
|
+
]);
|
|
2556
|
+
if (ahead && Number(ahead) > 0) {
|
|
2557
|
+
journal.mergeStatus = journal.mergeStatus || "pending";
|
|
2558
|
+
mergeHint = `🧩 合回决策:验收已通过,当前在特性分支 ${branch}(领先 main ${ahead} 个提交)。请**询问用户**是否合回,并按用户选择调用 teamflow_merge:\n ① host 代为合回 → action="merge"\n ② 给出命令用户自行合回 → action="command"\n ③ 暂不合回 → action="keep"\n(选项之外用户自定义输入亦可,如实转述)`;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
if ((journal.options || {}).preAction === "stash") {
|
|
2562
|
+
const stashHint = "🧩 启动前 stash 的改动仍在暂存区:流水线完成后请执行 git stash pop 恢复你的改动";
|
|
2563
|
+
mergeHint = mergeHint ? `${mergeHint}\n${stashHint}` : stashHint;
|
|
2564
|
+
}
|
|
2565
|
+
} catch (e) {}
|
|
2324
2566
|
const text = [
|
|
2325
2567
|
`【团队研发流水线汇报】runId=${journal.id}`,
|
|
2326
2568
|
`状态:${statusLine}${journal.error ? `(${clip(journal.error, 300)})` : ""}`,
|
|
@@ -2334,6 +2576,7 @@ function deliverCompletion(journal, parent) {
|
|
|
2334
2576
|
return typeof m === "string" && m !== "full" && m !== "medium" ? `模式:${MODE_REGISTRY[m] ? MODE_REGISTRY[m].label : m}` : "";
|
|
2335
2577
|
})(),
|
|
2336
2578
|
`backlog:需求 ${journal.reqId || "—"}($DSH_HOME/teamflow/ 持久化)`,
|
|
2579
|
+
mergeHint,
|
|
2337
2580
|
"用户可打开「🏭 团队工作台」tab 查看阶段泳道、拖拽看板与 token 明细。",
|
|
2338
2581
|
"如需继续处理:可认领缺陷(teamflow_claim)、人工流转(teamflow_update)、断点重跑(teamflow_resume)。",
|
|
2339
2582
|
"若用户在场请简明转述以上要点;若无人值守仅记录即可,不必长篇回复。"
|
|
@@ -2357,79 +2600,6 @@ function deliverCompletion(journal, parent) {
|
|
|
2357
2600
|
}
|
|
2358
2601
|
}
|
|
2359
2602
|
//#endregion
|
|
2360
|
-
//#region host/core/sanity.ts
|
|
2361
|
-
/**
|
|
2362
|
-
* dsh-plugin-teamflow core — M0 状态核对(sanity check)。
|
|
2363
|
-
*
|
|
2364
|
-
* 目的(三情况协议):
|
|
2365
|
-
* - 情况一(全新会话):0 认知 → 核对确认"从零开始"。
|
|
2366
|
-
* - 情况二(续会话):认知大概率过期(多人协作/场外提交/非流水线改动)→ 核对发现预期外变化。
|
|
2367
|
-
* - 情况三(新会话处理新需求):即使共用 state/记忆,仓库也可能被外部改动 → 核对现状。
|
|
2368
|
-
*
|
|
2369
|
-
* 核心原则:认知资产(索引/记忆/摘要)可复用"减量",但永不能替代"对代码库当前真实状态的核对"。
|
|
2370
|
-
* 本模块在 host 侧直接跑 git(零模型 token、轻量、失败优雅降级为"无法核对"),
|
|
2371
|
-
* 产出 externalDiffs 摘要注入到后续所有阶段 prompt。
|
|
2372
|
-
*/
|
|
2373
|
-
/** 单条 git 检查结果(失败返回 null,调用方降级)。 */
|
|
2374
|
-
function gitCmd(cwd, args, timeoutMs = 8e3) {
|
|
2375
|
-
try {
|
|
2376
|
-
return execFileSync("git", args, {
|
|
2377
|
-
cwd,
|
|
2378
|
-
encoding: "utf8",
|
|
2379
|
-
timeout: timeoutMs,
|
|
2380
|
-
windowsHide: true
|
|
2381
|
-
}).trim();
|
|
2382
|
-
} catch (e) {
|
|
2383
|
-
return null;
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
/**
|
|
2387
|
-
* 跑一次状态核对。
|
|
2388
|
-
* @param path - 工作区绝对路径(workspaceScopeOf(agent).path)。
|
|
2389
|
-
*/
|
|
2390
|
-
function runSanityCheck(path) {
|
|
2391
|
-
const defaultOut = {
|
|
2392
|
-
ok: false,
|
|
2393
|
-
inRepo: false,
|
|
2394
|
-
branch: null,
|
|
2395
|
-
dirty: "",
|
|
2396
|
-
hasDirty: false,
|
|
2397
|
-
recentCommits: "",
|
|
2398
|
-
summary: ""
|
|
2399
|
-
};
|
|
2400
|
-
if (!path) return {
|
|
2401
|
-
...defaultOut,
|
|
2402
|
-
summary: "⚠ 无法确定工作区路径,未做状态核对。"
|
|
2403
|
-
};
|
|
2404
|
-
const branch = gitCmd(path, ["branch", "--show-current"]);
|
|
2405
|
-
const status = gitCmd(path, ["status", "--short"]);
|
|
2406
|
-
if (branch === null && status === null) return {
|
|
2407
|
-
...defaultOut,
|
|
2408
|
-
summary: "⚠ 状态核对不可用(非 git 仓库或 git 命令被拒),无法确认代码库真实状态。"
|
|
2409
|
-
};
|
|
2410
|
-
const recent = gitCmd(path, [
|
|
2411
|
-
"log",
|
|
2412
|
-
"--oneline",
|
|
2413
|
-
"-5"
|
|
2414
|
-
]);
|
|
2415
|
-
const dirty = status || "";
|
|
2416
|
-
const dirtyLines = dirty.split("\n").filter(Boolean);
|
|
2417
|
-
const summaryParts = [];
|
|
2418
|
-
summaryParts.push(`分支:${branch && branch !== "HEAD" ? branch : "(detached HEAD / 非分支)"}`);
|
|
2419
|
-
if (dirtyLines.length) summaryParts.push(`未提交改动 ${dirtyLines.length} 处:${dirtyLines.slice(0, 8).join(";")}${dirtyLines.length > 8 ? "…" : ""}`);
|
|
2420
|
-
else summaryParts.push("工作区干净(无未提交改动)");
|
|
2421
|
-
if (recent) summaryParts.push(`近期提交(可能含场外改动):${recent.split("\n").slice(0, 3).join(";")}`);
|
|
2422
|
-
return {
|
|
2423
|
-
ok: true,
|
|
2424
|
-
inRepo: true,
|
|
2425
|
-
branch: branch && branch !== "HEAD" ? branch : null,
|
|
2426
|
-
dirty,
|
|
2427
|
-
hasDirty: dirtyLines.length > 0,
|
|
2428
|
-
recentCommits: recent || "",
|
|
2429
|
-
summary: `【状态核对】${summaryParts.join("。")}`
|
|
2430
|
-
};
|
|
2431
|
-
}
|
|
2432
|
-
//#endregion
|
|
2433
2603
|
//#region host/core/pipeline.ts
|
|
2434
2604
|
/**
|
|
2435
2605
|
* dsh-plugin-teamflow core — 流水线编排中枢(阶段执行 / 入口 / 取消 / 断点续跑)。
|
|
@@ -2454,14 +2624,38 @@ function buildResumeProducts(journal) {
|
|
|
2454
2624
|
return products;
|
|
2455
2625
|
}
|
|
2456
2626
|
/**
|
|
2457
|
-
*
|
|
2627
|
+
* 断点续跑起点:第一个「没有任意 done 尝试」的阶段。
|
|
2628
|
+
* ⚠️ 按阶段而非尝试判断(实锤 tf-mtcomxpq):PRD 第 1 次尝试 failed(护栏退化)但第 2 次重试 done——
|
|
2629
|
+
* 旧实现取第一个非 done stage → 断点错误回到 PRD,PRD/技术方案被无谓重跑。
|
|
2458
2630
|
* 全部完成仍被中断(理论极端)→ 从产品验收继续。
|
|
2459
2631
|
*/
|
|
2460
2632
|
function interruptedPhaseOf(journal) {
|
|
2461
|
-
|
|
2462
|
-
|
|
2633
|
+
if (hasOpenBlockingBugs(journal)) return "QA 测试";
|
|
2634
|
+
for (const phase of PHASE_ORDER) {
|
|
2635
|
+
const phaseStages = (journal.stages || []).filter((s) => s.phase === phase);
|
|
2636
|
+
if (phaseStages.length === 0) continue;
|
|
2637
|
+
if (!phaseStages.some((s) => s.status === "done")) return phase;
|
|
2638
|
+
}
|
|
2463
2639
|
return "产品验收";
|
|
2464
2640
|
}
|
|
2641
|
+
/** 开发任务定义(单一来源):架构蓝图自动拆 > 调用方显式 tasks > 整体开发兜底。
|
|
2642
|
+
* resume 补跑与正常执行共用(defByTitle 按 title 匹配失败子卡)。 */
|
|
2643
|
+
function buildDevTaskDefs(journal, tasks) {
|
|
2644
|
+
const blueprintTasks = journal.blueprint && Array.isArray(journal.blueprint.tasks) && journal.blueprint.tasks.length ? journal.blueprint.tasks.map((t) => ({
|
|
2645
|
+
title: t.title || "开发任务",
|
|
2646
|
+
files: Array.isArray(t.files) ? t.files : [],
|
|
2647
|
+
spec: t.spec || ""
|
|
2648
|
+
})) : [];
|
|
2649
|
+
return blueprintTasks.length ? blueprintTasks : tasks.length > 0 ? tasks.map((t) => ({
|
|
2650
|
+
title: t.title,
|
|
2651
|
+
spec: t.spec,
|
|
2652
|
+
files: []
|
|
2653
|
+
})) : [{
|
|
2654
|
+
title: "整体开发",
|
|
2655
|
+
spec: "按技术方案/需求实现全部改动",
|
|
2656
|
+
files: []
|
|
2657
|
+
}];
|
|
2658
|
+
}
|
|
2465
2659
|
/**
|
|
2466
2660
|
* 执行流水线。resume = null 全新运行;resume = { phase, products } 从断点续跑:
|
|
2467
2661
|
* phase 之前的阶段直接复用 products 产物(跳过执行),从 phase 阶段开始重跑。
|
|
@@ -2557,6 +2751,48 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2557
2751
|
level: "info",
|
|
2558
2752
|
message: `backlog 已建立需求 ${init.reqId}(产品 ${root || "unknown"},并发 ${maxConcurrency})`
|
|
2559
2753
|
});
|
|
2754
|
+
if (options.branchPolicy !== "keep" && journal.workspacePath) try {
|
|
2755
|
+
const s = runSanityCheck(journal.workspacePath);
|
|
2756
|
+
if (s.ok && s.inRepo && !s.hasDirty) {
|
|
2757
|
+
const branch = `feat/${deriveBranchSlug(requirement, journal.reqId, triageSlug, options.branchName)}`;
|
|
2758
|
+
const exists = gitCmd(journal.workspacePath, [
|
|
2759
|
+
"branch",
|
|
2760
|
+
"--list",
|
|
2761
|
+
branch
|
|
2762
|
+
]);
|
|
2763
|
+
if (exists && exists.trim()) {
|
|
2764
|
+
journal.logs.push({
|
|
2765
|
+
t: Date.now(),
|
|
2766
|
+
level: "warn",
|
|
2767
|
+
message: `分支策略:分支 ${branch} 已存在,沿用现有分支`
|
|
2768
|
+
});
|
|
2769
|
+
gitCmd(journal.workspacePath, ["checkout", branch]);
|
|
2770
|
+
journal.branch = branch;
|
|
2771
|
+
} else {
|
|
2772
|
+
const ok = gitCmd(journal.workspacePath, [
|
|
2773
|
+
"checkout",
|
|
2774
|
+
"-b",
|
|
2775
|
+
branch
|
|
2776
|
+
]);
|
|
2777
|
+
journal.logs.push({
|
|
2778
|
+
t: Date.now(),
|
|
2779
|
+
level: "info",
|
|
2780
|
+
message: ok !== null ? `分支策略:已创建特性分支 ${branch}(从 ${s.branch} 派生)` : `分支策略:自动建分支 ${branch} 失败(git 异常),沿用当前分支`
|
|
2781
|
+
});
|
|
2782
|
+
if (ok !== null) journal.branch = branch;
|
|
2783
|
+
}
|
|
2784
|
+
} else if (s.ok && s.inRepo && s.branch) journal.logs.push({
|
|
2785
|
+
t: Date.now(),
|
|
2786
|
+
level: "warn",
|
|
2787
|
+
message: `分支策略:工作区仍有未提交改动(${s.dirty.split(/\r?\n/).filter((l) => l.trim()).length} 处),跳过建分支,沿用 ${s.branch}——请知悉改动将混入本次开发`
|
|
2788
|
+
});
|
|
2789
|
+
} catch (e) {
|
|
2790
|
+
journal.logs.push({
|
|
2791
|
+
t: Date.now(),
|
|
2792
|
+
level: "warn",
|
|
2793
|
+
message: `分支策略检查失败(降级沿用当前分支):${String(e && e.message || e)}`
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2560
2796
|
}
|
|
2561
2797
|
persistJournal(journal);
|
|
2562
2798
|
if (!journal.runDocs && journal.reqId) {
|
|
@@ -2586,6 +2822,49 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2586
2822
|
});
|
|
2587
2823
|
}
|
|
2588
2824
|
}
|
|
2825
|
+
if (!resume && journal.workspacePath && options.preAction === "stash") try {
|
|
2826
|
+
if (gitCmd(journal.workspacePath, [
|
|
2827
|
+
"stash",
|
|
2828
|
+
"push",
|
|
2829
|
+
"-m",
|
|
2830
|
+
`teamflow:${journal.id} pre-pipeline`
|
|
2831
|
+
]) !== null) journal.logs.push({
|
|
2832
|
+
t: Date.now(),
|
|
2833
|
+
level: "info",
|
|
2834
|
+
message: `分支策略:工作区改动已 stash(teamflow:${journal.id}),流水线完成后 git stash pop 恢复你的改动`
|
|
2835
|
+
});
|
|
2836
|
+
else journal.logs.push({
|
|
2837
|
+
t: Date.now(),
|
|
2838
|
+
level: "warn",
|
|
2839
|
+
message: "分支策略:stash 执行失败(可能无改动),继续"
|
|
2840
|
+
});
|
|
2841
|
+
} catch (e) {
|
|
2842
|
+
journal.logs.push({
|
|
2843
|
+
t: Date.now(),
|
|
2844
|
+
level: "warn",
|
|
2845
|
+
message: `分支策略:stash 异常:${String(e && e.message || e)}`
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
else if (!resume && journal.workspacePath && options.preAction === "commit") try {
|
|
2849
|
+
const msg = typeof options.commitMessage === "string" && options.commitMessage.trim() ? options.commitMessage.trim() : `chore(teamflow): 流水线启动前提交现有改动(${journal.id})`;
|
|
2850
|
+
const add = gitCmd(journal.workspacePath, ["add", "-A"]);
|
|
2851
|
+
const cm = gitCmd(journal.workspacePath, [
|
|
2852
|
+
"commit",
|
|
2853
|
+
"-m",
|
|
2854
|
+
msg
|
|
2855
|
+
]);
|
|
2856
|
+
journal.logs.push({
|
|
2857
|
+
t: Date.now(),
|
|
2858
|
+
level: "info",
|
|
2859
|
+
message: add !== null && cm !== null ? `分支策略:已提交现有改动(preAction=commit:${msg.slice(0, 60)})` : "分支策略:commit 执行失败(可能无改动),继续"
|
|
2860
|
+
});
|
|
2861
|
+
} catch (e) {
|
|
2862
|
+
journal.logs.push({
|
|
2863
|
+
t: Date.now(),
|
|
2864
|
+
level: "warn",
|
|
2865
|
+
message: `分支策略:commit 异常:${String(e && e.message || e)}`
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2589
2868
|
const state = loadState(journal.workspace || "default");
|
|
2590
2869
|
state.__runCtx = { ...state.__runCtx || {} };
|
|
2591
2870
|
if (journal.runDocs) state.__runCtx.runDocs = journal.runDocs;
|
|
@@ -2743,28 +3022,50 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2743
3022
|
let devResults = null;
|
|
2744
3023
|
if (resumed("开发")) {
|
|
2745
3024
|
devResults = resume.products.dev || [];
|
|
2746
|
-
|
|
2747
|
-
|
|
3025
|
+
const failedSubs = (storeFor(scopeKey).tasks || []).filter((t) => t.reqId === journal.reqId && t.parentId === journal.taskId && (t.status === "failed" || t.failed === true));
|
|
3026
|
+
if (failedSubs.length > 0) {
|
|
3027
|
+
const defs = buildDevTaskDefs(journal, tasks);
|
|
3028
|
+
const defByTitle = new Map(defs.map((d) => [d.title, d]));
|
|
3029
|
+
const rerunDefs = failedSubs.map((s) => {
|
|
3030
|
+
const key = String(s.title || "").replace(/^开发 · /, "");
|
|
3031
|
+
return defByTitle.get(key) || {
|
|
3032
|
+
title: key,
|
|
3033
|
+
spec: s.spec || "按技术方案实现该任务改动",
|
|
3034
|
+
files: []
|
|
3035
|
+
};
|
|
3036
|
+
}).filter(Boolean);
|
|
3037
|
+
const reused = devResults.filter((r) => r && !rerunDefs.some((d) => d.title === r.title));
|
|
3038
|
+
journal.logs.push({
|
|
3039
|
+
t: Date.now(),
|
|
3040
|
+
level: "warn",
|
|
3041
|
+
message: `断点续跑开发:复用 ${reused.length} 个已完成任务,补跑 ${rerunDefs.length} 个失败任务`
|
|
3042
|
+
});
|
|
3043
|
+
const rerun = await runPool(rerunDefs, maxConcurrency, async (task) => {
|
|
3044
|
+
const devR = await withRetry(journal, parent, `开发 · ${task.title}(补跑)`, "开发", devPrompt(task, tech, prd, root, journal.id, state), signal);
|
|
3045
|
+
const ok = !!devR.text;
|
|
3046
|
+
return {
|
|
3047
|
+
title: task.title,
|
|
3048
|
+
failed: !ok,
|
|
3049
|
+
output: devR.text || "开发失败(Agent 未产出结果)"
|
|
3050
|
+
};
|
|
3051
|
+
});
|
|
3052
|
+
for (const t of rerun) {
|
|
3053
|
+
const sub = failedSubs.find((s) => String(s.title || "").replace(/^开发 · /, "") === t.title);
|
|
3054
|
+
if (sub) completeSubtask(journal, sub.id, t.failed, t.output ? snippet(t.output, 1e3) : null, null);
|
|
3055
|
+
}
|
|
3056
|
+
devResults = [...reused, ...rerun];
|
|
3057
|
+
timeline.dev = devResults;
|
|
3058
|
+
} else {
|
|
3059
|
+
timeline.dev = devResults;
|
|
3060
|
+
logSkip("开发");
|
|
3061
|
+
}
|
|
2748
3062
|
} else {
|
|
2749
3063
|
journal.logs.push({
|
|
2750
3064
|
t: Date.now(),
|
|
2751
3065
|
level: "phase",
|
|
2752
3066
|
message: "进入阶段:开发"
|
|
2753
3067
|
});
|
|
2754
|
-
const
|
|
2755
|
-
title: t.title || "开发任务",
|
|
2756
|
-
files: Array.isArray(t.files) ? t.files : [],
|
|
2757
|
-
spec: t.spec || ""
|
|
2758
|
-
})) : [];
|
|
2759
|
-
const devTaskDefs = blueprintTasks.length ? blueprintTasks : tasks.length > 0 ? tasks.map((t) => ({
|
|
2760
|
-
title: t.title,
|
|
2761
|
-
spec: t.spec,
|
|
2762
|
-
files: []
|
|
2763
|
-
})) : [{
|
|
2764
|
-
title: "整体开发",
|
|
2765
|
-
spec: "按技术方案/需求实现全部改动",
|
|
2766
|
-
files: []
|
|
2767
|
-
}];
|
|
3068
|
+
const devTaskDefs = buildDevTaskDefs(journal, tasks);
|
|
2768
3069
|
const mergedDefs = [];
|
|
2769
3070
|
for (const t of devTaskDefs) {
|
|
2770
3071
|
const hit = t.files && t.files.length ? mergedDefs.find((m) => m.files.some((f) => t.files.includes(f))) : void 0;
|
|
@@ -2781,7 +3082,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2781
3082
|
journal.logs.push({
|
|
2782
3083
|
t: Date.now(),
|
|
2783
3084
|
level: "info",
|
|
2784
|
-
message: `开发阶段开始,任务数:${mergedDefs.length}(并发 ${maxConcurrency}${
|
|
3085
|
+
message: `开发阶段开始,任务数:${mergedDefs.length}(并发 ${maxConcurrency}${journal.blueprint && Array.isArray(journal.blueprint.tasks) && journal.blueprint.tasks.length ? ",源自架构蓝图自动拆解" : ""})`
|
|
2785
3086
|
});
|
|
2786
3087
|
advanceTask(journal, "running", null, "开发开始(待办 → 开发中)", { by: "dev" });
|
|
2787
3088
|
const subCards = mergedDefs.map((dt) => createSubtask(journal, dt.title, dt.spec));
|
|
@@ -2825,9 +3126,10 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2825
3126
|
}
|
|
2826
3127
|
journal.logs.push({
|
|
2827
3128
|
t: Date.now(),
|
|
2828
|
-
level: "
|
|
2829
|
-
message:
|
|
3129
|
+
level: "error",
|
|
3130
|
+
message: `开发失败任务数:${failedCount}/${devResults.length}(提测门禁拦截):停止流水线,需人工介入——处理后可 teamflow_resume 从开发阶段补跑失败任务(已完成任务复用)`
|
|
2830
3131
|
});
|
|
3132
|
+
throw new Error(`开发失败任务 ${failedCount}/${devResults.length}:提测门禁拦截,不进 QA(已知缺口,QA 检查轮必然重复报告)`);
|
|
2831
3133
|
} else {
|
|
2832
3134
|
advanceTask(journal, "testable", null, "开发完成待测试(开发中 → 待测试)", { by: "dev" });
|
|
2833
3135
|
journal.logs.push({
|
|
@@ -2848,7 +3150,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2848
3150
|
message: "当前档位阶段集不含独立 QA:跳过(单点修复,开发自测兜底)"
|
|
2849
3151
|
});
|
|
2850
3152
|
qa = "(独立 QA 跳过:当前档位由开发自测兜底)";
|
|
2851
|
-
} else if (resumed("QA 测试")) {
|
|
3153
|
+
} else if (resumed("QA 测试") && !hasOpenBlockingBugs(journal)) {
|
|
2852
3154
|
qa = resume.products.qa;
|
|
2853
3155
|
timeline.qa = qa;
|
|
2854
3156
|
logSkip("QA 测试");
|
|
@@ -2872,7 +3174,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2872
3174
|
do {
|
|
2873
3175
|
round += 1;
|
|
2874
3176
|
const isReverify = round > 1;
|
|
2875
|
-
const qaR = await withRetry(journal, parent, isReverify ? `QA 复验 · 第${round - 1}轮修复后` : "QA 测试工程师 · 功能测试", "QA 测试", qaPrompt(prd, qaDevSummary(), root, journal.id, state), signal);
|
|
3177
|
+
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);
|
|
2876
3178
|
if (!qaR.text) {
|
|
2877
3179
|
advanceTask(journal, "needs-human", null, isReverify ? `QA 复验失败(第 ${round - 1} 轮修复后)` : "QA 失败", { by: "qa" });
|
|
2878
3180
|
throw stageFailError(isReverify ? "QA 测试(复验)" : "QA 测试", qaR);
|
|
@@ -2952,7 +3254,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2952
3254
|
const curTask = storeFor(scopeKey).find("task", journal.taskId);
|
|
2953
3255
|
if (curTask && curTask.status !== "pending-acceptance" && curTask.status !== "needs-human" && curTask.status !== "rework") advanceTask(journal, "pending-acceptance", null, "进入验收(待验收)", { by: "pm" });
|
|
2954
3256
|
}
|
|
2955
|
-
const accR = await withRetry(journal, parent, "产品经理 · 最终验收", "产品验收", acceptancePrompt(prd, qa, JSON.stringify(timeline.dev), root, journal.id, state), signal);
|
|
3257
|
+
const accR = await withRetry(journal, parent, "产品经理 · 最终验收", "产品验收", acceptancePrompt(prd, qa, JSON.stringify(timeline.dev), root, journal.id, state, await currentModelSupportsVision(resolveChildRoute(parent).provider, resolveChildRoute(parent).model)), signal);
|
|
2956
3258
|
if (!accR.text) {
|
|
2957
3259
|
advanceTask(journal, "needs-human", null, "验收失败", { by: "pm" });
|
|
2958
3260
|
throw stageFailError("产品验收", accR);
|
|
@@ -3021,6 +3323,35 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3021
3323
|
journal.endedAt = Date.now();
|
|
3022
3324
|
inFlight.delete(journal.id);
|
|
3023
3325
|
activeProducts.delete(scopeKey);
|
|
3326
|
+
if (journal.workspacePath && journal.status === "completed" && !journal.humanIntervention) try {
|
|
3327
|
+
const reqHead = String(journal.requirement || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
3328
|
+
const add = gitCmd(journal.workspacePath, ["add", "-A"]);
|
|
3329
|
+
if ((add === null ? null : gitCmd(journal.workspacePath, [
|
|
3330
|
+
"commit",
|
|
3331
|
+
"-m",
|
|
3332
|
+
`feat: ${reqHead}(runId=${journal.id}${journal.runDocs ? `,任务夹 ${journal.runDocs}` : ""})`
|
|
3333
|
+
])) !== null) journal.logs.push({
|
|
3334
|
+
t: Date.now(),
|
|
3335
|
+
level: "info",
|
|
3336
|
+
message: "统一收口提交完成(代码 + 任务夹产物,验收通过后一个 commit)"
|
|
3337
|
+
});
|
|
3338
|
+
else if (add !== null) journal.logs.push({
|
|
3339
|
+
t: Date.now(),
|
|
3340
|
+
level: "info",
|
|
3341
|
+
message: "统一收口提交:无待提交改动(跳过)"
|
|
3342
|
+
});
|
|
3343
|
+
} catch (e) {
|
|
3344
|
+
journal.logs.push({
|
|
3345
|
+
t: Date.now(),
|
|
3346
|
+
level: "warn",
|
|
3347
|
+
message: `统一收口提交失败(忽略):${String(e && e.message || e)}`
|
|
3348
|
+
});
|
|
3349
|
+
}
|
|
3350
|
+
else if (journal.workspacePath && journal.runDocs && (journal.status === "failed" || journal.status === "cancelled" || journal.status === "interrupted")) journal.logs.push({
|
|
3351
|
+
t: Date.now(),
|
|
3352
|
+
level: "warn",
|
|
3353
|
+
message: `终态非验收通过:工作区改动与任务夹产物 ${journal.runDocs}/ 保留未提交,供人工处理(resume 修复通过后自动收口提交)`
|
|
3354
|
+
});
|
|
3024
3355
|
journal.result = {
|
|
3025
3356
|
requirement,
|
|
3026
3357
|
options: sanitizeSnapOptions(options),
|
|
@@ -3098,7 +3429,11 @@ function startPipeline(agent, requirement, options, signal) {
|
|
|
3098
3429
|
teamId: options.teamId || void 0,
|
|
3099
3430
|
tasks: normalizeTasks(options.tasks),
|
|
3100
3431
|
productRoot: normalizeRoot(options.productRoot),
|
|
3101
|
-
maxConcurrency: Number.isFinite(options.maxConcurrency) && options.maxConcurrency > 0 ? Math.min(options.maxConcurrency, 8) : null
|
|
3432
|
+
maxConcurrency: Number.isFinite(options.maxConcurrency) && options.maxConcurrency > 0 ? Math.min(options.maxConcurrency, 8) : null,
|
|
3433
|
+
branchPolicy: options.branchPolicy || void 0,
|
|
3434
|
+
branchName: options.branchName || void 0,
|
|
3435
|
+
preAction: options.preAction || void 0,
|
|
3436
|
+
commitMessage: options.commitMessage || void 0
|
|
3102
3437
|
},
|
|
3103
3438
|
startedAt: null,
|
|
3104
3439
|
endedAt: null,
|
|
@@ -3201,7 +3536,7 @@ function resumeRun(runId, sessionId) {
|
|
|
3201
3536
|
level: "warn",
|
|
3202
3537
|
message: `断点续跑:从「${resumePhase}」继续(已完成阶段复用产物)`
|
|
3203
3538
|
});
|
|
3204
|
-
j.stages = (j.stages || []).filter((s) => s.status
|
|
3539
|
+
j.stages = (j.stages || []).filter((s) => s.status !== "running" && s.status !== "pending");
|
|
3205
3540
|
runs.set(id, j);
|
|
3206
3541
|
persistJournal(j);
|
|
3207
3542
|
executePipeline(j, agent, j.requirement, j.options, void 0, {
|
|
@@ -3303,7 +3638,7 @@ function registerTools(ctx) {
|
|
|
3303
3638
|
};
|
|
3304
3639
|
T({
|
|
3305
3640
|
name: "teamflow_start",
|
|
3306
|
-
description: "Start the team R&D pipeline (background async): runs the stages per team config (PRD→design→tech→dev→QA→acceptance). Specify teamId (matches teams.json) or pick a team via the UI \"+\" button first so messages auto-match. Stage failures auto-retry; beyond threshold → rework/human intervention; per-stage token usage recorded. NOTE: after calling, the implementation work is done by pipeline subagents — the main thread MUST NOT write code or run verifications for it. requirement must be a faithful transcription of the user's words; do not invent file paths / tech claims without code verification (downstream stages build the PRD from it).",
|
|
3641
|
+
description: "Start the team R&D pipeline (background async): runs the stages per team config (PRD→design→tech→dev→QA→acceptance). Specify teamId (matches teams.json) or pick a team via the UI \"+\" button first so messages auto-match. Stage failures auto-retry; beyond threshold → rework/human intervention; per-stage token usage recorded. NOTE: after calling, the implementation work is done by pipeline subagents — the main thread MUST NOT write code or run verifications for it. requirement must be a faithful transcription of the user's words; do not invent file paths / tech claims without code verification (downstream stages build the PRD from it). Branch decision: when the return status is \"needs-decision\", ASK THE USER to pick one of the options (or take their custom input, e.g. a branch name), then RE-CALL this tool passing the CHOSEN OPTION VALUE as branchPolicy (\"new\" = confirmed create branch, \"keep\" = stay), optionally combined with preAction / branchName / commitMessage. Pass branchPolicy=\"keep\" when the user chooses to stay on the current branch.",
|
|
3307
3642
|
parameters: {
|
|
3308
3643
|
requirement: {
|
|
3309
3644
|
type: "string",
|
|
@@ -3338,6 +3673,22 @@ function registerTools(ctx) {
|
|
|
3338
3673
|
type: "integer",
|
|
3339
3674
|
description: "Dev task concurrency (default 3, max 8)"
|
|
3340
3675
|
},
|
|
3676
|
+
branchPolicy: {
|
|
3677
|
+
type: "string",
|
|
3678
|
+
description: "Branch policy: \"auto\" (default, triggers needs-decision when not yet confirmed) — create a feature branch feat/<branchName|slug> from the current HEAD; \"keep\" — stay on current branch; \"new\" — the confirmed value returned by needs-decision options (user already picked \"create branch\"), pass it back as-is to proceed. When auto and a decision is needed (dirty workspace / on main etc.), the tool returns needs-decision for you to ask the user first."
|
|
3679
|
+
},
|
|
3680
|
+
branchName: {
|
|
3681
|
+
type: "string",
|
|
3682
|
+
description: "Custom branch name (used when branchPolicy=auto; defaults to the triage slug; [a-z0-9-_])"
|
|
3683
|
+
},
|
|
3684
|
+
preAction: {
|
|
3685
|
+
type: "string",
|
|
3686
|
+
description: "Pre-start handling of dirty workspace: \"stash\" (stash changes, restore later via git stash pop), \"commit\" (commit existing changes, custom commitMessage), omit = leave as-is (changes mix into this run)"
|
|
3687
|
+
},
|
|
3688
|
+
commitMessage: {
|
|
3689
|
+
type: "string",
|
|
3690
|
+
description: "Custom commit message when preAction=commit"
|
|
3691
|
+
},
|
|
3341
3692
|
tasks: {
|
|
3342
3693
|
type: "array",
|
|
3343
3694
|
description: "Optional splittable dev task list",
|
|
@@ -3362,16 +3713,32 @@ function registerTools(ctx) {
|
|
|
3362
3713
|
schema: {
|
|
3363
3714
|
type: "object",
|
|
3364
3715
|
additionalProperties: false,
|
|
3365
|
-
required: ["
|
|
3716
|
+
required: ["status"],
|
|
3366
3717
|
properties: {
|
|
3367
3718
|
runId: { type: "string" },
|
|
3368
|
-
status: { type: "string" }
|
|
3719
|
+
status: { type: "string" },
|
|
3720
|
+
question: { type: "string" },
|
|
3721
|
+
options: { type: "array" },
|
|
3722
|
+
note: { type: "string" }
|
|
3369
3723
|
}
|
|
3370
3724
|
},
|
|
3371
|
-
render: (args, value) =>
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3725
|
+
render: (args, value) => {
|
|
3726
|
+
if (value && value.status === "needs-decision") {
|
|
3727
|
+
const opts = Array.isArray(value.options) ? value.options.map((o, i) => `${i + 1}. ${o.label}`).join("\n") : "";
|
|
3728
|
+
return [{
|
|
3729
|
+
type: "text",
|
|
3730
|
+
text: `【分支决策】${value.question}\n${opts}\n(也可自定义输入)——请询问用户选择,确认后把所选选项的 value 作为 branchPolicy 重新调用 teamflow_start(如 'new'/'keep';脏工作区选项可拆为 branchPolicy + preAction 组合),自定义分支名则传 branchName。`
|
|
3731
|
+
}];
|
|
3732
|
+
}
|
|
3733
|
+
if (value && value.status === "needs-confirmation") return [{
|
|
3734
|
+
type: "text",
|
|
3735
|
+
text: `【需求确认】${value.question}\n${value.note || ""}——请按此询问用户后再决定。`
|
|
3736
|
+
}];
|
|
3737
|
+
return [{
|
|
3738
|
+
type: "text",
|
|
3739
|
+
text: `团队研发流水线已启动(runId=${value.runId},${value.status}),正在后台执行。【重要】你现在停手:不要自行读取/修改代码实现该需求,不要重复跑测试验证——实现、QA、汇报由流水线各阶段完成。你只需告知用户流水线已启动,等待流水线完成后的官方完成汇报,再向用户转述结果。可用 teamflow_status 查询进度/阶段 token;backlog 已持久化到 $DSH_HOME/teamflow。`
|
|
3740
|
+
}];
|
|
3741
|
+
}
|
|
3375
3742
|
},
|
|
3376
3743
|
async execute(args, exec) {
|
|
3377
3744
|
const parent = exec && exec.agent;
|
|
@@ -3388,18 +3755,105 @@ function registerTools(ctx) {
|
|
|
3388
3755
|
status: "no-team",
|
|
3389
3756
|
message: "请先通过输入框旁的 🏭 按钮选择团队,再发送需求消息。未选团队时不走 teamflow。"
|
|
3390
3757
|
};
|
|
3758
|
+
const rawReq = typeof args.requirement === "string" ? args.requirement : "";
|
|
3759
|
+
if (/是不是|要不要|需不需要|是否应该|要不要考虑|建议|我感觉|感觉不出|我们是不是|咱是不是/.test(rawReq)) return {
|
|
3760
|
+
status: "needs-confirmation",
|
|
3761
|
+
question: `这条消息(「${rawReq.slice(0, 60)}」)更像反馈/建议(含疑问句式)而非明确开发需求。请先向用户确认:是否要实现?`,
|
|
3762
|
+
note: "用户确认要实现后,请把明确需求(如「实现 combo/t-spin 触发 Toast 提示」)作为 requirement 重新调用 teamflow_start;若用户只是表达感受/讨论,直接正常回复即可。"
|
|
3763
|
+
};
|
|
3391
3764
|
try {
|
|
3765
|
+
const requirement = typeof args.requirement === "string" && args.requirement.trim() ? args.requirement.trim() : "(未提供需求)";
|
|
3766
|
+
const options = {
|
|
3767
|
+
needDesign: !!args.needDesign,
|
|
3768
|
+
needScaffold: !!args.needScaffold,
|
|
3769
|
+
lite: !!args.lite,
|
|
3770
|
+
mode: normalizeMode(args.mode) || void 0,
|
|
3771
|
+
teamId,
|
|
3772
|
+
tasks: normalizeTasks(args.tasks),
|
|
3773
|
+
productRoot: normalizeRoot(args.productRoot),
|
|
3774
|
+
maxConcurrency: args.maxConcurrency,
|
|
3775
|
+
branchPolicy: args.branchPolicy === "keep" ? "keep" : "auto",
|
|
3776
|
+
branchName: typeof args.branchName === "string" && args.branchName.trim() ? args.branchName.trim() : null,
|
|
3777
|
+
preAction: args.preAction === "stash" || args.preAction === "commit" ? args.preAction : null,
|
|
3778
|
+
commitMessage: typeof args.commitMessage === "string" && args.commitMessage.trim() ? args.commitMessage.trim() : null
|
|
3779
|
+
};
|
|
3780
|
+
const branchConfirmed = args.branchPolicy === "new";
|
|
3781
|
+
if (options.branchPolicy === "auto" && !branchConfirmed && !options.branchName && !options.preAction && exec && exec.agent) {
|
|
3782
|
+
const sc = workspaceScopeOf(exec.agent);
|
|
3783
|
+
if (sc.path) try {
|
|
3784
|
+
const s = runSanityCheck(sc.path);
|
|
3785
|
+
if (s.ok && s.inRepo) {
|
|
3786
|
+
const onMain = !!s.branch && s.branch.trim().toLowerCase() === "main";
|
|
3787
|
+
const dirty = s.hasDirty;
|
|
3788
|
+
const dirtyN = s.dirty.split(/\r?\n/).filter((l) => l.trim()).length;
|
|
3789
|
+
let question = "";
|
|
3790
|
+
let optionsList = [];
|
|
3791
|
+
if (onMain && !dirty) {
|
|
3792
|
+
question = `工作区 ${sc.path} 当前在 main 分支(工作区干净)。流水线默认在特性分支上开发,请选择:`;
|
|
3793
|
+
optionsList = [{
|
|
3794
|
+
label: "基于 main 新建分支开发(推荐,分支名取需求 slug,可自定义)",
|
|
3795
|
+
value: "new"
|
|
3796
|
+
}, {
|
|
3797
|
+
label: "直接在 main 上开发",
|
|
3798
|
+
value: "keep"
|
|
3799
|
+
}];
|
|
3800
|
+
} else if (onMain && dirty) {
|
|
3801
|
+
question = `工作区 ${sc.path} 当前在 main 分支,且有 ${dirtyN} 处未提交改动。请选择启动方式:`;
|
|
3802
|
+
optionsList = [
|
|
3803
|
+
{
|
|
3804
|
+
label: `stash 现有改动后新建分支开发(推荐,改动暂存,流水线完成后 git stash pop 恢复)`,
|
|
3805
|
+
value: "stash+auto"
|
|
3806
|
+
},
|
|
3807
|
+
{
|
|
3808
|
+
label: "提交现有改动后新建分支开发(提交信息可自定义)",
|
|
3809
|
+
value: "commit+auto"
|
|
3810
|
+
},
|
|
3811
|
+
{
|
|
3812
|
+
label: "直接在 main 上继续(未提交改动将混入本次开发)",
|
|
3813
|
+
value: "keep"
|
|
3814
|
+
}
|
|
3815
|
+
];
|
|
3816
|
+
} else if (!onMain && !dirty) {
|
|
3817
|
+
question = `工作区 ${sc.path} 当前在特性分支 ${s.branch}(工作区干净)。请选择启动方式:`;
|
|
3818
|
+
optionsList = [{
|
|
3819
|
+
label: "沿用当前分支开发(推荐)",
|
|
3820
|
+
value: "keep"
|
|
3821
|
+
}, {
|
|
3822
|
+
label: "基于当前分支再新建子分支开发",
|
|
3823
|
+
value: "new"
|
|
3824
|
+
}];
|
|
3825
|
+
} else {
|
|
3826
|
+
question = `工作区 ${sc.path} 当前在特性分支 ${s.branch},且有 ${dirtyN} 处未提交改动。请选择启动方式:`;
|
|
3827
|
+
optionsList = [
|
|
3828
|
+
{
|
|
3829
|
+
label: "stash 现有改动后沿用当前分支开发(推荐,完成后 git stash pop 恢复)",
|
|
3830
|
+
value: "stash+keep"
|
|
3831
|
+
},
|
|
3832
|
+
{
|
|
3833
|
+
label: "直接沿用当前分支(未提交改动混入本次开发)",
|
|
3834
|
+
value: "keep"
|
|
3835
|
+
},
|
|
3836
|
+
{
|
|
3837
|
+
label: "stash 现有改动后新建子分支开发",
|
|
3838
|
+
value: "stash+auto"
|
|
3839
|
+
},
|
|
3840
|
+
{
|
|
3841
|
+
label: "提交现有改动后新建子分支开发",
|
|
3842
|
+
value: "commit+auto"
|
|
3843
|
+
}
|
|
3844
|
+
];
|
|
3845
|
+
}
|
|
3846
|
+
return {
|
|
3847
|
+
status: "needs-decision",
|
|
3848
|
+
question,
|
|
3849
|
+
options: optionsList,
|
|
3850
|
+
note: "选项之外可自定义输入(如指定分支名)。确认选择后,请以 teamflow_start 的 branchPolicy(回传所选选项 value,如 new/keep)与 branchName/preAction/commitMessage 参数重新调用本工具。"
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
} catch (e) {}
|
|
3854
|
+
}
|
|
3392
3855
|
return {
|
|
3393
|
-
runId: startPipeline(parent,
|
|
3394
|
-
needDesign: !!args.needDesign,
|
|
3395
|
-
needScaffold: !!args.needScaffold,
|
|
3396
|
-
lite: !!args.lite,
|
|
3397
|
-
mode: normalizeMode(args.mode) || void 0,
|
|
3398
|
-
teamId,
|
|
3399
|
-
tasks: normalizeTasks(args.tasks),
|
|
3400
|
-
productRoot: normalizeRoot(args.productRoot),
|
|
3401
|
-
maxConcurrency: args.maxConcurrency
|
|
3402
|
-
}, exec && exec.signal),
|
|
3856
|
+
runId: startPipeline(parent, requirement, options, exec && exec.signal),
|
|
3403
3857
|
status: "running"
|
|
3404
3858
|
};
|
|
3405
3859
|
} catch (e) {
|
|
@@ -3407,6 +3861,88 @@ function registerTools(ctx) {
|
|
|
3407
3861
|
}
|
|
3408
3862
|
}
|
|
3409
3863
|
});
|
|
3864
|
+
T({
|
|
3865
|
+
name: "teamflow_merge",
|
|
3866
|
+
description: "Merge the completed run's feature branch back to main — the user-confirmed closing step (ADR-2026-08-27). Valid only after acceptance passed (run status=completed) while on a feature branch ahead of main. **Ask the user first** — the completion report carries the decision invitation (① host merge ② manual command ③ keep). Actions: \"merge\" — host performs git checkout main && git merge --no-ff <branch>; \"command\" — print the manual command for the user to run themselves; \"keep\" — defer, mark run as kept (branch stays).",
|
|
3867
|
+
parameters: {
|
|
3868
|
+
action: {
|
|
3869
|
+
type: "string",
|
|
3870
|
+
required: true,
|
|
3871
|
+
description: "\"merge\" (host performs the merge) / \"command\" (print the manual merge command) / \"keep\" (defer, mark kept)"
|
|
3872
|
+
},
|
|
3873
|
+
runId: {
|
|
3874
|
+
type: "string",
|
|
3875
|
+
description: "Run id (defaults to the latest completed run of this workspace)"
|
|
3876
|
+
}
|
|
3877
|
+
},
|
|
3878
|
+
output: {
|
|
3879
|
+
schema: {
|
|
3880
|
+
type: "object",
|
|
3881
|
+
additionalProperties: false,
|
|
3882
|
+
required: ["status"],
|
|
3883
|
+
properties: {
|
|
3884
|
+
status: { type: "string" },
|
|
3885
|
+
message: { type: "string" },
|
|
3886
|
+
command: { type: "string" }
|
|
3887
|
+
}
|
|
3888
|
+
},
|
|
3889
|
+
render: (args, value) => [{
|
|
3890
|
+
type: "text",
|
|
3891
|
+
text: value.message
|
|
3892
|
+
}]
|
|
3893
|
+
},
|
|
3894
|
+
async execute(args, exec) {
|
|
3895
|
+
const action = args && args.action;
|
|
3896
|
+
if (action !== "merge" && action !== "command" && action !== "keep") throw new Error("action 必须是 merge / command / keep");
|
|
3897
|
+
const sc = workspaceScopeOf(exec && exec.agent);
|
|
3898
|
+
if (!sc.path) throw new Error("当前会话无项目工作区");
|
|
3899
|
+
const key = sc.projectKey;
|
|
3900
|
+
const target = typeof args.runId === "string" && args.runId ? runs.get(args.runId) : [...runs.values()].filter((j) => j.workspace === key && j.status === "completed").sort((a, b) => (b.endedAt || 0) - (a.endedAt || 0))[0];
|
|
3901
|
+
if (!target) throw new Error("未找到已完成流水线(可传 runId 指定)");
|
|
3902
|
+
const branch = gitCmd(sc.path, [
|
|
3903
|
+
"rev-parse",
|
|
3904
|
+
"--abbrev-ref",
|
|
3905
|
+
"HEAD"
|
|
3906
|
+
]);
|
|
3907
|
+
if (!branch || branch === "main") return {
|
|
3908
|
+
status: "noop",
|
|
3909
|
+
message: "当前已在 main 分支,无需合回"
|
|
3910
|
+
};
|
|
3911
|
+
if (action === "command") return {
|
|
3912
|
+
status: "command",
|
|
3913
|
+
command: `git checkout main && git merge --no-ff ${branch}`,
|
|
3914
|
+
message: `请用户在项目目录执行以下命令完成合回(合回后可 git branch -d ${branch} 清理特性分支):\ngit checkout main && git merge --no-ff ${branch}`
|
|
3915
|
+
};
|
|
3916
|
+
if (action === "keep") {
|
|
3917
|
+
target.mergeStatus = "kept";
|
|
3918
|
+
persistJournal(target);
|
|
3919
|
+
return {
|
|
3920
|
+
status: "kept",
|
|
3921
|
+
message: `已标记暂不合回:特性分支 ${branch} 保留,后续可随时调用 teamflow_merge 合回`
|
|
3922
|
+
};
|
|
3923
|
+
}
|
|
3924
|
+
const co = gitCmd(sc.path, ["checkout", "main"]);
|
|
3925
|
+
const mg = co === null ? null : gitCmd(sc.path, [
|
|
3926
|
+
"merge",
|
|
3927
|
+
"--no-ff",
|
|
3928
|
+
branch
|
|
3929
|
+
]);
|
|
3930
|
+
if (co === null || mg === null) {
|
|
3931
|
+
target.mergeStatus = "failed";
|
|
3932
|
+
persistJournal(target);
|
|
3933
|
+
return {
|
|
3934
|
+
status: "failed",
|
|
3935
|
+
message: `合并失败(工作区可能不干净或有冲突)。请人工处理:先提交/处理当前工作区改动,再执行 git merge --no-ff ${branch}(冲突文件需手动解决)`
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
target.mergeStatus = "merged";
|
|
3939
|
+
persistJournal(target);
|
|
3940
|
+
return {
|
|
3941
|
+
status: "merged",
|
|
3942
|
+
message: `✅ 已合回 main(git merge --no-ff ${branch})。如需清理特性分支:git branch -d ${branch}`
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3945
|
+
});
|
|
3410
3946
|
T({
|
|
3411
3947
|
name: "teamflow_triage",
|
|
3412
3948
|
description: "Requirement triage (optional helper): teamflow_start already auto-triages by default — no need to call this manually. Use it only when you want to **pre-evaluate** which pipeline mode a requirement fits, or **force** a mode: a triage analyst Agent thinks one round and returns a suggested mode, nature, UI-need, complexity and rationale.",
|
|
@@ -3742,8 +4278,25 @@ function registerTools(ctx) {
|
|
|
3742
4278
|
}
|
|
3743
4279
|
/** 会话级暂停标记:pausedSessions.has(sessionId) → 该会话的 teamflow_start 被拦截。 */
|
|
3744
4280
|
const pausedSessions = /* @__PURE__ */ new Set();
|
|
3745
|
-
/** 会话级当前团队:activeTeams.get(sessionId) → 当前选中的团队 id。
|
|
4281
|
+
/** 会话级当前团队:activeTeams.get(sessionId) → 当前选中的团队 id。
|
|
4282
|
+
* 持久化到 $DSH_HOME/teamflow/active-teams.json——重启后恢复(实锤:重启/刷新后内存清空,
|
|
4283
|
+
* UI 显示无团队,但 agent 上下文记忆 teamId 显式传入仍启动流水线,UI 状态与启动通道不一致)。 */
|
|
3746
4284
|
const activeTeams = /* @__PURE__ */ new Map();
|
|
4285
|
+
const ACTIVE_TEAMS_FILE = () => join(teamflowRoot(), "active-teams.json");
|
|
4286
|
+
function loadActiveTeams() {
|
|
4287
|
+
try {
|
|
4288
|
+
const raw = readJsonAny(ACTIVE_TEAMS_FILE(), null);
|
|
4289
|
+
if (raw && typeof raw === "object") {
|
|
4290
|
+
for (const [k, v] of Object.entries(raw)) if (typeof k === "string" && typeof v === "string") activeTeams.set(k, v);
|
|
4291
|
+
}
|
|
4292
|
+
} catch (e) {}
|
|
4293
|
+
}
|
|
4294
|
+
function saveActiveTeams() {
|
|
4295
|
+
try {
|
|
4296
|
+
mkdirSync(teamflowRoot(), { recursive: true });
|
|
4297
|
+
writeJson(ACTIVE_TEAMS_FILE(), Object.fromEntries(activeTeams));
|
|
4298
|
+
} catch (e) {}
|
|
4299
|
+
}
|
|
3747
4300
|
/** 延迟注入队列:选团队时 agent 可能尚未加载,存入 pending,后续时机补发。 */
|
|
3748
4301
|
const pendingInjections = /* @__PURE__ */ new Map();
|
|
3749
4302
|
/** 尝试补发延迟注入:agent 可用时注入上下文并清除 pending。 */
|
|
@@ -3778,11 +4331,13 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
3778
4331
|
"subagents",
|
|
3779
4332
|
"tokenMeter",
|
|
3780
4333
|
"typert",
|
|
3781
|
-
"tools"
|
|
4334
|
+
"tools",
|
|
4335
|
+
"llm"
|
|
3782
4336
|
];
|
|
3783
4337
|
constructor(ctx) {
|
|
3784
4338
|
super(ctx, "teamflow");
|
|
3785
|
-
setRuntime(ctx.get("agents"), ctx.get("subagents"), ctx.get("tokenMeter"), ctx.get("workspaceRegistry"), ctx.get("agentDefaultModel"));
|
|
4339
|
+
setRuntime(ctx.get("agents"), ctx.get("subagents"), ctx.get("tokenMeter"), ctx.get("workspaceRegistry"), ctx.get("agentDefaultModel"), ctx.get("llm"));
|
|
4340
|
+
loadActiveTeams();
|
|
3786
4341
|
let interruptedCount = 0;
|
|
3787
4342
|
try {
|
|
3788
4343
|
for (const { journal, wasInterrupted } of loadJournals()) {
|
|
@@ -4083,6 +4638,7 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4083
4638
|
error: `团队 ${tid} 不存在`
|
|
4084
4639
|
};
|
|
4085
4640
|
activeTeams.set(sid, tid);
|
|
4641
|
+
saveActiveTeams();
|
|
4086
4642
|
const agent = runtime.agents && runtime.agents.get(sid);
|
|
4087
4643
|
const injectPayload = {
|
|
4088
4644
|
type: "user",
|
|
@@ -4139,6 +4695,7 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4139
4695
|
error: "缺少 sessionId"
|
|
4140
4696
|
};
|
|
4141
4697
|
activeTeams.delete(sid);
|
|
4698
|
+
saveActiveTeams();
|
|
4142
4699
|
return { ok: true };
|
|
4143
4700
|
}
|
|
4144
4701
|
/** 从断点续跑:跳过已完成阶段,从第一个未完成阶段重跑。 */
|