dsh-plugin-teamflow 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.en.md +22 -0
- package/README.md +22 -0
- package/lib/host.mjs +311 -240
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
> 本插件首次公开发布版本为 **v0.1.0**;发布前的内部迭代(v0.3~v0.13)记录于 `AGENTS.md` §5,对外统一归到 v0.1.0。
|
|
4
4
|
|
|
5
|
+
## [0.1.5] - 2026-09-01
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
- **patch 档兑现「单 agent 直改 + 自测即交付」**:阶段集精简为确认单 → 单 agent 直改(2 段,无技术方案/QA/验收),开发完成(自测通过)即统一收口提交。UI 微调(按钮换位置/挪控件/改文案/间距/颜色)、常量调整、单点修复、回滚、笔误修正走最轻路径
|
|
9
|
+
|
|
10
|
+
### 改进
|
|
11
|
+
- **需求分诊重构为模型主导**:档位判断交给模型(自然语言语义,天然双语);正则收窄为确定性护栏(架构信号强升/UI 不低于轻量档/显式设计升档)。UI 微调类需求(不改变行为/交互逻辑)不再误判标准档
|
|
12
|
+
- **分诊输出失败自动纠错重试**:只输出开场白无 JSON 时带提示重试一次,仍失败才走正则兜底
|
|
13
|
+
|
|
14
|
+
### 修复
|
|
15
|
+
- **外部中止(aborted)不再误报「预算熔断」**:重启等外部中止单独标记,明确引导断点续跑(补跑失败任务,已完成任务复用)
|
|
16
|
+
- **patch 档不再误跑架构蓝图**(技术方案块补 `enabled('tech')` 门控)
|
|
17
|
+
- **分支名不再被档位词污染**(「显式的用 patch 模式」不再产出 `feat/patch`)
|
|
18
|
+
- **provider 错误细节记录**(阶段失败记录底层错误信息)
|
|
19
|
+
|
|
20
|
+
## [0.1.4] - 2026-08-29
|
|
21
|
+
|
|
22
|
+
### 修复
|
|
23
|
+
- **分支决策死循环**:用户确认「新建分支」后重发启动参数仍会再次弹出分支决策(干净工作区场景)。修复:决策选项增加显式确认值,自定义分支名/工作区处理参数均视为已确认
|
|
24
|
+
- **护栏误杀大文件任务**:大文件「读-改-读-改」是正常模式(每次修改后必须重读确认),可能被误判为推理复读而中止。修复:复读判定升级为状态判定——有实际修改进展时不中止,仅纯复读(零进展)才中止
|
|
25
|
+
- **QA/验收只读任务不再被误杀**:执行测试脚本视为进展信号,只读分析任务(不做文件修改)不再被复读检测误伤
|
|
26
|
+
- **退化中止不再自动重试**:真退化(推理复读死循环)后自动重试大概率在污染会话内复现且持续烧钱——改为直接需人工介入,引导 `teamflow_resume`(全新会话续跑)
|
|
27
|
+
- **断点续跑尊重未闭环的 QA 缺陷**:阻断缺陷仍 open 时 resume 回到 QA 修复-复验闭环,不再带着已知缺陷直接进产品验收;验收失败过的 run 同样正确回 QA
|
|
28
|
+
- **历史失败记录保留**:resume 不再清除失败的阶段记录(审计可追溯)
|
|
29
|
+
|
|
30
|
+
### 新增
|
|
31
|
+
- **提测门禁**:开发阶段有任务失败(哪怕 1 个)→ 需人工介入,不再自动进入 QA(失败任务是已知缺口,QA 检查必然重复报告);resume 精确补跑失败任务(已完成任务产物复用,不重跑)
|
|
32
|
+
- **README 界面预览**:5 张真实工作台截图(流水线视图/看板/阶段详情/看板任务详情/团队选择)
|
|
33
|
+
|
|
34
|
+
### 其他
|
|
35
|
+
- pnpm-lockfile 同步(依赖调整后 CI 的 frozen-lockfile 校验失败,已修复)
|
|
36
|
+
|
|
5
37
|
## [0.1.3] - 2026-08-29
|
|
6
38
|
|
|
7
39
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -18,6 +18,28 @@ requirement → PRD (based on existing patterns / product memory, archived to pr
|
|
|
18
18
|
→ product acceptance (update product memory)
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Screenshots
|
|
22
|
+
|
|
23
|
+
1. Pipeline view — stage serpentine lanes + node cards (status / duration / tokens / subagent session)
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
2. Backlog board — draggable lanes for requirements / tasks / defects
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
3. Stage detail drawer — full stage artifacts + token breakdown + "🎬 jump to subagent session"
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
4. Board task detail — task-card drawer (requirement text / assignments / event timeline / subtasks / defects / tokens)
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
5. Team selector — 🏭 button + team dropdown
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
21
43
|
## Core Features
|
|
22
44
|
|
|
23
45
|
- **Anti-fake-delivery**: ① Substantive validation — outputs containing rejection phrases ("I cannot complete", etc.) or below the per-stage length floor are treated as undelivered and routed to retry / human intervention; ② Token circuit breaker — a single stage accumulating 60k budget stops retries; ③ Context-exhaustion failures are not retried (retrying the same prompt likely reproduces); ④ Product-level concurrency lock — only one active pipeline per product at a time, preventing requirement state from stepping on itself; ⑤ Full stage outputs are retained (memory + disk) for the detail drawer and checkpoint resume.
|
package/README.md
CHANGED
|
@@ -18,6 +18,28 @@ TeamFlow 团队研发流水线 —— DeepSeek Harness 可分发插件(`dsh pl
|
|
|
18
18
|
→ 产品验收(更新产品记忆)
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## 界面预览
|
|
22
|
+
|
|
23
|
+
1. 流水线视图——阶段蛇形泳道 + 节点卡片(状态/耗时/token/子代理会话)
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
2. Backlog 看板——需求/任务/缺陷拖拽泳道
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
3. 阶段详情抽屉——阶段性产物全文 + token 明细 +「🎬 跳转子代理会话」
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
4. 看板任务详情——任务卡抽屉(需求原文/分配/事件时间线/子卡/缺陷/token)
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
5. 团队选择——🏭 按钮 + 团队下拉
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
21
43
|
## 核心特性
|
|
22
44
|
|
|
23
45
|
- **防假交付**:① 实质校验——拒绝措辞("我无法完成"等)或低于阶段长度下限的输出视为未交付,走重试/需人工;② token 熔断——单阶段累计 60k 预算,超限停止重试;③ 上下文耗尽类失败不重试(重试同一 prompt 大概率复现);④ 产品级并发锁——同一产品同时只允许一条活跃流水线,防需求状态互踩;⑤ 阶段产物全文保留(内存 + 磁盘,供详情抽屉与断点续跑读取)。
|
package/lib/host.mjs
CHANGED
|
@@ -157,9 +157,7 @@ const STAGE_POLICY = {
|
|
|
157
157
|
key: "scaffold",
|
|
158
158
|
when: (o) => !!o.needScaffold
|
|
159
159
|
},
|
|
160
|
-
{ key: "
|
|
161
|
-
{ key: "dev" },
|
|
162
|
-
{ key: "acceptance" }
|
|
160
|
+
{ key: "dev" }
|
|
163
161
|
]
|
|
164
162
|
};
|
|
165
163
|
/** 按档位 + 条件展开实际执行阶段集(纯函数;未知档回退 full)。 */
|
|
@@ -266,7 +264,24 @@ function deriveBranchSlug(requirement, reqId, triageSlug, branchName) {
|
|
|
266
264
|
if (branchName && /^[a-z0-9][a-z0-9-_]*$/i.test(branchName)) return String(branchName).replace(/[^a-z0-9-]/gi, "-").toLowerCase().slice(0, 40);
|
|
267
265
|
if (triageSlug && /^[a-z0-9-]{3,24}$/i.test(triageSlug)) return triageSlug;
|
|
268
266
|
const en = String(requirement || "").match(/[a-zA-Z][a-zA-Z0-9-]{2,23}/g);
|
|
269
|
-
if (en && en.length)
|
|
267
|
+
if (en && en.length) {
|
|
268
|
+
const NOISE = /* @__PURE__ */ new Set([
|
|
269
|
+
"patch",
|
|
270
|
+
"lite",
|
|
271
|
+
"tech",
|
|
272
|
+
"full",
|
|
273
|
+
"medium",
|
|
274
|
+
"mode",
|
|
275
|
+
"the",
|
|
276
|
+
"and",
|
|
277
|
+
"for",
|
|
278
|
+
"with",
|
|
279
|
+
"use",
|
|
280
|
+
"using"
|
|
281
|
+
]);
|
|
282
|
+
const hit = en.find((w) => !NOISE.has(w.toLowerCase()));
|
|
283
|
+
if (hit) return hit.toLowerCase().slice(0, 40);
|
|
284
|
+
}
|
|
270
285
|
const num = String(reqId || "").match(/\d+/);
|
|
271
286
|
if (num) return `r${num[0]}`;
|
|
272
287
|
return "feature";
|
|
@@ -971,6 +986,17 @@ function verifyReqBugs(journal) {
|
|
|
971
986
|
}
|
|
972
987
|
if (touched) store.persist();
|
|
973
988
|
}
|
|
989
|
+
/** 该需求是否存在未闭环的阻断缺陷(P0/P1/P2 仍 open)——resume 断点定位与 QA 复用判定依赖。
|
|
990
|
+
* 实锤 run tf-mte906e9:QA 修复子代理失败 → run failed,但 QA 阶段本身 done(缺陷已登记),
|
|
991
|
+
* 旧 interruptedPhaseOf 直接定位产品验收 → 带缺陷代码进验收。
|
|
992
|
+
* ⚠️ store key 与 resumeRun/storeFor 解析一致(workspace || product || default),否则查错 store 误判无缺陷。 */
|
|
993
|
+
function hasOpenBlockingBugs(journal) {
|
|
994
|
+
try {
|
|
995
|
+
return storeFor(journal.workspace || journal.product || "default").bugs.some((b) => b.reqId === journal.reqId && b.status === "open" && b.severity !== "P3");
|
|
996
|
+
} catch (e) {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
974
1000
|
/** 默认团队配置文件内容。 */
|
|
975
1001
|
const DEFAULT_TEAMS_FILE = {
|
|
976
1002
|
version: 1,
|
|
@@ -1195,16 +1221,25 @@ function totalTokensOf(usage) {
|
|
|
1195
1221
|
* 的健康节奏工作到第 25 分钟被击落(tf-mt8fbavd dev 尝试一)。
|
|
1196
1222
|
*
|
|
1197
1223
|
* 设计原则:【只看进度信号,无任何时间配额】慢吞吐的合法任务永远不该被打断:
|
|
1198
|
-
* A. 复读检测:滑动窗口内同一规范化流式片段出现 ≥ GUARD_REPEAT_LIMIT
|
|
1199
|
-
*
|
|
1224
|
+
* A. 复读检测:滑动窗口内同一规范化流式片段出现 ≥ GUARD_REPEAT_LIMIT 次,且窗口内零变更进展
|
|
1225
|
+
* (无 edit/write 等写操作)→ 真退化(纯推理打转)→ outcome='degenerated'(豁免预算门,允许一次干净重试)。
|
|
1226
|
+
* ⚠️ 状态判定(实锤 run tf-mte906e9):大文件 read-edit 循环是正常模式——模型反复 read 同一大文件
|
|
1227
|
+
* (每次 edit 后内容已变,必须重读确认)、输出高度相似的「读后分析」,逐字片段在 400 条窗口内
|
|
1228
|
+
* 可累积 ≥12 次——伴随 edit/write 变更调用时只记录观察,不中止(否则大文件修改任务全被误杀)。
|
|
1200
1229
|
* B. 挂死检测:连续 GUARD_SILENCE_MS 一个新事件都没有(provider 层挂起/连接静默死亡)
|
|
1201
1230
|
* → outcome='stalled'(走正常预算门 → 熔断转人工,不自动重试烧钱)。
|
|
1202
1231
|
* C. 空转检测:会话仍在产出事件,但连续 GUARD_NO_TOOL_MS 没有任何工具调用
|
|
1203
1232
|
* (纯推理打转/改写式循环;正常 agent 每分钟都在调工具)→ outcome='stalled'。
|
|
1233
|
+
* 兜底关系:复读判定放宽后,edit 后陷入死循环的漏网场景由 C(长时间无工具调用)兜住。
|
|
1204
1234
|
*
|
|
1205
1235
|
* 中止方式:run.dispose() → run.result 结算。outcome 命名刻意避开 isUnretryable 的
|
|
1206
1236
|
* /token|context|limit/ 正则;只有 'degenerated' 享受干净重试豁免(runner.withRetry)。
|
|
1207
1237
|
*/
|
|
1238
|
+
/** 进展工具(复读状态判定):变更类写操作 + 脚本执行。
|
|
1239
|
+
* 「有进展」= 大文件 read-edit 循环(dev)或只读分析任务的 read+跑脚本循环(QA/验收)均属正常模式;
|
|
1240
|
+
* 纯 read 循环(反复整读同一文件却无变更/无脚本执行)= 真退化。实锤 run tf-mte906e9:QA 重跑
|
|
1241
|
+
* 只读分析(不 edit)→ 旧判定「零变更进展」误杀,第 2 次 provider error 后 450k 熔断。 */
|
|
1242
|
+
const PROGRESS_TOOLS = /^(edit|write|create|apply_patch|patch|remove|delete|rm|mkdir|move|rename|append|bash|pwsh|shell|powershell)$/i;
|
|
1208
1243
|
/** 与 metering 同款事件访问器(session.events 可能是数组或返回数组的函数)。 */
|
|
1209
1244
|
function eventsOf(run) {
|
|
1210
1245
|
const local = run && run.localAgent;
|
|
@@ -1267,6 +1302,8 @@ function startStageGuard(opts) {
|
|
|
1267
1302
|
const warnedReads = /* @__PURE__ */ new Set();
|
|
1268
1303
|
const scriptCounts = /* @__PURE__ */ new Map();
|
|
1269
1304
|
const warnedScripts = /* @__PURE__ */ new Set();
|
|
1305
|
+
let lastMutationAt = 0;
|
|
1306
|
+
let repeatWarned = false;
|
|
1270
1307
|
function warnOnce(key, set, message, hint) {
|
|
1271
1308
|
if (set.has(key)) return;
|
|
1272
1309
|
set.add(key);
|
|
@@ -1323,8 +1360,21 @@ function startStageGuard(opts) {
|
|
|
1323
1360
|
const counts = /* @__PURE__ */ new Map();
|
|
1324
1361
|
for (const w of window) counts.set(w, (counts.get(w) || 0) + 1);
|
|
1325
1362
|
for (const [, n] of counts) if (n >= 12) {
|
|
1326
|
-
|
|
1327
|
-
|
|
1363
|
+
if (lastMutationAt > 0) {
|
|
1364
|
+
if (!repeatWarned) {
|
|
1365
|
+
repeatWarned = true;
|
|
1366
|
+
try {
|
|
1367
|
+
journal.logs.push({
|
|
1368
|
+
t: Date.now(),
|
|
1369
|
+
level: "warn",
|
|
1370
|
+
message: `${label} 复读计数达阈值但检测到变更进展(read-edit 循环属正常模式),不中止;仅零变更进展的纯复读才中止`
|
|
1371
|
+
});
|
|
1372
|
+
} catch (e) {}
|
|
1373
|
+
}
|
|
1374
|
+
} else {
|
|
1375
|
+
fire(`推理复读(同一片段在近 ${window.length} 条流式片段中出现 ${n} 次,且窗口内零变更进展)`, "degenerated");
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1328
1378
|
}
|
|
1329
1379
|
observeToolCalls(newEvents);
|
|
1330
1380
|
if (newEvents.some((ev) => ev?.type === "step/end")) flushReminders(run);
|
|
@@ -1332,9 +1382,12 @@ function startStageGuard(opts) {
|
|
|
1332
1382
|
}
|
|
1333
1383
|
for (const ev of events.slice(-200)) {
|
|
1334
1384
|
const e = ev;
|
|
1335
|
-
if (e
|
|
1385
|
+
if (!e) continue;
|
|
1386
|
+
if (e.type === "tool-call-chunks" || e.type === "tool/call") {
|
|
1336
1387
|
seenToolCall = true;
|
|
1337
1388
|
lastToolSignalAt = Date.now();
|
|
1389
|
+
const d = e.data || e;
|
|
1390
|
+
if (d && typeof d.name === "string" && PROGRESS_TOOLS.test(d.name)) lastMutationAt = Date.now();
|
|
1338
1391
|
break;
|
|
1339
1392
|
}
|
|
1340
1393
|
}
|
|
@@ -1531,6 +1584,7 @@ async function runAgent(journal, parent, label, phase, prompt, signal) {
|
|
|
1531
1584
|
}
|
|
1532
1585
|
stage.status = "failed";
|
|
1533
1586
|
stage.outcome = stop === "completed" && text ? "insubstantial" : stop || "error";
|
|
1587
|
+
const errDetail = result && result.error;
|
|
1534
1588
|
if (stage.outcome === "insubstantial") {
|
|
1535
1589
|
stage.summary = "产出未通过实质校验(含拒绝措辞或内容过短),视为未交付";
|
|
1536
1590
|
journal.logs.push({
|
|
@@ -1539,11 +1593,11 @@ async function runAgent(journal, parent, label, phase, prompt, signal) {
|
|
|
1539
1593
|
message: `${label} 产出未通过实质校验(拒绝措辞/内容过短)`
|
|
1540
1594
|
});
|
|
1541
1595
|
} else {
|
|
1542
|
-
stage.summary = `未产出有效结果(stopReason=${stop || "unknown"})`;
|
|
1596
|
+
stage.summary = `未产出有效结果(stopReason=${stop || "unknown"}${errDetail ? `,error=${String(errDetail).slice(0, 200)}` : ""})`;
|
|
1543
1597
|
journal.logs.push({
|
|
1544
1598
|
t: Date.now(),
|
|
1545
1599
|
level: "error",
|
|
1546
|
-
message: `${label}
|
|
1600
|
+
message: `${label} ${stage.summary}`
|
|
1547
1601
|
});
|
|
1548
1602
|
}
|
|
1549
1603
|
return null;
|
|
@@ -1605,7 +1659,33 @@ async function withRetry(journal, parent, label, phase, prompt, signal) {
|
|
|
1605
1659
|
stageTokens
|
|
1606
1660
|
};
|
|
1607
1661
|
}
|
|
1608
|
-
if (
|
|
1662
|
+
if (lastStage && lastStage.outcome === "aborted") {
|
|
1663
|
+
journal.logs.push({
|
|
1664
|
+
t: Date.now(),
|
|
1665
|
+
level: "warn",
|
|
1666
|
+
message: `${label} 被外部中止(aborted),未正常产出——非预算问题;可 teamflow_resume 续跑(补跑失败任务,已完成任务复用)`
|
|
1667
|
+
});
|
|
1668
|
+
journal.humanIntervention = true;
|
|
1669
|
+
return {
|
|
1670
|
+
text: null,
|
|
1671
|
+
attempts,
|
|
1672
|
+
stageTokens
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
if (lastStage && lastStage.outcome === "degenerated") {
|
|
1676
|
+
journal.logs.push({
|
|
1677
|
+
t: Date.now(),
|
|
1678
|
+
level: "warn",
|
|
1679
|
+
message: `${label} 进行中护栏中止(退化/推理复读),不再自动重试(污染会话内重试大概率复现且烧钱);可 teamflow_resume 以全新会话续跑`
|
|
1680
|
+
});
|
|
1681
|
+
journal.humanIntervention = true;
|
|
1682
|
+
return {
|
|
1683
|
+
text: null,
|
|
1684
|
+
attempts,
|
|
1685
|
+
stageTokens
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
if (stageTokens >= 6e4) {
|
|
1609
1689
|
journal.logs.push({
|
|
1610
1690
|
t: Date.now(),
|
|
1611
1691
|
level: "error",
|
|
@@ -2117,8 +2197,8 @@ ${vision ? "[Visual re-check] If QA saved screenshots under the task folder, spo
|
|
|
2117
2197
|
5. Chinese Markdown.
|
|
2118
2198
|
6. [State] End with a state block (phase="acceptance"), summary = acceptance conclusion, verdict = "accepted/rework/reject/needs-human", extra.done = confirmation of this delivery.${STATE_BLOCK_INSTRUCTION}`;
|
|
2119
2199
|
/** 需求分诊模型 prompt(模型驱动 triage;供 core/triage.runTriage 使用)。 */
|
|
2120
|
-
const TRIAGE_PROMPT = (requirement, opts, pre) => `You are a senior research-dev triage analyst. Do ONE thing: analyze which pipeline mode this dev requirement fits, then give the conclusion. No code, no scope speculation.
|
|
2121
|
-
[RAW REQUIREMENT]
|
|
2200
|
+
const TRIAGE_PROMPT = (requirement, opts, pre, retryHint) => `You are a senior research-dev triage analyst. Do ONE thing: analyze which pipeline mode this dev requirement fits, then give the conclusion. No code, no scope speculation.
|
|
2201
|
+
${retryHint ? `[RETRY — YOUR LAST REPLY FAILED]\n${retryHint}\n` : ""}[RAW REQUIREMENT]
|
|
2122
2202
|
${requirement}
|
|
2123
2203
|
${pre.rationale.length ? `\n[REGEX PRE-FILTER SIGNALS (reference only; judge semantically, don't blindly follow)]\n${pre.rationale.join("\n")}` : ""}
|
|
2124
2204
|
\n[OPTIONAL SIGNAL] UI work needed: ${opts && opts.needDesign ? "yes" : "not flagged"}
|
|
@@ -2132,12 +2212,12 @@ ${pre.rationale.length ? `\n[REGEX PRE-FILTER SIGNALS (reference only; judge sem
|
|
|
2132
2212
|
|
|
2133
2213
|
[JUDGMENT POINTS]
|
|
2134
2214
|
1. Distinguish "user-visible functional change" vs "internal tech change": refactors/optimizations, even large code volume, usually go tech, not full.
|
|
2135
|
-
2. UI/
|
|
2215
|
+
2. **UI feature work** (new screens/components/interactions/pages) → at least medium (excludes patch/tech). **UI micro-adjustments** — moving a button, relocating a control, changing copy/labels, spacing/padding, color tweaks, "move the button", "switch the button position" — are patch-or-lite material, NOT medium: no new interaction logic, no design phase worth the token cost. The line: does it change behavior/interaction logic (medium) or just placement/appearance of existing elements (patch/lite)?
|
|
2136
2216
|
3. hotfix/single-point/pure numeric/pure docs → patch; clear "add feature X" → pick lite/medium/full by size.
|
|
2137
2217
|
4. Focused change (even with tests/regression) → lite/tech by nature; not necessarily full.
|
|
2138
2218
|
5. [M1 ARCHITECTURE CRITERION (important)] **Architecture-level changes** — persistence/localStorage/database/standalone module/abstraction/cross-many-files without an existing reusable wrapper (like a localStorage wrapper, storage layer, state management) — even if they look like "small features", go **at least medium** (must pass the architecture stage and produce a blueprint, avoiding scattered local implementations by dev); such changes collapse under a light "micro feature" tier. Tech-driven rework (refactor/optimize/arch upgrade) is itself tech (tech also runs the lightweight blueprint now).
|
|
2139
2219
|
|
|
2140
|
-
[OUTPUT]
|
|
2220
|
+
[OUTPUT] JSON object ONLY — no commentary, no preface, no closing text. The FIRST character of your reply must be '{'. Do NOT say anything like "here is the JSON" or "Let me output the JSON" — output the object itself:
|
|
2141
2221
|
{ "mode": "patch|lite|tech|medium|full", "slug": "<topic words> (3-24 lowercase letters/digits/hyphens, e.g. wallkick-toggle, 7bag-random; used to name the task folder)", "kind": "one-word nature", "needDesign": true|false, "complexity": "small|medium|large", "rationale": ["key argument 1","key argument 2"], "confidence": "high|medium|low" }`;
|
|
2142
2222
|
/** tech 档 PRD:技术变更单(无功能 AC,重范围/目标/改动面/回归)。 */
|
|
2143
2223
|
const techChangePrompt = (requirement, root, runId, state) => `You are the senior tech lead. The current workspace IS the target project. This is a **tech-driven rework** requirement (refactor/optimize/upgrade/architecture/dependencies/tech debt) — the product doesn't need a full feature PRD, but needs a **技术变更单** (tech change sheet) as the contract for dev/QA/acceptance and memory write-back.
|
|
@@ -2217,140 +2297,72 @@ const MODE_REGISTRY = {
|
|
|
2217
2297
|
desc: "单行/常量/版本号/hotfix:单 agent 直改+自测即交付(无独立 QA)"
|
|
2218
2298
|
}
|
|
2219
2299
|
};
|
|
2220
|
-
/**
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
"重构",
|
|
2244
|
-
"优化",
|
|
2245
|
-
"升级",
|
|
2246
|
-
"架构",
|
|
2247
|
-
"性能",
|
|
2248
|
-
"依赖",
|
|
2249
|
-
"脚手架",
|
|
2250
|
-
"缓存",
|
|
2251
|
-
"清理",
|
|
2252
|
-
"遗留债",
|
|
2253
|
-
"工程",
|
|
2254
|
-
"内部改造",
|
|
2255
|
-
"技术债"
|
|
2256
|
-
]
|
|
2257
|
-
},
|
|
2258
|
-
{
|
|
2259
|
-
mode: "lite",
|
|
2260
|
-
words: [
|
|
2261
|
-
"微功能",
|
|
2262
|
-
"小功能",
|
|
2263
|
-
"小增强",
|
|
2264
|
-
"加个",
|
|
2265
|
-
"补一个",
|
|
2266
|
-
"轻微",
|
|
2267
|
-
"顺手"
|
|
2268
|
-
]
|
|
2269
|
-
},
|
|
2270
|
-
{
|
|
2271
|
-
mode: "medium",
|
|
2272
|
-
words: [
|
|
2273
|
-
"界面",
|
|
2274
|
-
"UI",
|
|
2275
|
-
"视觉",
|
|
2276
|
-
"页面",
|
|
2277
|
-
"按钮",
|
|
2278
|
-
"样式",
|
|
2279
|
-
"交互",
|
|
2280
|
-
"布局",
|
|
2281
|
-
"组件",
|
|
2282
|
-
"持久化",
|
|
2283
|
-
"localStorage",
|
|
2284
|
-
"本地存储",
|
|
2285
|
-
"存储",
|
|
2286
|
-
"保存",
|
|
2287
|
-
"恢复",
|
|
2288
|
-
"存档",
|
|
2289
|
-
"独立模块",
|
|
2290
|
-
"抽象",
|
|
2291
|
-
"存储层",
|
|
2292
|
-
"sessionStorage",
|
|
2293
|
-
"IndexedDB",
|
|
2294
|
-
"跨模块",
|
|
2295
|
-
"数据层"
|
|
2296
|
-
]
|
|
2297
|
-
},
|
|
2298
|
-
{
|
|
2299
|
-
mode: "full",
|
|
2300
|
-
words: [
|
|
2301
|
-
"跨模块",
|
|
2302
|
-
"完整",
|
|
2303
|
-
"大型",
|
|
2304
|
-
"对接",
|
|
2305
|
-
"集成",
|
|
2306
|
-
"模块化",
|
|
2307
|
-
"重构为",
|
|
2308
|
-
"二期",
|
|
2309
|
-
"平台"
|
|
2310
|
-
]
|
|
2311
|
-
}
|
|
2300
|
+
/** 确定性护栏关键词(双语,仅 fallback 兜底用;主路由是模型——TRIAGE_PROMPT 语义判断)。
|
|
2301
|
+
* 架构信号:持久化/存储/独立模块/抽象/跨模块——防「轻档位局部实现塌方」(M1 架构护栏)。
|
|
2302
|
+
* UI 信号:UI 相关需求不得落 patch/tech(无设计/QA 的档位)——最低 lite。 */
|
|
2303
|
+
const ARCH_SIGNALS = [
|
|
2304
|
+
"持久化",
|
|
2305
|
+
"存储",
|
|
2306
|
+
"保存",
|
|
2307
|
+
"恢复",
|
|
2308
|
+
"存档",
|
|
2309
|
+
"独立模块",
|
|
2310
|
+
"抽象",
|
|
2311
|
+
"存储层",
|
|
2312
|
+
"localStorage",
|
|
2313
|
+
"sessionStorage",
|
|
2314
|
+
"IndexedDB",
|
|
2315
|
+
"跨模块",
|
|
2316
|
+
"数据层",
|
|
2317
|
+
"persistence",
|
|
2318
|
+
"storage",
|
|
2319
|
+
"database",
|
|
2320
|
+
"数据库",
|
|
2321
|
+
"standalone module",
|
|
2322
|
+
"abstraction"
|
|
2312
2323
|
];
|
|
2313
|
-
|
|
2324
|
+
const UI_SIGNALS = [
|
|
2325
|
+
"界面",
|
|
2326
|
+
"UI",
|
|
2327
|
+
"视觉",
|
|
2328
|
+
"页面",
|
|
2329
|
+
"按钮",
|
|
2330
|
+
"样式",
|
|
2331
|
+
"交互",
|
|
2332
|
+
"布局",
|
|
2333
|
+
"组件",
|
|
2334
|
+
"page",
|
|
2335
|
+
"button",
|
|
2336
|
+
"style",
|
|
2337
|
+
"layout",
|
|
2338
|
+
"component",
|
|
2339
|
+
"visual",
|
|
2340
|
+
"interaction"
|
|
2341
|
+
];
|
|
2342
|
+
/** 对原始需求做启发式分诊(兜底路径专用)。返回建议 mode + 判定理由 + 置信。
|
|
2343
|
+
* 只做确定性护栏(架构强升/UI 禁轻档/needDesign 升档)——不再逐词匹配五档信号:
|
|
2344
|
+
* 主路由是模型(TRIAGE_PROMPT 语义判断,天然双语),正则兜底在模型不可用时宁重勿漏(默认 full)。 */
|
|
2314
2345
|
function suggestMode(requirement, opts) {
|
|
2315
2346
|
const text = String(requirement || "");
|
|
2316
|
-
const
|
|
2317
|
-
for (const sig of SIGNALS) for (const w of sig.words) if (text.includes(w)) hits.push({
|
|
2318
|
-
mode: sig.mode,
|
|
2319
|
-
word: w
|
|
2320
|
-
});
|
|
2321
|
-
const rationale = hits.map((h) => `命中「${h.word}」→ ${h.mode}`);
|
|
2322
|
-
const countOf = (m) => hits.filter((h) => h.mode === m).length;
|
|
2347
|
+
const rationale = [];
|
|
2323
2348
|
let mode = "full";
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
else if (countOf("lite") > 0 && countOf("medium") === 0) mode = "lite";
|
|
2327
|
-
else if (countOf("medium") > 0 || opts && opts.needDesign) mode = "medium";
|
|
2328
|
-
else if (hits.length === 0) mode = "full";
|
|
2329
|
-
if ((mode === "patch" || mode === "tech") && opts && opts.needDesign) mode = "medium";
|
|
2330
|
-
if (mode === "patch" && (countOf("tech") > 0 || text.includes("新增") || text.includes("功能"))) mode = "lite";
|
|
2331
|
-
const architectureSignals = [
|
|
2332
|
-
"持久化",
|
|
2333
|
-
"localStorage",
|
|
2334
|
-
"本地存储",
|
|
2335
|
-
"存储",
|
|
2336
|
-
"保存",
|
|
2337
|
-
"恢复",
|
|
2338
|
-
"存档",
|
|
2339
|
-
"独立模块",
|
|
2340
|
-
"抽象",
|
|
2341
|
-
"存储层",
|
|
2342
|
-
"sessionStorage",
|
|
2343
|
-
"IndexedDB",
|
|
2344
|
-
"跨模块",
|
|
2345
|
-
"数据层"
|
|
2346
|
-
];
|
|
2347
|
-
if ((mode === "lite" || mode === "tech" || mode === "patch") && architectureSignals.some((w) => text.includes(w))) {
|
|
2349
|
+
const archHit = ARCH_SIGNALS.find((w) => text.includes(w));
|
|
2350
|
+
if (archHit) {
|
|
2348
2351
|
mode = "medium";
|
|
2349
|
-
rationale.push(`架构护栏:需求含「${
|
|
2352
|
+
rationale.push(`架构护栏:需求含「${archHit}」→ 强升 medium(需架构阶段产蓝图,防塌)`);
|
|
2350
2353
|
}
|
|
2351
|
-
const
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
+
const uiHit = !archHit ? UI_SIGNALS.find((w) => text.includes(w)) : void 0;
|
|
2355
|
+
if (uiHit) {
|
|
2356
|
+
mode = mode === "full" ? "lite" : mode;
|
|
2357
|
+
rationale.push(`UI 护栏:需求含「${uiHit}」→ 不低于 lite(UI 改动需 QA/验收)`);
|
|
2358
|
+
}
|
|
2359
|
+
if (opts && opts.needDesign && mode !== "medium") {
|
|
2360
|
+
mode = "medium";
|
|
2361
|
+
rationale.push("needDesign=true → 强升 medium(显式要求设计阶段)");
|
|
2362
|
+
}
|
|
2363
|
+
if (rationale.length === 0) rationale.push("无强护栏信号,默认 full(模型不可用时宁重勿漏)");
|
|
2364
|
+
const kind = mode === "medium" ? "标准功能(含UI)" : mode === "lite" ? "微功能" : "完整需求";
|
|
2365
|
+
const confidence = mode === "medium" && !opts?.needDesign ? "medium" : "low";
|
|
2354
2366
|
return {
|
|
2355
2367
|
mode,
|
|
2356
2368
|
kind,
|
|
@@ -2408,29 +2420,36 @@ function fallbackVerdict(requirement, opts) {
|
|
|
2408
2420
|
source: "fallback"
|
|
2409
2421
|
};
|
|
2410
2422
|
}
|
|
2411
|
-
/** 模型驱动分诊:spawn「分诊分析师」子代理思考一轮;子代理不可用/超时/解析失败 → 正则兜底。
|
|
2423
|
+
/** 模型驱动分诊:spawn「分诊分析师」子代理思考一轮;子代理不可用/超时/解析失败 → 正则兜底。
|
|
2424
|
+
* 解析失败先带纠错提示重试一次(实锤 run tf-mtfo8exi:模型输出「Let me output the JSON.」开场白
|
|
2425
|
+
* 后无 JSON——首轮输出预算被思考耗尽/模型停早;重试提示直接输出 JSON 对象本身)。 */
|
|
2412
2426
|
async function runTriage(requirement, opts, parent, signal) {
|
|
2413
2427
|
const subagents = runtime.subagents;
|
|
2414
2428
|
if (!subagents || typeof subagents.start !== "function") return fallbackVerdict(requirement, opts);
|
|
2415
2429
|
const pre = suggestMode(requirement, opts);
|
|
2416
|
-
let
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2430
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
2431
|
+
let run = null;
|
|
2432
|
+
try {
|
|
2433
|
+
const hint = attempt > 1 ? "Your previous reply contained only a preface (e.g. \"Let me output the JSON.\") with NO JSON object — that is a failed reply. Reply now with the JSON object ITSELF as the first and only content, starting with {." : "";
|
|
2434
|
+
run = await subagents.start(providerName(), {
|
|
2435
|
+
label: "需求分诊",
|
|
2436
|
+
prompt: [{
|
|
2437
|
+
type: "text",
|
|
2438
|
+
text: TRIAGE_PROMPT(requirement, opts, pre, hint)
|
|
2439
|
+
}],
|
|
2440
|
+
parent,
|
|
2441
|
+
signal
|
|
2442
|
+
});
|
|
2443
|
+
const result = await Promise.race([run.result, new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error("triage 超时(90s)")), 9e4))]);
|
|
2444
|
+
const parsed = parseVerdictText(extractText(result && result.output));
|
|
2445
|
+
if (parsed) return parsed;
|
|
2446
|
+
} catch (e) {
|
|
2447
|
+
if (attempt === 2) {}
|
|
2448
|
+
} finally {
|
|
2449
|
+
if (run && run.dispose) try {
|
|
2450
|
+
await run.dispose();
|
|
2451
|
+
} catch (e) {}
|
|
2452
|
+
}
|
|
2434
2453
|
}
|
|
2435
2454
|
return fallbackVerdict(requirement, opts);
|
|
2436
2455
|
}
|
|
@@ -2579,6 +2598,7 @@ function buildResumeProducts(journal) {
|
|
|
2579
2598
|
* 全部完成仍被中断(理论极端)→ 从产品验收继续。
|
|
2580
2599
|
*/
|
|
2581
2600
|
function interruptedPhaseOf(journal) {
|
|
2601
|
+
if (hasOpenBlockingBugs(journal)) return "QA 测试";
|
|
2582
2602
|
for (const phase of PHASE_ORDER) {
|
|
2583
2603
|
const phaseStages = (journal.stages || []).filter((s) => s.phase === phase);
|
|
2584
2604
|
if (phaseStages.length === 0) continue;
|
|
@@ -2586,6 +2606,24 @@ function interruptedPhaseOf(journal) {
|
|
|
2586
2606
|
}
|
|
2587
2607
|
return "产品验收";
|
|
2588
2608
|
}
|
|
2609
|
+
/** 开发任务定义(单一来源):架构蓝图自动拆 > 调用方显式 tasks > 整体开发兜底。
|
|
2610
|
+
* resume 补跑与正常执行共用(defByTitle 按 title 匹配失败子卡)。 */
|
|
2611
|
+
function buildDevTaskDefs(journal, tasks) {
|
|
2612
|
+
const blueprintTasks = journal.blueprint && Array.isArray(journal.blueprint.tasks) && journal.blueprint.tasks.length ? journal.blueprint.tasks.map((t) => ({
|
|
2613
|
+
title: t.title || "开发任务",
|
|
2614
|
+
files: Array.isArray(t.files) ? t.files : [],
|
|
2615
|
+
spec: t.spec || ""
|
|
2616
|
+
})) : [];
|
|
2617
|
+
return blueprintTasks.length ? blueprintTasks : tasks.length > 0 ? tasks.map((t) => ({
|
|
2618
|
+
title: t.title,
|
|
2619
|
+
spec: t.spec,
|
|
2620
|
+
files: []
|
|
2621
|
+
})) : [{
|
|
2622
|
+
title: "整体开发",
|
|
2623
|
+
spec: "按技术方案/需求实现全部改动",
|
|
2624
|
+
files: []
|
|
2625
|
+
}];
|
|
2626
|
+
}
|
|
2589
2627
|
/**
|
|
2590
2628
|
* 执行流水线。resume = null 全新运行;resume = { phase, products } 从断点续跑:
|
|
2591
2629
|
* phase 之前的阶段直接复用 products 产物(跳过执行),从 phase 阶段开始重跑。
|
|
@@ -2906,74 +2944,98 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2906
2944
|
}
|
|
2907
2945
|
}
|
|
2908
2946
|
let tech = null;
|
|
2909
|
-
if (
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
level: "phase",
|
|
2918
|
-
message: isHeavy ? "进入阶段:技术方案" : "进入阶段:架构蓝图"
|
|
2919
|
-
});
|
|
2920
|
-
const label = isHeavy ? "高级全栈工程师 · 技术方案" : "架构师 · 架构蓝图";
|
|
2921
|
-
const techR = await withRetry(journal, parent, label, "技术方案", isHeavy ? techPrompt(prd, design, scaffold, tasks, root, journal.id, state) : architectPrompt(prd, root, journal.id, state), signal);
|
|
2922
|
-
if (!techR.text) throw stageFailError(label, techR);
|
|
2923
|
-
tech = techR.text;
|
|
2924
|
-
timeline.tech = tech;
|
|
2925
|
-
mergeStageState("tech", tech);
|
|
2926
|
-
let bd = extractBlueprint(tech);
|
|
2927
|
-
if (!bd || bd.summary === void 0) try {
|
|
2928
|
-
const techFile = journal.runDocs && journal.workspacePath ? `${journal.workspacePath}/${journal.runDocs}/TECHNICAL.md` : null;
|
|
2929
|
-
if (techFile && existsSync(techFile)) bd = extractBlueprint(readFileSync(techFile, "utf8"));
|
|
2930
|
-
if (bd) journal.logs.push({
|
|
2947
|
+
if (enabled("tech")) {
|
|
2948
|
+
if (resumed("技术方案")) {
|
|
2949
|
+
tech = resume.products.tech;
|
|
2950
|
+
timeline.tech = tech;
|
|
2951
|
+
logSkip("技术方案");
|
|
2952
|
+
} else {
|
|
2953
|
+
const isHeavy = !options.lite && options.mode !== "tech" && options.mode !== "patch";
|
|
2954
|
+
journal.logs.push({
|
|
2931
2955
|
t: Date.now(),
|
|
2932
|
-
level: "
|
|
2933
|
-
message: "
|
|
2956
|
+
level: "phase",
|
|
2957
|
+
message: isHeavy ? "进入阶段:技术方案" : "进入阶段:架构蓝图"
|
|
2934
2958
|
});
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2959
|
+
const label = isHeavy ? "高级全栈工程师 · 技术方案" : "架构师 · 架构蓝图";
|
|
2960
|
+
const techR = await withRetry(journal, parent, label, "技术方案", isHeavy ? techPrompt(prd, design, scaffold, tasks, root, journal.id, state) : architectPrompt(prd, root, journal.id, state), signal);
|
|
2961
|
+
if (!techR.text) throw stageFailError(label, techR);
|
|
2962
|
+
tech = techR.text;
|
|
2963
|
+
timeline.tech = tech;
|
|
2964
|
+
mergeStageState("tech", tech);
|
|
2965
|
+
let bd = extractBlueprint(tech);
|
|
2966
|
+
if (!bd || bd.summary === void 0) try {
|
|
2967
|
+
const techFile = journal.runDocs && journal.workspacePath ? `${journal.workspacePath}/${journal.runDocs}/TECHNICAL.md` : null;
|
|
2968
|
+
if (techFile && existsSync(techFile)) bd = extractBlueprint(readFileSync(techFile, "utf8"));
|
|
2969
|
+
if (bd) journal.logs.push({
|
|
2970
|
+
t: Date.now(),
|
|
2971
|
+
level: "info",
|
|
2972
|
+
message: "蓝图从任务夹 TECHNICAL.md 提取(模型把蓝图写进了文档而非回复输出)"
|
|
2973
|
+
});
|
|
2974
|
+
} catch (e) {}
|
|
2975
|
+
if (bd && bd.summary !== void 0) try {
|
|
2976
|
+
state.__runCtx = state.__runCtx || {};
|
|
2977
|
+
state.__runCtx.blueprint = bd.render;
|
|
2978
|
+
journal.blueprint = {
|
|
2979
|
+
modules: bd.modules,
|
|
2980
|
+
tasks: bd.tasks
|
|
2981
|
+
};
|
|
2982
|
+
} catch (e) {}
|
|
2983
|
+
else if (/<!-- blueprint -->/.test(String(tech))) journal.logs.push({
|
|
2984
|
+
t: Date.now(),
|
|
2985
|
+
level: "warn",
|
|
2986
|
+
message: "技术方案蓝图块解析失败(JSON 畸形),开发任务将回退整体开发——TECHNICAL.md 蓝图块需人工检查"
|
|
2987
|
+
});
|
|
2988
|
+
noteTaskStageUsage(journal);
|
|
2989
|
+
if (journal.cancelled) return;
|
|
2990
|
+
}
|
|
2951
2991
|
}
|
|
2952
2992
|
let devResults = null;
|
|
2953
2993
|
if (resumed("开发")) {
|
|
2954
2994
|
devResults = resume.products.dev || [];
|
|
2955
|
-
|
|
2956
|
-
|
|
2995
|
+
const failedSubs = (storeFor(scopeKey).tasks || []).filter((t) => t.reqId === journal.reqId && t.parentId === journal.taskId && (t.status === "failed" || t.failed === true));
|
|
2996
|
+
if (failedSubs.length > 0) {
|
|
2997
|
+
const defs = buildDevTaskDefs(journal, tasks);
|
|
2998
|
+
const defByTitle = new Map(defs.map((d) => [d.title, d]));
|
|
2999
|
+
const rerunDefs = failedSubs.map((s) => {
|
|
3000
|
+
const key = String(s.title || "").replace(/^开发 · /, "");
|
|
3001
|
+
return defByTitle.get(key) || {
|
|
3002
|
+
title: key,
|
|
3003
|
+
spec: s.spec || "按技术方案实现该任务改动",
|
|
3004
|
+
files: []
|
|
3005
|
+
};
|
|
3006
|
+
}).filter(Boolean);
|
|
3007
|
+
const reused = devResults.filter((r) => r && !rerunDefs.some((d) => d.title === r.title));
|
|
3008
|
+
journal.logs.push({
|
|
3009
|
+
t: Date.now(),
|
|
3010
|
+
level: "warn",
|
|
3011
|
+
message: `断点续跑开发:复用 ${reused.length} 个已完成任务,补跑 ${rerunDefs.length} 个失败任务`
|
|
3012
|
+
});
|
|
3013
|
+
const rerun = await runPool(rerunDefs, maxConcurrency, async (task) => {
|
|
3014
|
+
const devR = await withRetry(journal, parent, `开发 · ${task.title}(补跑)`, "开发", devPrompt(task, tech, prd, root, journal.id, state), signal);
|
|
3015
|
+
const ok = !!devR.text;
|
|
3016
|
+
return {
|
|
3017
|
+
title: task.title,
|
|
3018
|
+
failed: !ok,
|
|
3019
|
+
output: devR.text || "开发失败(Agent 未产出结果)"
|
|
3020
|
+
};
|
|
3021
|
+
});
|
|
3022
|
+
for (const t of rerun) {
|
|
3023
|
+
const sub = failedSubs.find((s) => String(s.title || "").replace(/^开发 · /, "") === t.title);
|
|
3024
|
+
if (sub) completeSubtask(journal, sub.id, t.failed, t.output ? snippet(t.output, 1e3) : null, null);
|
|
3025
|
+
}
|
|
3026
|
+
devResults = [...reused, ...rerun];
|
|
3027
|
+
timeline.dev = devResults;
|
|
3028
|
+
} else {
|
|
3029
|
+
timeline.dev = devResults;
|
|
3030
|
+
logSkip("开发");
|
|
3031
|
+
}
|
|
2957
3032
|
} else {
|
|
2958
3033
|
journal.logs.push({
|
|
2959
3034
|
t: Date.now(),
|
|
2960
3035
|
level: "phase",
|
|
2961
3036
|
message: "进入阶段:开发"
|
|
2962
3037
|
});
|
|
2963
|
-
const
|
|
2964
|
-
title: t.title || "开发任务",
|
|
2965
|
-
files: Array.isArray(t.files) ? t.files : [],
|
|
2966
|
-
spec: t.spec || ""
|
|
2967
|
-
})) : [];
|
|
2968
|
-
const devTaskDefs = blueprintTasks.length ? blueprintTasks : tasks.length > 0 ? tasks.map((t) => ({
|
|
2969
|
-
title: t.title,
|
|
2970
|
-
spec: t.spec,
|
|
2971
|
-
files: []
|
|
2972
|
-
})) : [{
|
|
2973
|
-
title: "整体开发",
|
|
2974
|
-
spec: "按技术方案/需求实现全部改动",
|
|
2975
|
-
files: []
|
|
2976
|
-
}];
|
|
3038
|
+
const devTaskDefs = buildDevTaskDefs(journal, tasks);
|
|
2977
3039
|
const mergedDefs = [];
|
|
2978
3040
|
for (const t of devTaskDefs) {
|
|
2979
3041
|
const hit = t.files && t.files.length ? mergedDefs.find((m) => m.files.some((f) => t.files.includes(f))) : void 0;
|
|
@@ -2990,7 +3052,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
2990
3052
|
journal.logs.push({
|
|
2991
3053
|
t: Date.now(),
|
|
2992
3054
|
level: "info",
|
|
2993
|
-
message: `开发阶段开始,任务数:${mergedDefs.length}(并发 ${maxConcurrency}${
|
|
3055
|
+
message: `开发阶段开始,任务数:${mergedDefs.length}(并发 ${maxConcurrency}${journal.blueprint && Array.isArray(journal.blueprint.tasks) && journal.blueprint.tasks.length ? ",源自架构蓝图自动拆解" : ""})`
|
|
2994
3056
|
});
|
|
2995
3057
|
advanceTask(journal, "running", null, "开发开始(待办 → 开发中)", { by: "dev" });
|
|
2996
3058
|
const subCards = mergedDefs.map((dt) => createSubtask(journal, dt.title, dt.spec));
|
|
@@ -3034,17 +3096,10 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3034
3096
|
}
|
|
3035
3097
|
journal.logs.push({
|
|
3036
3098
|
t: Date.now(),
|
|
3037
|
-
level: "
|
|
3038
|
-
message:
|
|
3099
|
+
level: "error",
|
|
3100
|
+
message: `开发失败任务数:${failedCount}/${devResults.length}(提测门禁拦截):停止流水线,需人工介入——处理后可 teamflow_resume 从开发阶段补跑失败任务(已完成任务复用)`
|
|
3039
3101
|
});
|
|
3040
|
-
|
|
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
|
-
}
|
|
3102
|
+
throw new Error(`开发失败任务 ${failedCount}/${devResults.length}:提测门禁拦截,不进 QA(已知缺口,QA 检查轮必然重复报告)`);
|
|
3048
3103
|
} else {
|
|
3049
3104
|
advanceTask(journal, "testable", null, "开发完成待测试(开发中 → 待测试)", { by: "dev" });
|
|
3050
3105
|
journal.logs.push({
|
|
@@ -3065,7 +3120,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3065
3120
|
message: "当前档位阶段集不含独立 QA:跳过(单点修复,开发自测兜底)"
|
|
3066
3121
|
});
|
|
3067
3122
|
qa = "(独立 QA 跳过:当前档位由开发自测兜底)";
|
|
3068
|
-
} else if (resumed("QA 测试")) {
|
|
3123
|
+
} else if (resumed("QA 测试") && !hasOpenBlockingBugs(journal)) {
|
|
3069
3124
|
qa = resume.products.qa;
|
|
3070
3125
|
timeline.qa = qa;
|
|
3071
3126
|
logSkip("QA 测试");
|
|
@@ -3159,7 +3214,22 @@ async function executePipeline(journal, parent, requirement, options, signal, re
|
|
|
3159
3214
|
if (journal.cancelled) return;
|
|
3160
3215
|
}
|
|
3161
3216
|
persistJournal(journal);
|
|
3162
|
-
if (!
|
|
3217
|
+
if (!enabled("acceptance")) {
|
|
3218
|
+
journal.logs.push({
|
|
3219
|
+
t: Date.now(),
|
|
3220
|
+
level: "info",
|
|
3221
|
+
message: "patch 档交付完成:单 agent 直改 + 自测即收口(无独立 QA/验收)"
|
|
3222
|
+
});
|
|
3223
|
+
advanceTask(journal, "accepted", null, "patch 直改交付(自测通过)", { by: "dev" });
|
|
3224
|
+
const store = storeFor(scopeKey);
|
|
3225
|
+
const req = store.find("req", journal.reqId);
|
|
3226
|
+
if (req && req.status !== "accepted") {
|
|
3227
|
+
store.pushEvent(req, req.status, "accepted", "patch 交付通过(自测)");
|
|
3228
|
+
req.status = "accepted";
|
|
3229
|
+
store.persist();
|
|
3230
|
+
}
|
|
3231
|
+
journal.status = "completed";
|
|
3232
|
+
} else if (!qaBlocked) {
|
|
3163
3233
|
journal.logs.push({
|
|
3164
3234
|
t: Date.now(),
|
|
3165
3235
|
level: "phase",
|
|
@@ -3451,7 +3521,7 @@ function resumeRun(runId, sessionId) {
|
|
|
3451
3521
|
level: "warn",
|
|
3452
3522
|
message: `断点续跑:从「${resumePhase}」继续(已完成阶段复用产物)`
|
|
3453
3523
|
});
|
|
3454
|
-
j.stages = (j.stages || []).filter((s) => s.status
|
|
3524
|
+
j.stages = (j.stages || []).filter((s) => s.status !== "running" && s.status !== "pending");
|
|
3455
3525
|
runs.set(id, j);
|
|
3456
3526
|
persistJournal(j);
|
|
3457
3527
|
executePipeline(j, agent, j.requirement, j.options, void 0, {
|
|
@@ -3553,7 +3623,7 @@ function registerTools(ctx) {
|
|
|
3553
3623
|
};
|
|
3554
3624
|
T({
|
|
3555
3625
|
name: "teamflow_start",
|
|
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
|
|
3626
|
+
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.",
|
|
3557
3627
|
parameters: {
|
|
3558
3628
|
requirement: {
|
|
3559
3629
|
type: "string",
|
|
@@ -3590,7 +3660,7 @@ function registerTools(ctx) {
|
|
|
3590
3660
|
},
|
|
3591
3661
|
branchPolicy: {
|
|
3592
3662
|
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."
|
|
3663
|
+
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."
|
|
3594
3664
|
},
|
|
3595
3665
|
branchName: {
|
|
3596
3666
|
type: "string",
|
|
@@ -3642,7 +3712,7 @@ function registerTools(ctx) {
|
|
|
3642
3712
|
const opts = Array.isArray(value.options) ? value.options.map((o, i) => `${i + 1}. ${o.label}`).join("\n") : "";
|
|
3643
3713
|
return [{
|
|
3644
3714
|
type: "text",
|
|
3645
|
-
text: `【分支决策】${value.question}\n${opts}\n
|
|
3715
|
+
text: `【分支决策】${value.question}\n${opts}\n(也可自定义输入)——请询问用户选择,确认后把所选选项的 value 作为 branchPolicy 重新调用 teamflow_start(如 'new'/'keep';脏工作区选项可拆为 branchPolicy + preAction 组合),自定义分支名则传 branchName。`
|
|
3646
3716
|
}];
|
|
3647
3717
|
}
|
|
3648
3718
|
if (value && value.status === "needs-confirmation") return [{
|
|
@@ -3692,7 +3762,8 @@ function registerTools(ctx) {
|
|
|
3692
3762
|
preAction: args.preAction === "stash" || args.preAction === "commit" ? args.preAction : null,
|
|
3693
3763
|
commitMessage: typeof args.commitMessage === "string" && args.commitMessage.trim() ? args.commitMessage.trim() : null
|
|
3694
3764
|
};
|
|
3695
|
-
|
|
3765
|
+
const branchConfirmed = args.branchPolicy === "new";
|
|
3766
|
+
if (options.branchPolicy === "auto" && !branchConfirmed && !options.branchName && !options.preAction && exec && exec.agent) {
|
|
3696
3767
|
const sc = workspaceScopeOf(exec.agent);
|
|
3697
3768
|
if (sc.path) try {
|
|
3698
3769
|
const s = runSanityCheck(sc.path);
|
|
@@ -3706,7 +3777,7 @@ function registerTools(ctx) {
|
|
|
3706
3777
|
question = `工作区 ${sc.path} 当前在 main 分支(工作区干净)。流水线默认在特性分支上开发,请选择:`;
|
|
3707
3778
|
optionsList = [{
|
|
3708
3779
|
label: "基于 main 新建分支开发(推荐,分支名取需求 slug,可自定义)",
|
|
3709
|
-
value: "
|
|
3780
|
+
value: "new"
|
|
3710
3781
|
}, {
|
|
3711
3782
|
label: "直接在 main 上开发",
|
|
3712
3783
|
value: "keep"
|
|
@@ -3734,7 +3805,7 @@ function registerTools(ctx) {
|
|
|
3734
3805
|
value: "keep"
|
|
3735
3806
|
}, {
|
|
3736
3807
|
label: "基于当前分支再新建子分支开发",
|
|
3737
|
-
value: "
|
|
3808
|
+
value: "new"
|
|
3738
3809
|
}];
|
|
3739
3810
|
} else {
|
|
3740
3811
|
question = `工作区 ${sc.path} 当前在特性分支 ${s.branch},且有 ${dirtyN} 处未提交改动。请选择启动方式:`;
|
|
@@ -3761,7 +3832,7 @@ function registerTools(ctx) {
|
|
|
3761
3832
|
status: "needs-decision",
|
|
3762
3833
|
question,
|
|
3763
3834
|
options: optionsList,
|
|
3764
|
-
note: "选项之外可自定义输入(如指定分支名)。确认选择后,请以 teamflow_start 的 branchPolicy / branchName
|
|
3835
|
+
note: "选项之外可自定义输入(如指定分支名)。确认选择后,请以 teamflow_start 的 branchPolicy(回传所选选项 value,如 new/keep)与 branchName/preAction/commitMessage 参数重新调用本工具。"
|
|
3765
3836
|
};
|
|
3766
3837
|
}
|
|
3767
3838
|
} catch (e) {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-teamflow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "TeamFlow 团队研发流水线(TypeScript):需求→PRD→设计→架构→技术方案→并行开发→QA→验收;backlog 持久化 + 断点续跑 + 完成汇报 + 防假交付;Web 团队工作台(conversation.view tab + 拖拽看板)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/host.mjs",
|