dsh-plugin-teamflow 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  > 本插件首次公开发布版本为 **v0.1.0**;发布前的内部迭代(v0.3~v0.13)记录于 `AGENTS.md` §5,对外统一归到 v0.1.0。
4
4
 
5
+ ## [0.1.6] - 2026-09-07
6
+
7
+ ### 新增
8
+ - **评测层 L1+L2(prompt/注入改动收益评测)**:每次改 prompt/注入后可直接验证收益——L1 行为级契约测试(直接调用 prompt 工厂断言产出锚点,29 条契约分 HOST-ENFORCED/policy/structural 三级,改 prompt 一眼看出断了哪条、丢哪级保障)+ L2 回放语料一致性门禁(冻结真实形状的 QA-REPORT/ACCEPTANCE/dev 回复/蓝图产物,喂宿主真实解析器做 golden corpus 回归,零 LLM 成本)。语料只增不改,防评测过拟合
9
+ - **dev/qaFix 验证证据块**:开发回复末尾强制「[Verification evidence]」块(命令+退出码+断言计数+失败行引用,或显式 N/A),host 提取存证、阶段详情可见、可与命令日志交叉核对——开发阶段从「单方宣称全绿」升级为「可审计的具体自述」
10
+ - **代码级英文化(为 UI i18n 铺路)**:代码判断/命名全英文(阶段键 prd/design/scaffold/tech/dev/qa/acceptance、任务结构化键 taskKey),中文只留展示层;存量数据提供迁移脚本
11
+
12
+ ### 改进
13
+ - **resume 断点续跑状态机化**:两级状态(大阶段 + 子图任务)聚合判定——有 done 尝试的任务即成功(历史失败不算失败),resume 只补跑「聚合后未成功」任务、复用已完成产物;不再读 backlog 子卡(残留失败卡不再污染判定);dev 部分成功时起点精确回开发补跑未完成,全 done 时正确落在 QA/验收
14
+ - **输出单轨制**:QA/验收的完整报告只写在任务夹文件(QA-REPORT.md/ACCEPTANCE.md),子代理回复仅摘要+路径——host 直接导入文件(缺陷表/核对表),杜绝「回复与文件不一致」的双轨问题
15
+ - **验收结论契约强度**:验收报告结论行必须为字面量模板(✅/⚠️/❌/📝 四档)且为文件最后一行;无结论行/空结论行 → 需人工确认(不再默认通过,防质量门禁漏报)
16
+ - **prompt 约束分级**:区分 [HOST-ENFORCED](host 真实强制,如单轨产物/结论行)与 [policy](自律 + 轻提醒),prompt 内不再自称 hard constraint
17
+ - **重试诊断包**:重试时把上次失败详情(outcome/护栏原因/拒绝词命中点/产出尾部)附进 prompt——盲试 → 带因重试;stalled(挂死/空转)不再自动重试
18
+
19
+ ### 修复
20
+ - **token 计量适配宿主 session v2**:宿主新版会话已无 `events` 属性,计量读不到 → 流水线卡与图卡无「TOKEN · 官方口径」。修复:多源回退(snapshotEvents/ownEvents)+ usage 双路径(含 stream 内嵌),新 run 计量恢复
21
+ - **护栏挂死误杀**:QA 子代理正常干活却被判「10 分钟无事件」(宿主事件视图失明)——事件读取多源回退 + agent 活动守卫(非 idle 且已动手即不中止)
22
+ - **任务卡 token 统计**:子卡 usage 按 stage 引用直写(并发下不再错位/超计)
23
+ - **存量迁移脚本 taskKey 误判**:合法中文括号结尾的任务键不再被当作历史残留(幂等无损但计数虚高)
24
+ - **验收空结论行/裸否定词漏报**:四档词白名单校验覆盖 accepted 分支
25
+
26
+ ### 其他
27
+ - AGENTS.md 注入面瘦身 64%;dsh 0.1.3-alpha.1 兼容性核对(依赖包名更新)
28
+
5
29
  ## [0.1.5] - 2026-09-01
6
30
 
7
31
  ### 新增
package/README.md CHANGED
@@ -119,6 +119,16 @@ dsh-plugin-teamflow/
119
119
  - Node.js ≥ 22.18;
