dsh-plugin-teamflow 0.1.2 → 0.1.3
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 +27 -7
- package/lib/host.mjs +602 -131
- package/package.json +25 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,16 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
> 本插件首次公开发布版本为 **v0.1.0**;发布前的内部迭代(v0.3~v0.13)记录于 `AGENTS.md` §5,对外统一归到 v0.1.0。
|
|
4
4
|
|
|
5
|
+
## [0.1.3] - 2026-08-29
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
- **分支策略闭环(ADR-2026-08-27)**:启动前用户决策(`needs-decision` 四情况:main+干净/main+脏/feature+干净/feature+脏,stash/commit/新建/沿用/自定义兜底);`branchPolicy`/`branchName`/`preAction`/`commitMessage` 决策参数;auto=建 `feat/<slug|branchName>`、keep=沿用;`preAction`(stash/commit)在 sanity 前执行
|
|
9
|
+
- **收尾合回决策(对称交互)**:完成汇报带「合回决策邀请」,新工具 `teamflow_merge`(host 代为合回 / 给命令自行合回 / 暂缓);`journal.mergeStatus` 持久化(pending/merged/kept/failed)
|
|
10
|
+
- **统一收口提交**:子代理只改不提交(Git discipline 硬约束),host 验收通过后单 commit(代码+任务夹产物);结构性消灭文档漏提交与未验收中间态
|
|
11
|
+
- **视觉验证能力条件化**:`llm.resolveModelInfo` 探测模型多模态能力 → QA/验收视觉条款动态生成(支持视觉=DOM 计算断言+截图看图+人工收窄;不支持=禁截图看图防幻觉/循环,只走 DOM 断言);QA 人工补测清单收窄为音频/真机/FPS/读屏
|
|
12
|
+
- **需求意图预检**:疑问/建议/反馈句式(「是不是应该」「要不要」等)→ `needs-confirmation` 不启动,主线程先向用户确认
|
|
13
|
+
- **activeTeams 持久化**:会话→团队映射落盘,重启/刷新后恢复(UI 状态与启动通道一致)
|
|
14
|
+
|
|
15
|
+
### 修复
|
|
16
|
+
- **护栏注入通道**:`subagents.start` 句柄无 inject → 改用 DSH 官方 `session.append('user/message')`;注入改安全窗口(step/end 后 flush,防插进 tool_calls→tool/result 序列导致 provider 400,实测 tf-mtcnejqj 烧 1.98M)
|
|
17
|
+
- **复读检测重复计数 bug**:轮询重复收集事件导致计数虚增(实际 4 次 × 3 轮 = 12 压线误杀,实测 tf-mtcomxpq 开发两次)→ 增量收集
|
|
18
|
+
- **`isUnretryable` 覆盖 400/invalid_request**(provider 客户端拒绝不再重试烧钱)
|
|
19
|
+
- **开发任务全部失败停止流水线**(无产物可测时不再继续 QA 误测;部分失败仍继续)
|
|
20
|
+
- **resume 断点按阶段定位**(PRD 重试成功后不再被失败尝试带回重跑)
|
|
21
|
+
- **分支 slug 派生**(branchName > triageSlug > 需求英文词 > r<N> > feature;分支检查移到 initBacklog 之后)
|
|
22
|
+
- **journal.options 透传 branchPolicy 等决策参数**(keep 不再被吞,实测 tf-mtd6mbeq)
|
|
23
|
+
- **DSH 0.1.2-alpha.1 事件词汇适配**(text-chunks/reasoning-chunks → assistant/chunk 双兼容);schema 校验兼容(needs-decision 不返回 runId 字段)
|
|
24
|
+
|
|
25
|
+
### 其他
|
|
26
|
+
- 执行路径基准(小需求样本)与假优化判定收敛:`docs/benchmarks/hold-pipeline-vs-native.md`(多花 44% 是质量预算非浪费,cacheRead 命中价≈1/10)
|
|
27
|
+
|
|
5
28
|
## [0.1.2] - 2026-08-26
|
|
6
29
|
|
|
30
|
+
### 修复
|
|
31
|
+
- host 就绪日志工具数改为动态计数(消除「工具 8 个」写死文案,与实际注册数一致)
|
|
32
|
+
|
|
7
33
|
### 变更
|
|
8
|
-
-
|
|
9
|
-
- backlog 卡片 / 详情抽屉跳转指定 run 的流水线视图(host `backlog` 附带 `runId` 映射)
|
|
10
|
-
- 详情抽屉长文本折叠(概览 / 需求原文默认预览几行,可展开收起)
|
|
11
|
-
- 断点重跑语义修正:阶段全 done 的 failed/cancelled 不再显示续跑按钮(host 硬拒绝兜底);按钮/pill/chip 补 runId 全名可溯
|
|
12
|
-
- 架构蓝图提取回退任务夹 `TECHNICAL.md`(模型把蓝图写进文档而非回复输出时 M2 拆卡不再退化为整体开发)
|
|
13
|
-
- `meta.json` 改为静态标识卡(废弃终态回写,status/endedAt 权威在 journal)
|
|
14
|
-
- 开源发布元数据:keywords / homepage / bugs;`react` 移入 peerDependencies;README.en.md 与 CHANGELOG.md 显式进包
|
|
34
|
+
- 开源发布收尾:README 中英双语修正(架构计数 17/11、防假交付真实语义、快速上手、环境要求);npm 元数据(keywords / homepage / bugs);`react` 移入 peerDependencies + `peerDependenciesMeta`(防私有包 ERESOLVE);CI 触发扩展 `main` + `release-*`;构建关闭 sourcemap(npm 包 -58%);新增 SECURITY.md / CONTRIBUTING.md;deploy 同步清单移除 `.map`
|
|
15
35
|
|
|
16
36
|
## [0.1.1] - 2026-08-26
|
|
17
37
|
|
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
|
* 优先级:
|
|
@@ -1030,6 +1069,79 @@ function getActiveStages(team, options) {
|
|
|
1030
1069
|
});
|
|
1031
1070
|
}
|
|
1032
1071
|
//#endregion
|
|
1072
|
+
//#region host/core/sanity.ts
|
|
1073
|
+
/**
|
|
1074
|
+
* dsh-plugin-teamflow core — M0 状态核对(sanity check)。
|
|
1075
|
+
*
|
|
1076
|
+
* 目的(三情况协议):
|
|
1077
|
+
* - 情况一(全新会话):0 认知 → 核对确认"从零开始"。
|
|
1078
|
+
* - 情况二(续会话):认知大概率过期(多人协作/场外提交/非流水线改动)→ 核对发现预期外变化。
|
|
1079
|
+
* - 情况三(新会话处理新需求):即使共用 state/记忆,仓库也可能被外部改动 → 核对现状。
|
|
1080
|
+
*
|
|
1081
|
+
* 核心原则:认知资产(索引/记忆/摘要)可复用"减量",但永不能替代"对代码库当前真实状态的核对"。
|
|
1082
|
+
* 本模块在 host 侧直接跑 git(零模型 token、轻量、失败优雅降级为"无法核对"),
|
|
1083
|
+
* 产出 externalDiffs 摘要注入到后续所有阶段 prompt。
|
|
1084
|
+
*/
|
|
1085
|
+
/** 单条 git 检查结果(失败返回 null,调用方降级)。 */
|
|
1086
|
+
function gitCmd(cwd, args, timeoutMs = 8e3) {
|
|
1087
|
+
try {
|
|
1088
|
+
return execFileSync("git", args, {
|
|
1089
|
+
cwd,
|
|
1090
|
+
encoding: "utf8",
|
|
1091
|
+
timeout: timeoutMs,
|
|
1092
|
+
windowsHide: true
|
|
1093
|
+
}).trim();
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* 跑一次状态核对。
|
|
1100
|
+
* @param path - 工作区绝对路径(workspaceScopeOf(agent).path)。
|
|
1101
|
+
*/
|
|
1102
|
+
function runSanityCheck(path) {
|
|
1103
|
+
const defaultOut = {
|
|
1104
|
+
ok: false,
|
|
1105
|
+
inRepo: false,
|
|
1106
|
+
branch: null,
|
|
1107
|
+
dirty: "",
|
|
1108
|
+
hasDirty: false,
|
|
1109
|
+
recentCommits: "",
|
|
1110
|
+
summary: ""
|
|
1111
|
+
};
|
|
1112
|
+
if (!path) return {
|
|
1113
|
+
...defaultOut,
|
|
1114
|
+
summary: "⚠ 无法确定工作区路径,未做状态核对。"
|
|
1115
|
+
};
|
|
1116
|
+
const branch = gitCmd(path, ["branch", "--show-current"]);
|
|
1117
|
+
const status = gitCmd(path, ["status", "--short"]);
|
|
1118
|
+
if (branch === null && status === null) return {
|
|
1119
|
+
...defaultOut,
|
|
1120
|
+
summary: "⚠ 状态核对不可用(非 git 仓库或 git 命令被拒),无法确认代码库真实状态。"
|
|
1121
|
+
};
|
|
1122
|
+
const recent = gitCmd(path, [
|
|
1123
|
+
"log",
|
|
1124
|
+
"--oneline",
|
|
1125
|
+
"-5"
|
|
1126
|
+
]);
|
|
1127
|
+
const dirty = status || "";
|
|
1128
|
+
const dirtyLines = dirty.split("\n").filter(Boolean);
|
|
1129
|
+
const summaryParts = [];
|
|
1130
|
+
summaryParts.push(`分支:${branch && branch !== "HEAD" ? branch : "(detached HEAD / 非分支)"}`);
|
|
1131
|
+
if (dirtyLines.length) summaryParts.push(`未提交改动 ${dirtyLines.length} 处:${dirtyLines.slice(0, 8).join(";")}${dirtyLines.length > 8 ? "…" : ""}`);
|
|
1132
|
+
else summaryParts.push("工作区干净(无未提交改动)");
|
|
1133
|
+
if (recent) summaryParts.push(`近期提交(可能含场外改动):${recent.split("\n").slice(0, 3).join(";")}`);
|
|
1134
|
+
return {
|
|
1135
|
+
ok: true,
|
|
1136
|
+
inRepo: true,
|
|
1137
|
+
branch: branch && branch !== "HEAD" ? branch : null,
|
|
1138
|
+
dirty,
|
|
1139
|
+
hasDirty: dirtyLines.length > 0,
|
|
1140
|
+
recentCommits: recent || "",
|
|
1141
|
+
summary: `【状态核对】${summaryParts.join("。")}`
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
//#endregion
|
|
1033
1145
|
//#region host/core/metering.ts
|
|
1034
1146
|
/**
|
|
1035
1147
|
* 累计子代理会话中所有 LLM 调用的真实 usage(官方三桶 + 调用数)。
|
|
@@ -1106,6 +1218,38 @@ function eventsOf(run) {
|
|
|
1106
1218
|
function normalizeFragment(s) {
|
|
1107
1219
|
return String(s || "").toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "");
|
|
1108
1220
|
}
|
|
1221
|
+
/** 观测→执行闭环:向运行中的子代理注入轻提醒(不打断,下一轮 step 可见)。
|
|
1222
|
+
* 通道:subagents.start 句柄无 inject——用 DSH 官方 session.append('user/message')(in-process driver 同款用法)。
|
|
1223
|
+
* ⚠️ 安全窗口(实锤 tf-mtcnejqj):绝不能插在 assistant(tool_calls) → tool/result 之间——
|
|
1224
|
+
* provider 校验「tool 消息必须响应前序 tool_calls」,插入 user 消息会 400 invalid_request_error。
|
|
1225
|
+
* 因此只入队(pendingInjects),在观察到 step/end(该 step 的 tool/result 已写入)后统一 flush。 */
|
|
1226
|
+
function injectReminder(run, text) {
|
|
1227
|
+
const queue = run;
|
|
1228
|
+
try {
|
|
1229
|
+
if (!Array.isArray(queue.__teamflowPending)) queue.__teamflowPending = [];
|
|
1230
|
+
queue.__teamflowPending.push(text);
|
|
1231
|
+
} catch (e) {}
|
|
1232
|
+
}
|
|
1233
|
+
function flushReminders(run) {
|
|
1234
|
+
try {
|
|
1235
|
+
const queue = run.__teamflowPending;
|
|
1236
|
+
if (!queue || queue.length === 0) return;
|
|
1237
|
+
const local = run.localAgent;
|
|
1238
|
+
if (!local || typeof local.session?.append !== "function") return;
|
|
1239
|
+
for (const text of queue.splice(0)) local.session.append("user/message", {
|
|
1240
|
+
id: crypto.randomUUID(),
|
|
1241
|
+
role: "user",
|
|
1242
|
+
content: [{
|
|
1243
|
+
type: "text",
|
|
1244
|
+
text
|
|
1245
|
+
}],
|
|
1246
|
+
source: {
|
|
1247
|
+
kind: "plugin",
|
|
1248
|
+
plugin: "teamflow"
|
|
1249
|
+
}
|
|
1250
|
+
}, { surfaceOp: "append" });
|
|
1251
|
+
} catch (e) {}
|
|
1252
|
+
}
|
|
1109
1253
|
/**
|
|
1110
1254
|
* 启动单调用护栏轮询,返回取消函数(runAgent finally 必须调用)。
|
|
1111
1255
|
* 触发时:stage.guardReason/guardOutcome 记录原因 + journal 落日志 + dispose 中止本次尝试。
|
|
@@ -1123,7 +1267,7 @@ function startStageGuard(opts) {
|
|
|
1123
1267
|
const warnedReads = /* @__PURE__ */ new Set();
|
|
1124
1268
|
const scriptCounts = /* @__PURE__ */ new Map();
|
|
1125
1269
|
const warnedScripts = /* @__PURE__ */ new Set();
|
|
1126
|
-
function warnOnce(key, set, message) {
|
|
1270
|
+
function warnOnce(key, set, message, hint) {
|
|
1127
1271
|
if (set.has(key)) return;
|
|
1128
1272
|
set.add(key);
|
|
1129
1273
|
try {
|
|
@@ -1133,6 +1277,7 @@ function startStageGuard(opts) {
|
|
|
1133
1277
|
message: `${label} [token 观测] ${message}`
|
|
1134
1278
|
});
|
|
1135
1279
|
} catch (e) {}
|
|
1280
|
+
if (hint) injectReminder(run, `[TOKEN GUARD · reminder] ${hint}`);
|
|
1136
1281
|
}
|
|
1137
1282
|
function fire(reason, outcome) {
|
|
1138
1283
|
if (fired) return;
|
|
@@ -1155,24 +1300,35 @@ function startStageGuard(opts) {
|
|
|
1155
1300
|
if (fired || journal.cancelled) return;
|
|
1156
1301
|
try {
|
|
1157
1302
|
const events = eventsOf(run);
|
|
1158
|
-
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1303
|
+
if (events.length > processed) {
|
|
1304
|
+
const newEvents = events.slice(processed);
|
|
1305
|
+
for (const ev of newEvents) {
|
|
1306
|
+
const e = ev;
|
|
1307
|
+
if (!e) continue;
|
|
1308
|
+
let streamText = null;
|
|
1309
|
+
if (e.type === "text-chunks" || e.type === "reasoning-chunks") {
|
|
1310
|
+
const arr = e.data && e.data.texts || e.texts;
|
|
1311
|
+
if (Array.isArray(arr)) streamText = arr.map(String).join("");
|
|
1312
|
+
} else if (e.type === "assistant/chunk") {
|
|
1313
|
+
const chunk = e.data && e.data.chunk || null;
|
|
1314
|
+
if (chunk && (chunk.type === "text-delta" || chunk.type === "reasoning-delta") && typeof chunk.text === "string") streamText = chunk.text;
|
|
1315
|
+
}
|
|
1316
|
+
if (streamText !== null && streamText.length > 0) {
|
|
1317
|
+
const s = normalizeFragment(streamText);
|
|
1318
|
+
if (s.length >= 12) window.push(s);
|
|
1319
|
+
lastGrowthAt = Date.now();
|
|
1320
|
+
}
|
|
1167
1321
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1322
|
+
if (window.length > 400) window.splice(0, window.length - 400);
|
|
1323
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1324
|
+
for (const w of window) counts.set(w, (counts.get(w) || 0) + 1);
|
|
1325
|
+
for (const [, n] of counts) if (n >= 12) {
|
|
1326
|
+
fire(`推理复读(同一片段在近 ${window.length} 条流式片段中出现 ${n} 次)`, "degenerated");
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
observeToolCalls(newEvents);
|
|
1330
|
+
if (newEvents.some((ev) => ev?.type === "step/end")) flushReminders(run);
|
|
1331
|
+
processed = events.length;
|
|
1176
1332
|
}
|
|
1177
1333
|
for (const ev of events.slice(-200)) {
|
|
1178
1334
|
const e = ev;
|
|
@@ -1182,10 +1338,6 @@ function startStageGuard(opts) {
|
|
|
1182
1338
|
break;
|
|
1183
1339
|
}
|
|
1184
1340
|
}
|
|
1185
|
-
if (events.length > processed) {
|
|
1186
|
-
observeToolCalls(events.slice(processed));
|
|
1187
|
-
processed = events.length;
|
|
1188
|
-
}
|
|
1189
1341
|
if (events.length !== lastEventCount) {
|
|
1190
1342
|
lastEventCount = events.length;
|
|
1191
1343
|
lastGrowthAt = Date.now();
|
|
@@ -1213,14 +1365,14 @@ function startStageGuard(opts) {
|
|
|
1213
1365
|
const key = m[1].replace(/\\\\/g, "\\").toLowerCase();
|
|
1214
1366
|
const n = (readCounts.get(key) || 0) + 1;
|
|
1215
1367
|
readCounts.set(key, n);
|
|
1216
|
-
if (n === 3) warnOnce(key, warnedReads, `同一文件重复 read ${n} 次:${m[1].split(/[\\\\/]/).pop()}(TOKEN_HYGIENE 上限 1 次,多余读取在为后续每一步付 cache
|
|
1368
|
+
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
1369
|
} else if (/bash|pwsh|shell|powershell/i.test(name || "")) {
|
|
1218
1370
|
const sm = String(args).match(/(verify-[a-z0-9-]+\.cjs|assembly-check\.cjs|qa-e2e-jsdom\.cjs)/);
|
|
1219
1371
|
if (!sm) continue;
|
|
1220
1372
|
const key = sm[1];
|
|
1221
1373
|
const n = (scriptCounts.get(key) || 0) + 1;
|
|
1222
1374
|
scriptCounts.set(key, n);
|
|
1223
|
-
if (n === 3) warnOnce(key, warnedScripts, `验证脚本重复执行 ${n} 次:${key}(批量修复纪律:一次修完所有失败再跑,≤3
|
|
1375
|
+
if (n === 3) warnOnce(key, warnedScripts, `验证脚本重复执行 ${n} 次:${key}(批量修复纪律:一次修完所有失败再跑,≤3 轮)`, `验证脚本 ${key} 已重复执行 3 次。遵守批量修复纪律:一次读完所有失败用例 → 一次全修 → 再跑一次;超出 3 轮请输出诊断摘要并停止。`);
|
|
1224
1376
|
}
|
|
1225
1377
|
}
|
|
1226
1378
|
}
|
|
@@ -1889,10 +2041,24 @@ ${clip(tech, 12e3)}` : ""}
|
|
|
1889
2041
|
3. If spec contradicts reality, explain with evidence in the summary instead of claiming completion or expanding scope on your own.
|
|
1890
2042
|
4. Actually write/modify code (grep + segmented reads to locate; no repeated whole-file reads), then run relevant build/verification to ensure green.
|
|
1891
2043
|
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.
|
|
2044
|
+
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
2045
|
6. [Log discipline] Redirect command output to logs/teamflow/${runId || "<runId>"}/.
|
|
1893
2046
|
7. Output an implementation summary (≤40 lines): changed files, key implementation points, how verified, leftovers. No big code pastes.
|
|
1894
2047
|
8. [State] End with a state block (phase="dev"), touched = array of changed files, summary = implementation conclusion.${STATE_BLOCK_INSTRUCTION}`;
|
|
1895
|
-
|
|
2048
|
+
/** 视觉验证能力条款(ADR-2026-08-27,解锁 browser-use 视觉验证):
|
|
2049
|
+
* 按当前模型多模态能力动态生成——vision=true 允许截图看图(人眼类项),精确值仍走 DOM 计算断言;
|
|
2050
|
+
* vision=false 禁截图看图(防幻觉/循环,历史禁令动机=模型不识图),只走 DOM 计算断言(evaluate 返回文本)。
|
|
2051
|
+
* 两者都要求:浏览器失败降级不重试。 */
|
|
2052
|
+
const VISUAL_POLICY = (vision) => vision ? `[Visual verification · enabled (model supports image input)]
|
|
2053
|
+
- 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.
|
|
2054
|
+
- **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.
|
|
2055
|
+
- 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.
|
|
2056
|
+
- 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)]
|
|
2057
|
+
- 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.
|
|
2058
|
+
- 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.
|
|
2059
|
+
- 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.
|
|
2060
|
+
- On any browser failure: degrade to jsdom + 「人工补测清单」, do not retry.`;
|
|
2061
|
+
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
2062
|
${productCtx(root)}${stateSliceFor(state, "qa")}${TOKEN_HYGIENE(runId)}[PRD (this change & relevant ACs)]
|
|
1897
2063
|
${headTailClip(prd, 5e3, 7e3)}
|
|
1898
2064
|
[DEV RESULT SUMMARY]
|
|
@@ -1902,8 +2068,9 @@ ${clip(devSummary, 15e3)}
|
|
|
1902
2068
|
- 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
2069
|
- 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
2070
|
- 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
|
-
|
|
2071
|
+
1. [Environment limits · dynamic by model capability]${VISUAL_POLICY(!!vision)}
|
|
2072
|
+
- Always-available sandbox-legal paths: build/assembly checks, unit tests, DOM-level E2E (jsdom or equivalent), static audit, adversarial spot-checks.
|
|
2073
|
+
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
2074
|
3. Read AGENTS.md §4 engineering conventions (verify commands) and the code changes first, then actually run those sandbox-legal verifications.
|
|
1908
2075
|
4. [Log discipline] Redirect command output to logs/teamflow/${runId || "<runId>"}/ (e.g. qa-out.log); no scatter at project root.
|
|
1909
2076
|
5. Output the test report (body ≤150 lines, verdict first): scope & environment, cases & results (pass/fail/blocked), conclusion (whether acceptance-ready).
|
|
@@ -1929,7 +2096,7 @@ ${tech && String(tech).trim() ? clip(tech, 12e3) : ""}
|
|
|
1929
2096
|
3. After fixing, run relevant verification to ensure green (regression floor: existing verify suites pass untouched); redirect output to logs/teamflow/${runId || "<runId>"}/.
|
|
1930
2097
|
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
2098
|
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.
|
|
2099
|
+
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
2100
|
${productCtx(root)}${stateSliceFor(state, "acceptance")}${TOKEN_HYGIENE(runId)}
|
|
1934
2101
|
${ONCE_DISCIPLINE}[PRD (revision log + this run's new ACs)]
|
|
1935
2102
|
${headTailClip(prd, 4e3, 5e3)}
|
|
@@ -1937,6 +2104,7 @@ ${headTailClip(prd, 4e3, 5e3)}
|
|
|
1937
2104
|
${clip(qa, 1e4)}
|
|
1938
2105
|
[DEV RESULT SUMMARY]
|
|
1939
2106
|
${clip(devSummary, 8e3)}
|
|
2107
|
+
${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
2108
|
[REQUIREMENTS]
|
|
1941
2109
|
0. [Architecture consistency check · mandatory (M3 quality gate)] Beyond functional ACs, check structural quality:
|
|
1942
2110
|
- 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 +2489,29 @@ function deliverCompletion(journal, parent) {
|
|
|
2321
2489
|
interrupted: "⚠ 中断(可用 teamflow_resume 从断点重跑)"
|
|
2322
2490
|
}[journal.status] || journal.status;
|
|
2323
2491
|
const stagesLine = stages.length === 0 ? "尚未进入任何阶段" : `${stages.length} 个阶段 · ${done} 完成${failed > 0 ? ` · ${failed} 失败` : ""}${cancelledStages > 0 ? ` · ${cancelledStages} 取消` : ""}`;
|
|
2492
|
+
let mergeHint = "";
|
|
2493
|
+
if (journal.status === "completed" && !journal.error && journal.workspacePath) try {
|
|
2494
|
+
const branch = gitCmd(journal.workspacePath, [
|
|
2495
|
+
"rev-parse",
|
|
2496
|
+
"--abbrev-ref",
|
|
2497
|
+
"HEAD"
|
|
2498
|
+
]);
|
|
2499
|
+
if (branch && branch !== "main") {
|
|
2500
|
+
const ahead = gitCmd(journal.workspacePath, [
|
|
2501
|
+
"rev-list",
|
|
2502
|
+
"--count",
|
|
2503
|
+
"main..HEAD"
|
|
2504
|
+
]);
|
|
2505
|
+
if (ahead && Number(ahead) > 0) {
|
|
2506
|
+
journal.mergeStatus = journal.mergeStatus || "pending";
|
|
2507
|
+
mergeHint = `🧩 合回决策:验收已通过,当前在特性分支 ${branch}(领先 main ${ahead} 个提交)。请**询问用户**是否合回,并按用户选择调用 teamflow_merge:\n ① host 代为合回 → action="merge"\n ② 给出命令用户自行合回 → action="command"\n ③ 暂不合回 → action="keep"\n(选项之外用户自定义输入亦可,如实转述)`;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
if ((journal.options || {}).preAction === "stash") {
|
|
2511
|
+
const stashHint = "🧩 启动前 stash 的改动仍在暂存区:流水线完成后请执行 git stash pop 恢复你的改动";
|
|
2512
|
+
mergeHint = mergeHint ? `${mergeHint}\n${stashHint}` : stashHint;
|
|
2513
|
+
}
|
|
2514
|
+
} catch (e) {}
|
|
2324
2515
|
const text = [
|
|
2325
2516
|
`【团队研发流水线汇报】runId=${journal.id}`,
|
|
2326
2517
|
`状态:${statusLine}${journal.error ? `(${clip(journal.error, 300)})` : ""}`,
|
|
@@ -2334,6 +2525,7 @@ function deliverCompletion(journal, parent) {
|
|
|
2334
2525
|
return typeof m === "string" && m !== "full" && m !== "medium" ? `模式:${MODE_REGISTRY[m] ? MODE_REGISTRY[m].label : m}` : "";
|
|
2335
2526
|
})(),
|
|
2336
2527
|
`backlog:需求 ${journal.reqId || "—"}($DSH_HOME/teamflow/ 持久化)`,
|
|
2528
|
+
mergeHint,
|
|
2337
2529
|
"用户可打开「🏭 团队工作台」tab 查看阶段泳道、拖拽看板与 token 明细。",
|
|
2338
2530
|
"如需继续处理:可认领缺陷(teamflow_claim)、人工流转(teamflow_update)、断点重跑(teamflow_resume)。",
|
|
2339
2531
|
"若用户在场请简明转述以上要点;若无人值守仅记录即可,不必长篇回复。"
|
|
@@ -2357,79 +2549,6 @@ function deliverCompletion(journal, parent) {
|
|
|
2357
2549
|
}
|
|
2358
2550
|
}
|
|
2359
2551
|
//#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
2552
|
//#region host/core/pipeline.ts
|
|
2434
2553
|
/**
|
|
2435
2554
|
* dsh-plugin-teamflow core — 流水线编排中枢(阶段执行 / 入口 / 取消 / 断点续跑)。
|
|
@@ -2454,12 +2573,17 @@ function buildResumeProducts(journal) {
|
|
|
2454
2573
|
return products;
|
|
2455
2574
|
}
|
|
2456
2575
|
/**
|
|
2457
|
-
*
|
|
2576
|
+
* 断点续跑起点:第一个「没有任意 done 尝试」的阶段。
|
|
2577
|
+
* ⚠️ 按阶段而非尝试判断(实锤 tf-mtcomxpq):PRD 第 1 次尝试 failed(护栏退化)但第 2 次重试 done——
|
|
2578
|
+
* 旧实现取第一个非 done stage → 断点错误回到 PRD,PRD/技术方案被无谓重跑。
|
|
2458
2579
|
* 全部完成仍被中断(理论极端)→ 从产品验收继续。
|
|
2459
2580
|
*/
|
|
2460
2581
|
function interruptedPhaseOf(journal) {
|
|
2461
|
-
const
|
|
2462
|
-
|
|
2582
|
+
for (const phase of PHASE_ORDER) {
|
|
2583
|
+
const phaseStages = (journal.stages || []).filter((s) => s.phase === phase);
|
|
2584
|
+
if (phaseStages.length === 0) continue;
|
|
2585
|
+
if (!phaseStages.some((s) => s.status === "done")) return phase;
|
|
2586
|
+
}
|
|
2463
2587
|
return "产品验收";
|
|
2464
2588
|
}
|
|
2465
2589
|
/**
|
|
@@ -2557,6 +2681,48 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2557
2681
|
level: "info",
|
|
2558
2682
|
message: `backlog 已建立需求 ${init.reqId}(产品 ${root || "unknown"},并发 ${maxConcurrency})`
|
|
2559
2683
|
});
|
|
2684
|
+
if (options.branchPolicy !== "keep" && journal.workspacePath) try {
|
|
2685
|
+
const s = runSanityCheck(journal.workspacePath);
|
|
2686
|
+
if (s.ok && s.inRepo && !s.hasDirty) {
|
|
2687
|
+
const branch = `feat/${deriveBranchSlug(requirement, journal.reqId, triageSlug, options.branchName)}`;
|
|
2688
|
+
const exists = gitCmd(journal.workspacePath, [
|
|
2689
|
+
"branch",
|
|
2690
|
+
"--list",
|
|
2691
|
+
branch
|
|
2692
|
+
]);
|
|
2693
|
+
if (exists && exists.trim()) {
|
|
2694
|
+
journal.logs.push({
|
|
2695
|
+
t: Date.now(),
|
|
2696
|
+
level: "warn",
|
|
2697
|
+
message: `分支策略:分支 ${branch} 已存在,沿用现有分支`
|
|
2698
|
+
});
|
|
2699
|
+
gitCmd(journal.workspacePath, ["checkout", branch]);
|
|
2700
|
+
journal.branch = branch;
|
|
2701
|
+
} else {
|
|
2702
|
+
const ok = gitCmd(journal.workspacePath, [
|
|
2703
|
+
"checkout",
|
|
2704
|
+
"-b",
|
|
2705
|
+
branch
|
|
2706
|
+
]);
|
|
2707
|
+
journal.logs.push({
|
|
2708
|
+
t: Date.now(),
|
|
2709
|
+
level: "info",
|
|
2710
|
+
message: ok !== null ? `分支策略:已创建特性分支 ${branch}(从 ${s.branch} 派生)` : `分支策略:自动建分支 ${branch} 失败(git 异常),沿用当前分支`
|
|
2711
|
+
});
|
|
2712
|
+
if (ok !== null) journal.branch = branch;
|
|
2713
|
+
}
|
|
2714
|
+
} else if (s.ok && s.inRepo && s.branch) journal.logs.push({
|
|
2715
|
+
t: Date.now(),
|
|
2716
|
+
level: "warn",
|
|
2717
|
+
message: `分支策略:工作区仍有未提交改动(${s.dirty.split(/\r?\n/).filter((l) => l.trim()).length} 处),跳过建分支,沿用 ${s.branch}——请知悉改动将混入本次开发`
|
|
2718
|
+
});
|
|
2719
|
+
} catch (e) {
|
|
2720
|
+
journal.logs.push({
|
|
2721
|
+
t: Date.now(),
|
|
2722
|
+
level: "warn",
|
|
2723
|
+
message: `分支策略检查失败(降级沿用当前分支):${String(e && e.message || e)}`
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2560
2726
|
}
|
|
2561
2727
|
persistJournal(journal);
|
|
2562
2728
|
if (!journal.runDocs && journal.reqId) {
|
|
@@ -2586,6 +2752,49 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2586
2752
|
});
|
|
2587
2753
|
}
|
|
2588
2754
|
}
|
|
2755
|
+
if (!resume && journal.workspacePath && options.preAction === "stash") try {
|
|
2756
|
+
if (gitCmd(journal.workspacePath, [
|
|
2757
|
+
"stash",
|
|
2758
|
+
"push",
|
|
2759
|
+
"-m",
|
|
2760
|
+
`teamflow:${journal.id} pre-pipeline`
|
|
2761
|
+
]) !== null) journal.logs.push({
|
|
2762
|
+
t: Date.now(),
|
|
2763
|
+
level: "info",
|
|
2764
|
+
message: `分支策略:工作区改动已 stash(teamflow:${journal.id}),流水线完成后 git stash pop 恢复你的改动`
|
|
2765
|
+
});
|
|
2766
|
+
else journal.logs.push({
|
|
2767
|
+
t: Date.now(),
|
|
2768
|
+
level: "warn",
|
|
2769
|
+
message: "分支策略:stash 执行失败(可能无改动),继续"
|
|
2770
|
+
});
|
|
2771
|
+
} catch (e) {
|
|
2772
|
+
journal.logs.push({
|
|
2773
|
+
t: Date.now(),
|
|
2774
|
+
level: "warn",
|
|
2775
|
+
message: `分支策略:stash 异常:${String(e && e.message || e)}`
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
else if (!resume && journal.workspacePath && options.preAction === "commit") try {
|
|
2779
|
+
const msg = typeof options.commitMessage === "string" && options.commitMessage.trim() ? options.commitMessage.trim() : `chore(teamflow): 流水线启动前提交现有改动(${journal.id})`;
|
|
2780
|
+
const add = gitCmd(journal.workspacePath, ["add", "-A"]);
|
|
2781
|
+
const cm = gitCmd(journal.workspacePath, [
|
|
2782
|
+
"commit",
|
|
2783
|
+
"-m",
|
|
2784
|
+
msg
|
|
2785
|
+
]);
|
|
2786
|
+
journal.logs.push({
|
|
2787
|
+
t: Date.now(),
|
|
2788
|
+
level: "info",
|
|
2789
|
+
message: add !== null && cm !== null ? `分支策略:已提交现有改动(preAction=commit:${msg.slice(0, 60)})` : "分支策略:commit 执行失败(可能无改动),继续"
|
|
2790
|
+
});
|
|
2791
|
+
} catch (e) {
|
|
2792
|
+
journal.logs.push({
|
|
2793
|
+
t: Date.now(),
|
|
2794
|
+
level: "warn",
|
|
2795
|
+
message: `分支策略:commit 异常:${String(e && e.message || e)}`
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2589
2798
|
const state = loadState(journal.workspace || "default");
|
|
2590
2799
|
state.__runCtx = { ...state.__runCtx || {} };
|
|
2591
2800
|
if (journal.runDocs) state.__runCtx.runDocs = journal.runDocs;
|
|
@@ -2828,6 +3037,14 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2828
3037
|
level: "warn",
|
|
2829
3038
|
message: `开发完成,失败任务数:${failedCount}(需人工介入)`
|
|
2830
3039
|
});
|
|
3040
|
+
if (failedCount >= devResults.length) {
|
|
3041
|
+
journal.logs.push({
|
|
3042
|
+
t: Date.now(),
|
|
3043
|
+
level: "error",
|
|
3044
|
+
message: `开发任务全部失败(${failedCount}/${devResults.length}):停止流水线,需人工介入——可用 teamflow_resume 从开发阶段重跑`
|
|
3045
|
+
});
|
|
3046
|
+
throw new Error(`开发任务全部失败(${failedCount}/${devResults.length}):无产物可测,停止流水线`);
|
|
3047
|
+
}
|
|
2831
3048
|
} else {
|
|
2832
3049
|
advanceTask(journal, "testable", null, "开发完成待测试(开发中 → 待测试)", { by: "dev" });
|
|
2833
3050
|
journal.logs.push({
|
|
@@ -2872,7 +3089,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2872
3089
|
do {
|
|
2873
3090
|
round += 1;
|
|
2874
3091
|
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);
|
|
3092
|
+
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
3093
|
if (!qaR.text) {
|
|
2877
3094
|
advanceTask(journal, "needs-human", null, isReverify ? `QA 复验失败(第 ${round - 1} 轮修复后)` : "QA 失败", { by: "qa" });
|
|
2878
3095
|
throw stageFailError(isReverify ? "QA 测试(复验)" : "QA 测试", qaR);
|
|
@@ -2952,7 +3169,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2952
3169
|
const curTask = storeFor(scopeKey).find("task", journal.taskId);
|
|
2953
3170
|
if (curTask && curTask.status !== "pending-acceptance" && curTask.status !== "needs-human" && curTask.status !== "rework") advanceTask(journal, "pending-acceptance", null, "进入验收(待验收)", { by: "pm" });
|
|
2954
3171
|
}
|
|
2955
|
-
const accR = await withRetry(journal, parent, "产品经理 · 最终验收", "产品验收", acceptancePrompt(prd, qa, JSON.stringify(timeline.dev), root, journal.id, state), signal);
|
|
3172
|
+
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
3173
|
if (!accR.text) {
|
|
2957
3174
|
advanceTask(journal, "needs-human", null, "验收失败", { by: "pm" });
|
|
2958
3175
|
throw stageFailError("产品验收", accR);
|
|
@@ -3021,6 +3238,35 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3021
3238
|
journal.endedAt = Date.now();
|
|
3022
3239
|
inFlight.delete(journal.id);
|
|
3023
3240
|
activeProducts.delete(scopeKey);
|
|
3241
|
+
if (journal.workspacePath && journal.status === "completed" && !journal.humanIntervention) try {
|
|
3242
|
+
const reqHead = String(journal.requirement || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
3243
|
+
const add = gitCmd(journal.workspacePath, ["add", "-A"]);
|
|
3244
|
+
if ((add === null ? null : gitCmd(journal.workspacePath, [
|
|
3245
|
+
"commit",
|
|
3246
|
+
"-m",
|
|
3247
|
+
`feat: ${reqHead}(runId=${journal.id}${journal.runDocs ? `,任务夹 ${journal.runDocs}` : ""})`
|
|
3248
|
+
])) !== null) journal.logs.push({
|
|
3249
|
+
t: Date.now(),
|
|
3250
|
+
level: "info",
|
|
3251
|
+
message: "统一收口提交完成(代码 + 任务夹产物,验收通过后一个 commit)"
|
|
3252
|
+
});
|
|
3253
|
+
else if (add !== null) journal.logs.push({
|
|
3254
|
+
t: Date.now(),
|
|
3255
|
+
level: "info",
|
|
3256
|
+
message: "统一收口提交:无待提交改动(跳过)"
|
|
3257
|
+
});
|
|
3258
|
+
} catch (e) {
|
|
3259
|
+
journal.logs.push({
|
|
3260
|
+
t: Date.now(),
|
|
3261
|
+
level: "warn",
|
|
3262
|
+
message: `统一收口提交失败(忽略):${String(e && e.message || e)}`
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
else if (journal.workspacePath && journal.runDocs && (journal.status === "failed" || journal.status === "cancelled" || journal.status === "interrupted")) journal.logs.push({
|
|
3266
|
+
t: Date.now(),
|
|
3267
|
+
level: "warn",
|
|
3268
|
+
message: `终态非验收通过:工作区改动与任务夹产物 ${journal.runDocs}/ 保留未提交,供人工处理(resume 修复通过后自动收口提交)`
|
|
3269
|
+
});
|
|
3024
3270
|
journal.result = {
|
|
3025
3271
|
requirement,
|
|
3026
3272
|
options: sanitizeSnapOptions(options),
|
|
@@ -3098,7 +3344,11 @@ function startPipeline(agent, requirement, options, signal) {
|
|
|
3098
3344
|
teamId: options.teamId || void 0,
|
|
3099
3345
|
tasks: normalizeTasks(options.tasks),
|
|
3100
3346
|
productRoot: normalizeRoot(options.productRoot),
|
|
3101
|
-
maxConcurrency: Number.isFinite(options.maxConcurrency) && options.maxConcurrency > 0 ? Math.min(options.maxConcurrency, 8) : null
|
|
3347
|
+
maxConcurrency: Number.isFinite(options.maxConcurrency) && options.maxConcurrency > 0 ? Math.min(options.maxConcurrency, 8) : null,
|
|
3348
|
+
branchPolicy: options.branchPolicy || void 0,
|
|
3349
|
+
branchName: options.branchName || void 0,
|
|
3350
|
+
preAction: options.preAction || void 0,
|
|
3351
|
+
commitMessage: options.commitMessage || void 0
|
|
3102
3352
|
},
|
|
3103
3353
|
startedAt: null,
|
|
3104
3354
|
endedAt: null,
|
|
@@ -3303,7 +3553,7 @@ function registerTools(ctx) {
|
|
|
3303
3553
|
};
|
|
3304
3554
|
T({
|
|
3305
3555
|
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).",
|
|
3556
|
+
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 with the chosen branchPolicy / branchName / preAction / commitMessage parameters. Pass branchPolicy=\"keep\" when the user chooses to stay on the current branch.",
|
|
3307
3557
|
parameters: {
|
|
3308
3558
|
requirement: {
|
|
3309
3559
|
type: "string",
|
|
@@ -3338,6 +3588,22 @@ function registerTools(ctx) {
|
|
|
3338
3588
|
type: "integer",
|
|
3339
3589
|
description: "Dev task concurrency (default 3, max 8)"
|
|
3340
3590
|
},
|
|
3591
|
+
branchPolicy: {
|
|
3592
|
+
type: "string",
|
|
3593
|
+
description: "Branch policy: \"auto\" (default) — create a feature branch feat/<branchName|slug> from the current HEAD; \"keep\" — stay on current branch. When auto and a decision is needed (dirty workspace / on main etc.), the tool returns needs-decision for you to ask the user first."
|
|
3594
|
+
},
|
|
3595
|
+
branchName: {
|
|
3596
|
+
type: "string",
|
|
3597
|
+
description: "Custom branch name (used when branchPolicy=auto; defaults to the triage slug; [a-z0-9-_])"
|
|
3598
|
+
},
|
|
3599
|
+
preAction: {
|
|
3600
|
+
type: "string",
|
|
3601
|
+
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)"
|
|
3602
|
+
},
|
|
3603
|
+
commitMessage: {
|
|
3604
|
+
type: "string",
|
|
3605
|
+
description: "Custom commit message when preAction=commit"
|
|
3606
|
+
},
|
|
3341
3607
|
tasks: {
|
|
3342
3608
|
type: "array",
|
|
3343
3609
|
description: "Optional splittable dev task list",
|
|
@@ -3362,16 +3628,32 @@ function registerTools(ctx) {
|
|
|
3362
3628
|
schema: {
|
|
3363
3629
|
type: "object",
|
|
3364
3630
|
additionalProperties: false,
|
|
3365
|
-
required: ["
|
|
3631
|
+
required: ["status"],
|
|
3366
3632
|
properties: {
|
|
3367
3633
|
runId: { type: "string" },
|
|
3368
|
-
status: { type: "string" }
|
|
3634
|
+
status: { type: "string" },
|
|
3635
|
+
question: { type: "string" },
|
|
3636
|
+
options: { type: "array" },
|
|
3637
|
+
note: { type: "string" }
|
|
3369
3638
|
}
|
|
3370
3639
|
},
|
|
3371
|
-
render: (args, value) =>
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3640
|
+
render: (args, value) => {
|
|
3641
|
+
if (value && value.status === "needs-decision") {
|
|
3642
|
+
const opts = Array.isArray(value.options) ? value.options.map((o, i) => `${i + 1}. ${o.label}`).join("\n") : "";
|
|
3643
|
+
return [{
|
|
3644
|
+
type: "text",
|
|
3645
|
+
text: `【分支决策】${value.question}\n${opts}\n(也可自定义输入)——请询问用户选择,确认后以 teamflow_start 的 branchPolicy/branchName/preAction/commitMessage 参数重新调用。`
|
|
3646
|
+
}];
|
|
3647
|
+
}
|
|
3648
|
+
if (value && value.status === "needs-confirmation") return [{
|
|
3649
|
+
type: "text",
|
|
3650
|
+
text: `【需求确认】${value.question}\n${value.note || ""}——请按此询问用户后再决定。`
|
|
3651
|
+
}];
|
|
3652
|
+
return [{
|
|
3653
|
+
type: "text",
|
|
3654
|
+
text: `团队研发流水线已启动(runId=${value.runId},${value.status}),正在后台执行。【重要】你现在停手:不要自行读取/修改代码实现该需求,不要重复跑测试验证——实现、QA、汇报由流水线各阶段完成。你只需告知用户流水线已启动,等待流水线完成后的官方完成汇报,再向用户转述结果。可用 teamflow_status 查询进度/阶段 token;backlog 已持久化到 $DSH_HOME/teamflow。`
|
|
3655
|
+
}];
|
|
3656
|
+
}
|
|
3375
3657
|
},
|
|
3376
3658
|
async execute(args, exec) {
|
|
3377
3659
|
const parent = exec && exec.agent;
|
|
@@ -3388,18 +3670,104 @@ function registerTools(ctx) {
|
|
|
3388
3670
|
status: "no-team",
|
|
3389
3671
|
message: "请先通过输入框旁的 🏭 按钮选择团队,再发送需求消息。未选团队时不走 teamflow。"
|
|
3390
3672
|
};
|
|
3673
|
+
const rawReq = typeof args.requirement === "string" ? args.requirement : "";
|
|
3674
|
+
if (/是不是|要不要|需不需要|是否应该|要不要考虑|建议|我感觉|感觉不出|我们是不是|咱是不是/.test(rawReq)) return {
|
|
3675
|
+
status: "needs-confirmation",
|
|
3676
|
+
question: `这条消息(「${rawReq.slice(0, 60)}」)更像反馈/建议(含疑问句式)而非明确开发需求。请先向用户确认:是否要实现?`,
|
|
3677
|
+
note: "用户确认要实现后,请把明确需求(如「实现 combo/t-spin 触发 Toast 提示」)作为 requirement 重新调用 teamflow_start;若用户只是表达感受/讨论,直接正常回复即可。"
|
|
3678
|
+
};
|
|
3391
3679
|
try {
|
|
3680
|
+
const requirement = typeof args.requirement === "string" && args.requirement.trim() ? args.requirement.trim() : "(未提供需求)";
|
|
3681
|
+
const options = {
|
|
3682
|
+
needDesign: !!args.needDesign,
|
|
3683
|
+
needScaffold: !!args.needScaffold,
|
|
3684
|
+
lite: !!args.lite,
|
|
3685
|
+
mode: normalizeMode(args.mode) || void 0,
|
|
3686
|
+
teamId,
|
|
3687
|
+
tasks: normalizeTasks(args.tasks),
|
|
3688
|
+
productRoot: normalizeRoot(args.productRoot),
|
|
3689
|
+
maxConcurrency: args.maxConcurrency,
|
|
3690
|
+
branchPolicy: args.branchPolicy === "keep" ? "keep" : "auto",
|
|
3691
|
+
branchName: typeof args.branchName === "string" && args.branchName.trim() ? args.branchName.trim() : null,
|
|
3692
|
+
preAction: args.preAction === "stash" || args.preAction === "commit" ? args.preAction : null,
|
|
3693
|
+
commitMessage: typeof args.commitMessage === "string" && args.commitMessage.trim() ? args.commitMessage.trim() : null
|
|
3694
|
+
};
|
|
3695
|
+
if (options.branchPolicy === "auto" && !options.preAction && exec && exec.agent) {
|
|
3696
|
+
const sc = workspaceScopeOf(exec.agent);
|
|
3697
|
+
if (sc.path) try {
|
|
3698
|
+
const s = runSanityCheck(sc.path);
|
|
3699
|
+
if (s.ok && s.inRepo) {
|
|
3700
|
+
const onMain = !!s.branch && s.branch.trim().toLowerCase() === "main";
|
|
3701
|
+
const dirty = s.hasDirty;
|
|
3702
|
+
const dirtyN = s.dirty.split(/\r?\n/).filter((l) => l.trim()).length;
|
|
3703
|
+
let question = "";
|
|
3704
|
+
let optionsList = [];
|
|
3705
|
+
if (onMain && !dirty) {
|
|
3706
|
+
question = `工作区 ${sc.path} 当前在 main 分支(工作区干净)。流水线默认在特性分支上开发,请选择:`;
|
|
3707
|
+
optionsList = [{
|
|
3708
|
+
label: "基于 main 新建分支开发(推荐,分支名取需求 slug,可自定义)",
|
|
3709
|
+
value: "auto"
|
|
3710
|
+
}, {
|
|
3711
|
+
label: "直接在 main 上开发",
|
|
3712
|
+
value: "keep"
|
|
3713
|
+
}];
|
|
3714
|
+
} else if (onMain && dirty) {
|
|
3715
|
+
question = `工作区 ${sc.path} 当前在 main 分支,且有 ${dirtyN} 处未提交改动。请选择启动方式:`;
|
|
3716
|
+
optionsList = [
|
|
3717
|
+
{
|
|
3718
|
+
label: `stash 现有改动后新建分支开发(推荐,改动暂存,流水线完成后 git stash pop 恢复)`,
|
|
3719
|
+
value: "stash+auto"
|
|
3720
|
+
},
|
|
3721
|
+
{
|
|
3722
|
+
label: "提交现有改动后新建分支开发(提交信息可自定义)",
|
|
3723
|
+
value: "commit+auto"
|
|
3724
|
+
},
|
|
3725
|
+
{
|
|
3726
|
+
label: "直接在 main 上继续(未提交改动将混入本次开发)",
|
|
3727
|
+
value: "keep"
|
|
3728
|
+
}
|
|
3729
|
+
];
|
|
3730
|
+
} else if (!onMain && !dirty) {
|
|
3731
|
+
question = `工作区 ${sc.path} 当前在特性分支 ${s.branch}(工作区干净)。请选择启动方式:`;
|
|
3732
|
+
optionsList = [{
|
|
3733
|
+
label: "沿用当前分支开发(推荐)",
|
|
3734
|
+
value: "keep"
|
|
3735
|
+
}, {
|
|
3736
|
+
label: "基于当前分支再新建子分支开发",
|
|
3737
|
+
value: "auto"
|
|
3738
|
+
}];
|
|
3739
|
+
} else {
|
|
3740
|
+
question = `工作区 ${sc.path} 当前在特性分支 ${s.branch},且有 ${dirtyN} 处未提交改动。请选择启动方式:`;
|
|
3741
|
+
optionsList = [
|
|
3742
|
+
{
|
|
3743
|
+
label: "stash 现有改动后沿用当前分支开发(推荐,完成后 git stash pop 恢复)",
|
|
3744
|
+
value: "stash+keep"
|
|
3745
|
+
},
|
|
3746
|
+
{
|
|
3747
|
+
label: "直接沿用当前分支(未提交改动混入本次开发)",
|
|
3748
|
+
value: "keep"
|
|
3749
|
+
},
|
|
3750
|
+
{
|
|
3751
|
+
label: "stash 现有改动后新建子分支开发",
|
|
3752
|
+
value: "stash+auto"
|
|
3753
|
+
},
|
|
3754
|
+
{
|
|
3755
|
+
label: "提交现有改动后新建子分支开发",
|
|
3756
|
+
value: "commit+auto"
|
|
3757
|
+
}
|
|
3758
|
+
];
|
|
3759
|
+
}
|
|
3760
|
+
return {
|
|
3761
|
+
status: "needs-decision",
|
|
3762
|
+
question,
|
|
3763
|
+
options: optionsList,
|
|
3764
|
+
note: "选项之外可自定义输入(如指定分支名)。确认选择后,请以 teamflow_start 的 branchPolicy / branchName / preAction / commitMessage 参数重新调用本工具。"
|
|
3765
|
+
};
|
|
3766
|
+
}
|
|
3767
|
+
} catch (e) {}
|
|
3768
|
+
}
|
|
3392
3769
|
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),
|
|
3770
|
+
runId: startPipeline(parent, requirement, options, exec && exec.signal),
|
|
3403
3771
|
status: "running"
|
|
3404
3772
|
};
|
|
3405
3773
|
} catch (e) {
|
|
@@ -3407,6 +3775,88 @@ function registerTools(ctx) {
|
|
|
3407
3775
|
}
|
|
3408
3776
|
}
|
|
3409
3777
|
});
|
|
3778
|
+
T({
|
|
3779
|
+
name: "teamflow_merge",
|
|
3780
|
+
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).",
|
|
3781
|
+
parameters: {
|
|
3782
|
+
action: {
|
|
3783
|
+
type: "string",
|
|
3784
|
+
required: true,
|
|
3785
|
+
description: "\"merge\" (host performs the merge) / \"command\" (print the manual merge command) / \"keep\" (defer, mark kept)"
|
|
3786
|
+
},
|
|
3787
|
+
runId: {
|
|
3788
|
+
type: "string",
|
|
3789
|
+
description: "Run id (defaults to the latest completed run of this workspace)"
|
|
3790
|
+
}
|
|
3791
|
+
},
|
|
3792
|
+
output: {
|
|
3793
|
+
schema: {
|
|
3794
|
+
type: "object",
|
|
3795
|
+
additionalProperties: false,
|
|
3796
|
+
required: ["status"],
|
|
3797
|
+
properties: {
|
|
3798
|
+
status: { type: "string" },
|
|
3799
|
+
message: { type: "string" },
|
|
3800
|
+
command: { type: "string" }
|
|
3801
|
+
}
|
|
3802
|
+
},
|
|
3803
|
+
render: (args, value) => [{
|
|
3804
|
+
type: "text",
|
|
3805
|
+
text: value.message
|
|
3806
|
+
}]
|
|
3807
|
+
},
|
|
3808
|
+
async execute(args, exec) {
|
|
3809
|
+
const action = args && args.action;
|
|
3810
|
+
if (action !== "merge" && action !== "command" && action !== "keep") throw new Error("action 必须是 merge / command / keep");
|
|
3811
|
+
const sc = workspaceScopeOf(exec && exec.agent);
|
|
3812
|
+
if (!sc.path) throw new Error("当前会话无项目工作区");
|
|
3813
|
+
const key = sc.projectKey;
|
|
3814
|
+
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];
|
|
3815
|
+
if (!target) throw new Error("未找到已完成流水线(可传 runId 指定)");
|
|
3816
|
+
const branch = gitCmd(sc.path, [
|
|
3817
|
+
"rev-parse",
|
|
3818
|
+
"--abbrev-ref",
|
|
3819
|
+
"HEAD"
|
|
3820
|
+
]);
|
|
3821
|
+
if (!branch || branch === "main") return {
|
|
3822
|
+
status: "noop",
|
|
3823
|
+
message: "当前已在 main 分支,无需合回"
|
|
3824
|
+
};
|
|
3825
|
+
if (action === "command") return {
|
|
3826
|
+
status: "command",
|
|
3827
|
+
command: `git checkout main && git merge --no-ff ${branch}`,
|
|
3828
|
+
message: `请用户在项目目录执行以下命令完成合回(合回后可 git branch -d ${branch} 清理特性分支):\ngit checkout main && git merge --no-ff ${branch}`
|
|
3829
|
+
};
|
|
3830
|
+
if (action === "keep") {
|
|
3831
|
+
target.mergeStatus = "kept";
|
|
3832
|
+
persistJournal(target);
|
|
3833
|
+
return {
|
|
3834
|
+
status: "kept",
|
|
3835
|
+
message: `已标记暂不合回:特性分支 ${branch} 保留,后续可随时调用 teamflow_merge 合回`
|
|
3836
|
+
};
|
|
3837
|
+
}
|
|
3838
|
+
const co = gitCmd(sc.path, ["checkout", "main"]);
|
|
3839
|
+
const mg = co === null ? null : gitCmd(sc.path, [
|
|
3840
|
+
"merge",
|
|
3841
|
+
"--no-ff",
|
|
3842
|
+
branch
|
|
3843
|
+
]);
|
|
3844
|
+
if (co === null || mg === null) {
|
|
3845
|
+
target.mergeStatus = "failed";
|
|
3846
|
+
persistJournal(target);
|
|
3847
|
+
return {
|
|
3848
|
+
status: "failed",
|
|
3849
|
+
message: `合并失败(工作区可能不干净或有冲突)。请人工处理:先提交/处理当前工作区改动,再执行 git merge --no-ff ${branch}(冲突文件需手动解决)`
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
target.mergeStatus = "merged";
|
|
3853
|
+
persistJournal(target);
|
|
3854
|
+
return {
|
|
3855
|
+
status: "merged",
|
|
3856
|
+
message: `✅ 已合回 main(git merge --no-ff ${branch})。如需清理特性分支:git branch -d ${branch}`
|
|
3857
|
+
};
|
|
3858
|
+
}
|
|
3859
|
+
});
|
|
3410
3860
|
T({
|
|
3411
3861
|
name: "teamflow_triage",
|
|
3412
3862
|
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 +4192,25 @@ function registerTools(ctx) {
|
|
|
3742
4192
|
}
|
|
3743
4193
|
/** 会话级暂停标记:pausedSessions.has(sessionId) → 该会话的 teamflow_start 被拦截。 */
|
|
3744
4194
|
const pausedSessions = /* @__PURE__ */ new Set();
|
|
3745
|
-
/** 会话级当前团队:activeTeams.get(sessionId) → 当前选中的团队 id。
|
|
4195
|
+
/** 会话级当前团队:activeTeams.get(sessionId) → 当前选中的团队 id。
|
|
4196
|
+
* 持久化到 $DSH_HOME/teamflow/active-teams.json——重启后恢复(实锤:重启/刷新后内存清空,
|
|
4197
|
+
* UI 显示无团队,但 agent 上下文记忆 teamId 显式传入仍启动流水线,UI 状态与启动通道不一致)。 */
|
|
3746
4198
|
const activeTeams = /* @__PURE__ */ new Map();
|
|
4199
|
+
const ACTIVE_TEAMS_FILE = () => join(teamflowRoot(), "active-teams.json");
|
|
4200
|
+
function loadActiveTeams() {
|
|
4201
|
+
try {
|
|
4202
|
+
const raw = readJsonAny(ACTIVE_TEAMS_FILE(), null);
|
|
4203
|
+
if (raw && typeof raw === "object") {
|
|
4204
|
+
for (const [k, v] of Object.entries(raw)) if (typeof k === "string" && typeof v === "string") activeTeams.set(k, v);
|
|
4205
|
+
}
|
|
4206
|
+
} catch (e) {}
|
|
4207
|
+
}
|
|
4208
|
+
function saveActiveTeams() {
|
|
4209
|
+
try {
|
|
4210
|
+
mkdirSync(teamflowRoot(), { recursive: true });
|
|
4211
|
+
writeJson(ACTIVE_TEAMS_FILE(), Object.fromEntries(activeTeams));
|
|
4212
|
+
} catch (e) {}
|
|
4213
|
+
}
|
|
3747
4214
|
/** 延迟注入队列:选团队时 agent 可能尚未加载,存入 pending,后续时机补发。 */
|
|
3748
4215
|
const pendingInjections = /* @__PURE__ */ new Map();
|
|
3749
4216
|
/** 尝试补发延迟注入:agent 可用时注入上下文并清除 pending。 */
|
|
@@ -3778,11 +4245,13 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
3778
4245
|
"subagents",
|
|
3779
4246
|
"tokenMeter",
|
|
3780
4247
|
"typert",
|
|
3781
|
-
"tools"
|
|
4248
|
+
"tools",
|
|
4249
|
+
"llm"
|
|
3782
4250
|
];
|
|
3783
4251
|
constructor(ctx) {
|
|
3784
4252
|
super(ctx, "teamflow");
|
|
3785
|
-
setRuntime(ctx.get("agents"), ctx.get("subagents"), ctx.get("tokenMeter"), ctx.get("workspaceRegistry"), ctx.get("agentDefaultModel"));
|
|
4253
|
+
setRuntime(ctx.get("agents"), ctx.get("subagents"), ctx.get("tokenMeter"), ctx.get("workspaceRegistry"), ctx.get("agentDefaultModel"), ctx.get("llm"));
|
|
4254
|
+
loadActiveTeams();
|
|
3786
4255
|
let interruptedCount = 0;
|
|
3787
4256
|
try {
|
|
3788
4257
|
for (const { journal, wasInterrupted } of loadJournals()) {
|
|
@@ -4083,6 +4552,7 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4083
4552
|
error: `团队 ${tid} 不存在`
|
|
4084
4553
|
};
|
|
4085
4554
|
activeTeams.set(sid, tid);
|
|
4555
|
+
saveActiveTeams();
|
|
4086
4556
|
const agent = runtime.agents && runtime.agents.get(sid);
|
|
4087
4557
|
const injectPayload = {
|
|
4088
4558
|
type: "user",
|
|
@@ -4139,6 +4609,7 @@ var TeamflowService = class extends TypertRemoteService {
|
|
|
4139
4609
|
error: "缺少 sessionId"
|
|
4140
4610
|
};
|
|
4141
4611
|
activeTeams.delete(sid);
|
|
4612
|
+
saveActiveTeams();
|
|
4142
4613
|
return { ok: true };
|
|
4143
4614
|
}
|
|
4144
4615
|
/** 从断点续跑:跳过已完成阶段,从第一个未完成阶段重跑。 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-teamflow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "TeamFlow 团队研发流水线(TypeScript):需求→PRD→设计→架构→技术方案→并行开发→QA→验收;backlog 持久化 + 断点续跑 + 完成汇报 + 防假交付;Web 团队工作台(conversation.view tab + 拖拽看板)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/host.mjs",
|
|
@@ -76,14 +76,30 @@
|
|
|
76
76
|
"@deepseek-ai/dsh-typert-protocol": "*"
|
|
77
77
|
},
|
|
78
78
|
"peerDependenciesMeta": {
|
|
79
|
-
"@deepseek-ai/dsh-base": {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"@deepseek-ai/dsh-client-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"@deepseek-ai/dsh-
|
|
86
|
-
|
|
79
|
+
"@deepseek-ai/dsh-base": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"@deepseek-ai/dsh-client-locale": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"@deepseek-ai/dsh-client-runtime": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"@deepseek-ai/dsh-client-ui-conversation": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
91
|
+
"@deepseek-ai/dsh-llm": {
|
|
92
|
+
"optional": true
|
|
93
|
+
},
|
|
94
|
+
"@deepseek-ai/dsh-tools": {
|
|
95
|
+
"optional": true
|
|
96
|
+
},
|
|
97
|
+
"@deepseek-ai/dsh-typert-protocol": {
|
|
98
|
+
"optional": true
|
|
99
|
+
},
|
|
100
|
+
"react": {
|
|
101
|
+
"optional": true
|
|
102
|
+
}
|
|
87
103
|
},
|
|
88
104
|
"license": "MIT",
|
|
89
105
|
"repository": {
|