120
120
  - 依赖宿主提供的 `@deepseek-ai/dsh-*` 与 `react`(peerDependencies,宿主注入,无需单独安装)。
121
121
 
122
+ opencode### 版本锚定(dsh 宿主兼容性)
123
+
124
+ 本插件开发与验证基于 **dsh git master(2026-09-04,≈ 0.1.3-alpha.1 发布线)**;npm 侧最新发布线 `0.1.2-rc.1`(next tag;`latest` 标签滞后为 0.0.1-rc.1,勿以 latest 判断)。`peerDependencies` 保持 `*`(宿主注入,宽松兼容)。
125
+
126
+ 2026-09-04 兼容性核对(dsh 0.1.3-alpha.1):session 持久化 v2(write-lease/JSONL 快照/版本化导出)、attachment/file-upload 收口、Windows 子进程隐藏、workspace 全限定路径硬化——**全部兼容,插件无需调整**。
127
+
128
+ 插件侧契约约束(dsh 升级后若行为异常先核对本段;锚定版本变更会在此更新):
129
+ - 插件注入的 session 事件(`tool-workflow/agent-start`、`user/message` 带 `source.kind='plugin'`)均在 released 词表内;**未来新增自定义事件类型须带 `ignorable: true`**,已知类型载荷不加 released 词表外键;
130
+ - `@deepseek-ai/dsh-client-modules` 自 0.1.2-rc.1 起替代 `@deepseek-ai/dsh-client-runtime`(后者已从 monorepo 移除)。
131
+
122
132
  ## 安装(对使用者)
123
133
 
124
134
  ```bash
package/lib/client.js CHANGED
@@ -308,15 +308,37 @@ window.__ModuleLoader__.load({
308
308
  interrupted: T.warn,
309
309
  superseded: T.text2
310
310
  };
311
+ /** 阶段英文键 → 图标/中文展示名(2026-09-06 英文化:journal.phase 为英文键,展示名统一走映射——未来 i18n 换表即换语言)。 */
311
312
  const PHASE_ICON = {
312
- "PRD 产品需求": "📋",
313
- "UI/UX 设计": "🎨",
314
- "架构规划": "🏗️",
315
- "技术方案": "📐",
316
- 开发: "💻",
317
- "QA 测试": "🧪",
318
- "产品验收": "✅"
313
+ prd: "📋",
314
+ design: "🎨",
315
+ scaffold: "🏗️",
316
+ tech: "📐",
317
+ dev: "💻",
318
+ qa: "🧪",
319
+ acceptance: "✅"
319
320
  };
321
+ const PHASE_NAME = {
322
+ prd: "PRD 产品需求",
323
+ design: "UI/UX 设计",
324
+ scaffold: "架构规划",
325
+ tech: "技术方案",
326
+ dev: "开发",
327
+ qa: "QA 测试",
328
+ acceptance: "产品验收"
329
+ };
330
+ const phaseNameOf = (p) => PHASE_NAME[p] || p || "—";
331
+ const phaseIconOf = (p) => PHASE_ICON[p] || "⚙️";
332
+ /** phase 归一:英文键直通;存量中文映射(防御性——新数据全英文)。 */
333
+ const phaseKeyOf = (p) => ({
334
+ "PRD 产品需求": "prd",
335
+ "UI/UX 设计": "design",
336
+ "架构规划": "scaffold",
337
+ "技术方案": "tech",
338
+ "开发": "dev",
339
+ "QA 测试": "qa",
340
+ "产品验收": "acceptance"
341
+ })[p] || String(p || "");
320
342
  const RUN_STATUS_TEXT = {
321
343
  pending: "等待中",
322
344
  running: "进行中",
@@ -642,7 +664,20 @@ window.__ModuleLoader__.load({
642
664
  textOverflow: "ellipsis",
643
665
  whiteSpace: "nowrap"
644
666
  }
645
- }, s.label), chip(stText(s.status), color, { dot: true }), h("span", { style: {
667
+ }, s.__taskKey || s.label), s.attempts && s.attempts.length > 1 ? h("span", {
668
+ title: `重试 ${s.attempts.length - 1} 次(共 ${s.attempts.length} 次尝试)`,
669
+ style: {
670
+ fontFamily: MONO,
671
+ fontSize: 10,
672
+ fontWeight: 800,
673
+ color: T.warn,
674
+ background: `color-mix(in srgb, ${T.warn} 14%, transparent)`,
675
+ borderRadius: 999,
676
+ padding: "0 6px",
677
+ lineHeight: "15px",
678
+ flex: "0 0 auto"
679
+ }
680
+ }, `↻${s.attempts.length - 1}`) : null, chip(stText(s.status), color, { dot: true }), h("span", { style: {
646
681
  color: T.text2,
647
682
  fontSize: 11,
648
683
  opacity: .5
@@ -724,12 +759,12 @@ window.__ModuleLoader__.load({
724
759
  fontWeight: 700,
725
760
  background: `linear-gradient(90deg, color-mix(in srgb, ${g.headColor} 13%, transparent), color-mix(in srgb, ${g.headColor} 5%, transparent))`,
726
761
  border: `1px solid color-mix(in srgb, ${g.headColor} 30%, transparent)`
727
- } }, h("span", { style: { fontSize: 13.5 } }, PHASE_ICON[g.phase] || "⚙️"), h("span", { style: {
762
+ } }, h("span", { style: { fontSize: 13.5 } }, phaseIconOf(g.phase)), h("span", { style: {
728
763
  flex: 1,
729
764
  overflow: "hidden",
730
765
  textOverflow: "ellipsis",
731
766
  whiteSpace: "nowrap"
732
- } }, g.phase), g.stages.length > 1 ? h("span", { style: {
767
+ } }, phaseNameOf(g.phase)), g.stages.length > 1 ? h("span", { style: {
733
768
  fontFamily: MONO,
734
769
  fontSize: 10,
735
770
  fontWeight: 800,
@@ -752,26 +787,32 @@ window.__ModuleLoader__.load({
752
787
  paddingTop: 10
753
788
  } }, g.stages.map((s) => FlowStageCard(s, s.seq, onOpen))));
754
789
  }
755
- /** 阶段详情抽屉(卡片点击打开;浮于画布右侧,不参与拖动/缩放)。 */
790
+ /** 阶段详情抽屉(卡片点击打开;浮于画布右侧,不参与拖动/缩放)。
791
+ * 2026-09-06 状态机化:同任务多次尝试 → 顶部尝试时间线 + 选中展开(默认最新);
792
+ * 单次尝试保持现状(不渲染时间线)。 */
756
793
  function StageDetailDrawer({ det, onClose, sessionId, sessions }) {
794
+ const [sel, setSel] = react.default.useState(null);
757
795
  const st = det.stage;
758
796
  const d = det.data;
797
+ const attempts = d && d.attempts && d.attempts.length > 1 ? d.attempts : null;
798
+ const selIdx = attempts ? sel === null ? attempts.length - 1 : sel : null;
799
+ const cur = attempts ? attempts[selIdx] || d : d;
759
800
  const color = st && st.status ? stColor(st.status) : T.text2;
760
801
  const ownerSession = d && d.ownerSession ? String(d.ownerSession) : null;
761
802
  const mySession = sessionId ? String(sessionId) : null;
762
803
  const crossSession = !!ownerSession && !!mySession && ownerSession !== mySession;
763
- const hasChild = !!(!crossSession && d && d.childId && sessions && typeof sessions.openSubagent === "function");
804
+ const hasChild = !!(!crossSession && cur && cur.childId && sessions && typeof sessions.openSubagent === "function");
764
805
  const openChild = () => {
765
806
  if (crossSession || !hasChild) return;
766
807
  try {
767
808
  sessions.openSubagent({
768
809
  parentSessionId: ownerSession || sessionId,
769
- childSessionId: d.childId,
810
+ childSessionId: cur.childId,
770
811
  mode: "one-shot"
771
812
  });
772
813
  } catch (e) {}
773
814
  };
774
- const outText = d && d.output ? d.output : d && d.summary ? `(该 run 未保存完整正文,展示摘要)\n\n${d.summary}` : det.err ? `⚠ 加载失败:${det.err}` : det.loading ? "加载中…" : "(无产物正文)";
815
+ const outText = cur && cur.output ? cur.output : cur && cur.summary ? `(该 run 未保存完整正文,展示摘要)\n\n${cur.summary}` : det.err ? `⚠ 加载失败:${det.err}` : det.loading ? "加载中…" : "(无产物正文)";
775
816
  const closeBtn = {
776
817
  font: "inherit",
777
818
  width: 26,
@@ -820,7 +861,7 @@ window.__ModuleLoader__.load({
820
861
  padding: "12px 14px",
821
862
  borderBottom: `1px solid ${T.border}`,
822
863
  background: `linear-gradient(135deg, color-mix(in srgb, ${color} 14%, transparent), transparent 62%)`
823
- } }, h("span", { style: { fontSize: 17 } }, PHASE_ICON[st && st.phase] || "⚙️"), h("div", { style: {
864
+ } }, h("span", { style: { fontSize: 17 } }, phaseIconOf(st && st.phase)), h("div", { style: {
824
865
  flex: 1,
825
866
  minWidth: 0
826
867
  } }, h("div", {
@@ -864,7 +905,66 @@ window.__ModuleLoader__.load({
864
905
  fontFamily: MONO,
865
906
  color: T.text,
866
907
  lineHeight: 1.65
867
- } }, usageDetail(d || st || {}))), h("div", { style: {
908
+ } }, usageDetail(cur || st || {}))), attempts ? h("div", { style: {
909
+ display: "flex",
910
+ flexDirection: "column",
911
+ gap: 5
912
+ } }, h("span", { style: {
913
+ fontSize: 10.5,
914
+ fontWeight: 700,
915
+ color: T.text2,
916
+ letterSpacing: .3
917
+ } }, `↻ 尝试历史(${attempts.length} 次 · 点击查看该次详情)`), h("div", { style: {
918
+ display: "flex",
919
+ flexDirection: "column",
920
+ gap: 4
921
+ } }, attempts.map((a, idx) => {
922
+ const aColor = a.status === "done" ? T.success : a.status === "failed" ? T.error : T.warn;
923
+ const sel = idx === selIdx;
924
+ return h("div", {
925
+ key: a.seq,
926
+ onClick: () => setSel(idx),
927
+ title: (a.summary || a.outcome || "").slice(0, 200),
928
+ style: {
929
+ cursor: "pointer",
930
+ borderRadius: 9,
931
+ padding: "6px 9px",
932
+ display: "flex",
933
+ gap: 7,
934
+ alignItems: "center",
935
+ border: `1px solid ${sel ? color : T.border}`,
936
+ background: sel ? `color-mix(in srgb, ${color} 10%, transparent)` : T.layer1
937
+ }
938
+ }, h("span", { style: {
939
+ fontFamily: MONO,
940
+ fontSize: 10.5,
941
+ color: T.text2,
942
+ flex: "0 0 52px"
943
+ } }, `#${a.seq}`), h("span", { style: {
944
+ fontSize: 11,
945
+ color: aColor,
946
+ flex: "0 0 64px",
947
+ fontWeight: 700
948
+ } }, a.status === "done" ? "✅ 成功" : a.status === "failed" ? `❌ ${a.outcome || "失败"}` : "⏳ 进行中"), h("span", { style: {
949
+ flex: 1,
950
+ minWidth: 0,
951
+ fontSize: 10.5,
952
+ color: T.text2,
953
+ overflow: "hidden",
954
+ textOverflow: "ellipsis",
955
+ whiteSpace: "nowrap"
956
+ } }, (a.summary || a.outcome || "(无摘要)").slice(0, 80)), h("span", { style: {
957
+ fontFamily: MONO,
958
+ fontSize: 10,
959
+ color: T.text2,
960
+ flex: "0 0 auto"
961
+ } }, a.startedAt ? fmtDur(a.startedAt, a.endedAt) : "—"), h("span", { style: {
962
+ fontFamily: MONO,
963
+ fontSize: 10,
964
+ color: T.text2,
965
+ flex: "0 0 auto"
966
+ } }, a.usage ? fmtTokens(totalTokens(a.usage)) : ""));
967
+ }))) : null, h("div", { style: {
868
968
  display: "flex",
869
969
  flexDirection: "column",
870
970
  gap: 5
@@ -898,7 +998,37 @@ window.__ModuleLoader__.load({
898
998
  color: T.text2,
899
999
  textAlign: "center",
900
1000
  lineHeight: 1.55
901
- } }, "跳转成功后,请切「对话」tab 查看该子代理的完整会话轨迹") : null), h("div", { style: {
1001
+ } }, "跳转成功后,请切「对话」tab 查看该子代理的完整会话轨迹") : null), st && phaseKeyOf(st.phase) === "dev" ? h("div", { style: {
1002
+ display: "flex",
1003
+ flexDirection: "column",
1004
+ gap: 5
1005
+ } }, h("span", { style: {
1006
+ fontSize: 10.5,
1007
+ fontWeight: 700,
1008
+ color: T.text2,
1009
+ letterSpacing: .3
1010
+ } }, "🔬 验证证据"), cur && cur.verifyEvidence ? h("div", { style: {
1011
+ whiteSpace: "pre-wrap",
1012
+ wordBreak: "break-word",
1013
+ fontSize: 11.5,
1014
+ lineHeight: 1.62,
1015
+ color: T.text,
1016
+ background: `color-mix(in srgb, ${T.layer2} 55%, transparent)`,
1017
+ border: `1px solid ${T.border}`,
1018
+ borderRadius: 10,
1019
+ padding: "10px 12px",
1020
+ maxHeight: 180,
1021
+ overflowY: "auto",
1022
+ fontFamily: MONO
1023
+ } }, cur.verifyEvidence) : h("div", { style: {
1024
+ fontSize: 11,
1025
+ color: T.warn,
1026
+ background: `color-mix(in srgb, ${T.warn} 8%, transparent)`,
1027
+ border: `1px dashed color-mix(in srgb, ${T.warn} 45%, transparent)`,
1028
+ borderRadius: 10,
1029
+ padding: "8px 12px",
1030
+ lineHeight: 1.55
1031
+ } }, "(缺失——契约未兑现,host 已记警告;可与 logs/teamflow/<runId>/ 命令输出日志对照)")) : null, h("div", { style: {
902
1032
  display: "flex",
903
1033
  flexDirection: "column",
904
1034
  gap: 5
@@ -932,6 +1062,7 @@ window.__ModuleLoader__.load({
932
1062
  marginBottom: 8
933
1063
  } }, "🏭"), "暂无运行中的流水线——让模型调用 teamflow_start,或在上方输入需求");
934
1064
  const groups = [];
1065
+ const taskKeyOf = (s) => String(s.taskKey || String(s.label || "").replace(/^开发 · /, "").replace(/((?:第 \d+ 次重试|补跑))$/, "").trim());
935
1066
  for (const st of active.stages || []) {
936
1067
  let g = groups.length ? groups[groups.length - 1] : null;
937
1068
  if (!g || g.phase !== st.phase) {
@@ -941,7 +1072,31 @@ window.__ModuleLoader__.load({
941
1072
  };
942
1073
  groups.push(g);
943
1074
  }
944
- g.stages.push(st);
1075
+ const key = taskKeyOf(st);
1076
+ const prev = g.stages.find((x) => x.__taskKey === key);
1077
+ if (prev) {
1078
+ prev.attempts = prev.attempts || [prev];
1079
+ prev.attempts.push(st);
1080
+ if ((st.seq || 0) > (prev.seq || 0)) {
1081
+ for (const f of [
1082
+ "status",
1083
+ "outcome",
1084
+ "usage",
1085
+ "output",
1086
+ "summary",
1087
+ "childId",
1088
+ "startedAt",
1089
+ "endedAt",
1090
+ "verifyEvidence"
1091
+ ]) prev[f] = st[f];
1092
+ prev.seq = st.seq;
1093
+ prev.label = st.label;
1094
+ }
1095
+ } else g.stages.push({
1096
+ ...st,
1097
+ __taskKey: key,
1098
+ attempts: [st]
1099
+ });
945
1100
  }
946
1101
  const wrapRef = react.default.useRef(null);
947
1102
  const [vw, setVw] = react.default.useState(900);