dsh-plugin-teamflow 0.1.6 → 0.1.7

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,25 @@
2
2
 
3
3
  > 本插件首次公开发布版本为 **v0.1.0**;发布前的内部迭代(v0.3~v0.13)记录于 `AGENTS.md` §5,对外统一归到 v0.1.0。
4
4
 
5
+ ## [0.1.7] - 2026-09-11
6
+
7
+ ### 新增
8
+ - **工作台产物一键预览**:任务卡详情里的「任务夹」现在按真实存在的产物列按钮(PRD / DESIGN / TECHNICAL / QA-REPORT / ACCEPTANCE / meta),点一下即在 DSH **右侧栏**打开预览(Markdown 由官方文档预览器接管)。地址由 host 用官方 `fileAddressFor` 生成(`dsh-resource://file/session/<id>/<相对路径>`)——客户端不拼地址、也不引宿主包进 client bundle;只列真实存在的文件(不出死按钮);右侧栏服务缺失时静默降级
9
+ - **产物交付(`present`)**:prd/tech/qa/acceptance 四个阶段被要求把任务夹产物交给官方 `present` 工具 → 用户在该会话得到「交付文件卡」(预览 / 默认程序打开 / 文件管理器定位)。诚实标注为 `[policy]` 增强项:文件仍是唯一事实源,缺文件依旧是硬失败;卡片渲染在**该子代理会话**的轮次尾部(主会话不显示)
10
+ - **机械阶段推理强度降档(省 token)**:DeepSeek 路由默认 `reasoningEffort: high`,而推理 token **计入 output** 且**推理内容每个带推理回合原样回传**(同时抬高后续 input)。现对两处机械阶段下发 `low`——patch 档的「单点确认」与 `scaffold`(脚手架落地);判据类阶段(PRD/设计/技术方案/QA/验收)保持宿主默认 `high`,**重试自动回升 `high`**(质量优先)。安全前提:先经 `llm.resolveModelInfo()` 探测该路由的 `reasoning.efforts`,只有声明支持才下发——宿主对不支持的值会 `UNSUPPORTED_REASONING_EFFORT` 硬失败且不降级;探测结果按 provider/model 缓存。阶段日志记录实际下发的档位
11
+
12
+ ### 修复
13
+ - **token 计量改走官方 Session 投影(宿主弃用同步事件读取器)**:dsh 0.1.5-rc.2 起 `Session.eventAt()` / `snapshotEvents()` / `ownEvents()` 标记为 deprecated(存量可留、新调用禁止,宿主方向是不再把完整事件序列常驻内存)。计量来源改为**官方投影优先**——`ctx.sessionProjections.stateOf(session,'tokenUsage')` 取四桶(与宿主 token-meter 同一份 fold,重试替换语义更准)+ `'sessionStats'.steps` 取调用数,**零历史扫描**;投影缺失/无数据/读取异常时静默回退原事件扫描(最小 profile 与存量宿主不受影响,不虚报 0,不中断流水线)。`sessionProjections` 走可选 `ctx.inject`,不进 `static inject`——服务缺失时插件照常加载
14
+ - **护栏适配官方通道(提醒 + 挂死检测)**:轻提醒从手写 `session.append('user/message')` + step/end flush 时序状态机,改为官方 `run.localAgent.inject()`(宿主在协议安全边界整批认领,旧注释声称的「会插进 tool_calls→tool_result 触发 400」不成立);挂死检测从「多源取最长事件视图」长度启发式改为官方 **`subagentTiming` 投影**的 `active.through`(已提交事件时间,不受视图失明影响——上次 QA 误判 stalled 的根因)。长工具静默执行仍由 agent 活动守卫豁免;投影不可用时回退旧启发式。中止语义未变
15
+
16
+ ### 改进
17
+ - **声明宿主兼容窗口**:`package.json` 新增 `engines.dsh: ">=0.1.5-rc.2 <0.2.0"` 与 `dsh.manifestVersion: 1`(dsh 0.1.5 起支持的公共 manifest 字段;当前宿主不校验,属作者声明);README「版本锚定」段同步到 v0.1.5-rc.2,并记录本次兼容核对结论与两个待跟进项
18
+ - **清理死注入**:`static inject` 长期硬注入 `tokenMeter` 却全仓从未使用 → 从 static inject / setRuntime / runtime 三处移除(假依赖会拖累插件的加载条件)
19
+ - **工作区 key 文档纠偏**:`workspaceScopeOf` 的「优先用 DSH workspace UUID」分支**当前不可达**(宿主 `resolveByPath` 是异步、我们同步调用),实际生效的是路径派生 `slugPath(cwd)`;注释与 AGENTS.md 改为事实描述,真修(改异步 + 存储 key 迁移)列入 `docs/TODO.md` 待 v0.1.8
20
+
21
+ ### 已知待办
22
+ - **护栏复读检测仍读已弃用的事件读取器**(提醒通道与挂死检测已改官方;复读需要流式文本内容):官方替代是订阅 `'session/event'` post-commit 投递,需先定等价判据,见 `docs/TODO.md`(当前行为不变,存量调用被宿主明确允许)
23
+
5
24
  ## [0.1.6] - 2026-09-07
6
25
 
7
26
  ### 新增
package/README.md CHANGED
@@ -119,15 +119,20 @@ dsh-plugin-teamflow/
119
119
  - Node.js ≥ 22.18;
120
120
  - 依赖宿主提供的 `@deepseek-ai/dsh-*` 与 `react`(peerDependencies,宿主注入,无需单独安装)。
121
121
 
122
- opencode### 版本锚定(dsh 宿主兼容性)
122
+ ### 版本锚定(dsh 宿主兼容性)
123
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` 保持 `*`(宿主注入,宽松兼容)。
124
+ 本插件开发与验证基于 **dsh v0.1.5-rc.2(2026-09-10,tag `dsh-v0.1.5-rc.2`)**;npm `next`=0.1.5-rc.2、`latest`=0.1.5-rc.1(`latest` 常滞后于 `next`,勿以 latest 判断发布线)。`peerDependencies` 保持 `*`(宿主注入,宽松兼容),并在 `package.json` 声明兼容窗口 **`engines.dsh: ">=0.1.5-rc.2 <0.2.0"`** **`dsh.manifestVersion: 1`**——当前 dsh 不读取/校验这两个字段(源码内仅有类型声明),属作者声明性元数据。
125
125
 
126
- 2026-09-04 兼容性核对(dsh 0.1.3-alpha.1):session 持久化 v2(write-lease/JSONL 快照/版本化导出)、attachment/file-upload 收口、Windows 子进程隐藏、workspace 全限定路径硬化——**全部兼容,插件无需调整**。
126
+ 2026-09-10 兼容性核对(dsh 0.1.5-rc.2):插件面板 Slot(原 `conversation` 根 slot → `main` 下的 `conversation` key)、会话格式 V3 + Session 生命周期(`SessionHandle`、异步 `agentLoop.create()`、会话锁)、`ctx.agent` 移除与 Inbox 类型化、SDK/Headless/ACP 默认工具调整、subprocess handle 去除 pid——**插件全部兼容**(未使用被改动的接口;`conversation.view` / `conversation.input.right` 声明未变,slot 树无删除)。其中一项需要跟进:
127
+
128
+ - **Session 同步事件读取器已弃用**(`session.eventAt()` / `snapshotEvents()` / `ownEvents()`,宿主 2026-09-09 起「存量可留、新调用禁止」,方向是不再把完整事件序列常驻内存):**token 计量已改为官方 Session 投影优先**(`ctx.sessionProjections.stateOf(session,'tokenUsage')` 取四桶 + `'sessionStats'.steps` 取调用数),事件扫描降级为无投影宿主的回退;**护栏的提醒通道已改官方 `Agent.inject()`、挂死判据已改用官方 `subagentTiming` 投影的 `active.through`**(长工具静默仍由 agent 活动守卫豁免),只剩**复读检测**仍在读事件(需要流式文本内容,官方替代=订阅 `'session/event'` post-commit 投递,需先定等价判据,见 `docs/TODO.md`)。
129
+
130
+ 2026-09-04 核对(dsh 0.1.3-alpha.1):session 持久化 v2(write-lease/JSONL 快照/版本化导出)、attachment/file-upload 收口、Windows 子进程隐藏、workspace 全限定路径硬化——全部兼容,插件无需调整。
127
131
 
128
132
  插件侧契约约束(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 移除)。
133
+ - 插件注入的 session 事件(`tool-workflow/agent-start`、`user/message` 带 `source.kind='plugin'`)均在宿主 `known-event-types` 词表内;**未来新增自定义事件类型须带 `ignorable: true`**,已知类型载荷不加词表外键;
134
+ - `@deepseek-ai/dsh-client-modules` 自 0.1.2-rc.1 起替代 `@deepseek-ai/dsh-client-runtime`(后者已从 monorepo 移除);
135
+ - 计量读的是**宿主投影 key**(`tokenUsage` / `sessionStats`)而非插件自有格式:宿主若改 key 或 state 版本,此段与 `host/core/metering.ts` 同步更新。
131
136
 
132
137
  ## 安装(对使用者)
133
138
 
package/lib/client.js CHANGED
@@ -1392,7 +1392,7 @@ window.__ModuleLoader__.load({
1392
1392
  sessions
1393
1393
  }) : null);
1394
1394
  }
1395
- function BoardPanel({ backlog, api, onRefresh, sessionId, onShowRun }) {
1395
+ function BoardPanel({ backlog, api, onRefresh, sessionId, onShowRun, openArtifact }) {
1396
1396
  const [drag, setDrag] = react.default.useState(null);
1397
1397
  const [over, setOver] = react.default.useState(null);
1398
1398
  const [det, setDet] = react.default.useState(null);
@@ -1682,7 +1682,8 @@ window.__ModuleLoader__.load({
1682
1682
  }), det ? h(ItemDetailDrawer, {
1683
1683
  det,
1684
1684
  onClose: () => setDet(null),
1685
- onShowRun
1685
+ onShowRun,
1686
+ openArtifact
1686
1687
  }) : null);
1687
1688
  }
1688
1689
  function fmtAt(ts) {
@@ -1691,7 +1692,7 @@ window.__ModuleLoader__.load({
1691
1692
  minute: "2-digit"
1692
1693
  }) : "—";
1693
1694
  }
1694
- function ItemDetailDrawer({ det, onClose, onShowRun }) {
1695
+ function ItemDetailDrawer({ det, onClose, onShowRun, openArtifact }) {
1695
1696
  const d = det && det.data;
1696
1697
  const loading = det && det.loading;
1697
1698
  const err = det && det.err;
@@ -1950,7 +1951,30 @@ window.__ModuleLoader__.load({
1950
1951
  borderRadius: 8,
1951
1952
  padding: "7px 10px",
1952
1953
  wordBreak: "break-all"
1953
- } }, d.runDocs + "/")) : null,
1954
+ } }, d.runDocs + "/"), Array.isArray(d.artifacts) && d.artifacts.length > 0 ? h("div", { style: {
1955
+ display: "flex",
1956
+ flexWrap: "wrap",
1957
+ gap: 6,
1958
+ marginTop: 1
1959
+ } }, d.artifacts.map((a) => h("button", {
1960
+ key: a.name,
1961
+ onClick: () => {
1962
+ if (openArtifact) openArtifact(a.address, a.name);
1963
+ },
1964
+ title: `在右侧栏预览 ${d.runDocs}/${a.name}`,
1965
+ style: {
1966
+ fontSize: 10.5,
1967
+ fontWeight: 600,
1968
+ padding: "3px 9px",
1969
+ borderRadius: 6,
1970
+ cursor: "pointer",
1971
+ lineHeight: "16px",
1972
+ border: `1px solid color-mix(in srgb, ${T.brand} 38%, transparent)`,
1973
+ background: `color-mix(in srgb, ${T.brand} 10%, transparent)`,
1974
+ color: T.brand,
1975
+ whiteSpace: "nowrap"
1976
+ }
1977
+ }, `📄 ${String(a.name).replace(/\.md$/, "")}`))) : null) : null,
1954
1978
  kind === "task" && d.usage ? h("div", { style: {
1955
1979
  display: "flex",
1956
1980
  flexDirection: "column",
@@ -2518,6 +2542,7 @@ window.__ModuleLoader__.load({
2518
2542
  api,
2519
2543
  onRefresh: refresh,
2520
2544
  sessionId: props.sessionId,
2545
+ openArtifact: props.openArtifact,
2521
2546
  onShowRun: (rid) => {
2522
2547
  setTab("pipeline");
2523
2548
  setRunId(rid);
@@ -2536,6 +2561,18 @@ window.__ModuleLoader__.load({
2536
2561
  async function apply(ctx) {
2537
2562
  await ctx.remote.$mount(TEAMFLOW_REMOTE_CONTRIBUTION);
2538
2563
  const teamflow = ctx.get("remote.teamflow");
2564
+ const openArtifact = (address, name) => {
2565
+ try {
2566
+ const sidebarRight = ctx.get("sidebarRight");
2567
+ if (!sidebarRight || typeof sidebarRight.openResource !== "function") {
2568
+ console.warn("[teamflow] 右侧栏服务不可用,无法预览产物", name);
2569
+ return;
2570
+ }
2571
+ sidebarRight.openResource(address);
2572
+ } catch (e) {
2573
+ console.warn("[teamflow] 打开产物失败", name, e && e.message);
2574
+ }
2575
+ };
2539
2576
  ctx.slots.inject("conversation.view", () => ctx.slots.register({
2540
2577
  name: "conversation.view",
2541
2578
  id: "teamflow",
@@ -2544,7 +2581,8 @@ window.__ModuleLoader__.load({
2544
2581
  inject: (sessionId) => ({
2545
2582
  sessionId,
2546
2583
  remote: teamflow,
2547
- sessions: ctx.get("sessions")
2584
+ sessions: ctx.get("sessions"),
2585
+ openArtifact
2548
2586
  })
2549
2587
  }, TeamFlowView));
2550
2588
  ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
package/lib/host.mjs CHANGED
@@ -4,11 +4,21 @@ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
4
4
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
5
5
  import { parameterSchemaSpecToJsonSchema } from "@deepseek-ai/dsh-tools";
6
6
  import { dirname, join } from "node:path";
7
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
8
+ import { fileAddressFor } from "@deepseek-ai/dsh-util-workspace-path";
8
9
  import { execFileSync } from "node:child_process";
9
10
  //#region host/constants.ts
10
11
  /** 单阶段 token 熔断预算(官方口径总消耗:input+cacheRead+cacheWrite+output 累计)。 */
11
12
  const STAGE_TOKEN_BUDGET = 6e4;
13
+ /** 任务夹产物展示顺序(ADR-0008):工作台只列其中**真实存在**的文件,按此顺序出「一键右侧栏预览」按钮。 */
14
+ const TEAMFLOW_ARTIFACT_ORDER = [
15
+ "PRD.md",
16
+ "DESIGN.md",
17
+ "TECHNICAL.md",
18
+ "QA-REPORT.md",
19
+ "ACCEPTANCE.md",
20
+ "meta.json"
21
+ ];
12
22
  /** 护栏轮询间隔 ms。 */
13
23
  const GUARD_POLL_MS = 15e3;
14
24
  /** 挂死判定:连续这么久没有任何新会话事件(provider 挂起/静默死亡)→ stalled(走预算门转人工)。 */
@@ -471,20 +481,27 @@ function extractBlueprint(text) {
471
481
  //#region host/core/context.ts
472
482
  /**
473
483
  * dsh-plugin-teamflow core — 运行期共享状态(进程单例)。
474
- * - runtime(agents/subagents/tokenMeter/workspaceRegistry/agentDefaultModel):由 index=TeamflowService 的 static inject 注入(setRuntime)。
484
+ * - runtime(agents/subagents/workspaceRegistry/agentDefaultModel/llm):由 index=TeamflowService 的 static inject 注入(setRuntime)。
475
485
  * - runs/inFlight/activeProducts:流水线运行期 Map(跨 runner/pipeline/report/服务共享)。
476
486
  * 这是 ADR-0004「共享状态」在编排层的落点:共享对象集中、单向被 core 各模块 import(不反向)。
477
487
  */
478
488
  /** 子代理/计量等宿主能力(由 TeamflowService 装配时 setRuntime 注入)。字段为鸭子类型:消费方自行窄化。 */
479
489
  const runtime = {};
480
- function setRuntime(agents, subagents, tokenMeter, workspaceRegistry, agentDefaultModel, llm) {
490
+ function setRuntime(agents, subagents, workspaceRegistry, agentDefaultModel, llm) {
481
491
  runtime.agents = agents;
482
492
  runtime.subagents = subagents;
483
- runtime.tokenMeter = tokenMeter;
484
493
  runtime.workspaceRegistry = workspaceRegistry;
485
494
  runtime.agentDefaultModel = agentDefaultModel;
486
495
  runtime.llm = llm;
487
496
  }
497
+ /**
498
+ * 可选能力:官方 Session 投影注册表(ctx.sessionProjections,dsh-session-projection)。
499
+ * 单独 setter 而非并入 setRuntime——它是**可选**依赖:用 ctx.inject 在服务可用时注册,
500
+ * 未挂载(最小 profile)时计量自动回退事件扫描,插件照常加载。
501
+ */
502
+ function setSessionProjections(projections) {
503
+ runtime.sessionProjections = projections;
504
+ }
488
505
  /** 运行期 run 注册表(runId → Journal)。 */
489
506
  const runs = /* @__PURE__ */ new Map();
490
507
  /** 进行中的 stage 注册表(runId → { run, stage }),供取消/完成清理。 */
@@ -530,15 +547,19 @@ async function currentModelSupportsVision(provider, model) {
530
547
  /**
531
548
  * 从发起会话推导工作区作用域。
532
549
  *
533
- * 优先级:
534
- * 1. workspaceRegistry.resolveByPath(cwd) → 用 workspace.id(UUID,稳定)作 projectKey
535
- * 2. 回退到 session cwd basename + 短 hash(兼容无 workspaceRegistry 的场景)
550
+ * 优先级(**实际生效的只有第 2 条**):
551
+ * 1. workspaceRegistry.resolveByPath(cwd) → 用 workspace.id(UUID)作 projectKey
552
+ * —— ⚠️ **当前不可达**:宿主 `resolveByPath` `async`(返回 Promise,见
553
+ * `packages/workspace/workspace/src/index.ts`),本函数同步调用 → `ws.id` 恒为 undefined,
554
+ * 永远落到第 2 条。分支保留是为将来迁移(需 await + 存储 key 迁移,见 docs/TODO.md)。
555
+ * 2. session cwd 的 basename + 短 hash(`slugPath`)—— **当前实际使用的 key**
536
556
  * 3. 兜底 'default'
537
557
  *
538
558
  * projectKey 用于 $DSH_HOME/teamflow/<projectKey>/ 目录,要求:
539
- * - 同一 workspace 永远解析到同一个 key(UUID 天然满足)
540
- * - 不同 workspace 即使 basename 相同也不碰撞(UUID 天然满足)
559
+ * - 同一路径永远解析到同一个 key(sha1 派生,满足)
560
+ * - 不同 cwd 即使 basename 相同也不碰撞(hash 参与,满足)
541
561
  * - 目录名安全(只含 [a-zA-Z0-9_-])
562
+ * ⚠️ 代价:key 绑定**路径字符串**,同一工作区换个写法(盘符大小写/软链/尾斜杠)会得到不同 key。
542
563
  */
543
564
  function workspaceScopeOf(agent) {
544
565
  const session = agent?.session;
@@ -1254,7 +1275,63 @@ function runSanityCheck(path) {
1254
1275
  //#endregion
1255
1276
  //#region host/core/metering.ts
1256
1277
  /**
1257
- * 采集子代理会话事件(多源回退——2026-09-07 实锤 r38 usage 全空):
1278
+ * dsh-plugin-teamflow core token 计量(官方口径)。
1279
+ * 依赖:types.ts、context.ts(runtime.sessionProjections)。
1280
+ *
1281
+ * 口径与模型 provider 账单一致(模型无关):
1282
+ * - input : 输入(缓存未命中)
1283
+ * - cacheRead : 输入(缓存命中)
1284
+ * - cacheWrite : 输入写入缓存
1285
+ * - output : 输出
1286
+ * billed input = input + cacheRead + cacheWrite。
1287
+ * 缓存命中率 = cacheRead / (input + cacheRead)。
1288
+ *
1289
+ * 来源优先级(2026-09-10 适配 dsh 0.1.5-rc.2):
1290
+ * 1) **官方 Session 投影**(首选):`ctx.sessionProjections.stateOf(session,'tokenUsage')` 取四桶 +
1291
+ * `'sessionStats'` 取调用数——零历史扫描,且与官方 token-meter 同一份 fold(不再自行复刻口径)。
1292
+ * 2) **事件扫描回退**(存量路径):宿主未挂载投影(最小 profile/未来移除)或投影无 provider usage 时,
1293
+ * 沿用 events → snapshotEvents() → ownEvents() 多源回退。宿主自 2026-09-09 起把这三个同步历史读取器
1294
+ * 标记为 deprecated(存量可留、新调用禁止),本路径仅为无投影宿主保底,不再扩展(见 docs/TODO.md)。
1295
+ */
1296
+ /** 投影字段读数(宽进严出:非法/非正一律 0,不虚报)。 */
1297
+ function countOf(value) {
1298
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
1299
+ }
1300
+ /**
1301
+ * 投影路径(官方口径首选):
1302
+ * - `tokenUsage`(dsh-token-meter 注册,stateVersion 2)→ totals 四桶:与官方同一份 fold,
1303
+ * `assistant/attempt` 内嵌 stream usage 同样计入、`llm/retry-started` 会先关掉被替换的重试槽位
1304
+ * ——比旧事件扫描(只认 assistant/message)更准,重试不重复计。
1305
+ * - `sessionStats`(dsh-session-stats 注册)steps → 调用数(一个 step = 一次模型请求;
1306
+ * 旧扫描按 assistant/message 的 turn.step 去重,语义等价)。
1307
+ * 返回 null = 投影不可用或该会话无 provider usage —— 交给事件扫描回退(不虚报 0)。
1308
+ */
1309
+ function projectedUsageOf(run) {
1310
+ try {
1311
+ const projections = runtime.sessionProjections;
1312
+ if (!projections || typeof projections.stateOf !== "function") return null;
1313
+ const session = run && run.localAgent ? run.localAgent.session : null;
1314
+ if (!session) return null;
1315
+ const usage = projections.stateOf(session, "tokenUsage");
1316
+ const totals = usage && usage.totals;
1317
+ if (!totals) return null;
1318
+ const buckets = {
1319
+ input: countOf(totals.uncachedInputTokens),
1320
+ cacheRead: countOf(totals.cacheReadTokens),
1321
+ cacheWrite: countOf(totals.cacheWriteTokens),
1322
+ output: countOf(totals.outputTokens),
1323
+ calls: 0
1324
+ };
1325
+ if (totalTokensOf(buckets) <= 0) return null;
1326
+ const stats = projections.stateOf(session, "sessionStats");
1327
+ buckets.calls = countOf(stats && stats.steps) || 1;
1328
+ return buckets;
1329
+ } catch (e) {
1330
+ return null;
1331
+ }
1332
+ }
1333
+ /**
1334
+ * 采集子代理会话事件(存量回退路径——2026-09-07 实锤 r38 usage 全空):
1258
1335
  * 宿主新版 Session(session v2)已无 `events` 属性/getter(仅私有 eventsSnapshot 缓存 +
1259
1336
  * 官方 snapshotEvents()/ownEvents() 方法),旧实现读 session.events = undefined → usage 全 null。
1260
1337
  * 回退链(与 guard.eventsOf 同款语义):events(老宿主快照,兼容)→ snapshotEvents()(官方完整日志)
@@ -1302,9 +1379,16 @@ function usageOfEvent(e) {
1302
1379
  }
1303
1380
  /**
1304
1381
  * 累计子代理会话中所有 LLM 调用的真实 usage(官方三桶 + 调用数)。
1305
- * 返回 null 表示拿不到 usage(会话未暴露 events / 无数据)。
1382
+ * 来源优先级:官方 Session 投影(首选,零历史扫描)→ 事件扫描(无投影宿主的存量回退)。
1383
+ * 返回 null 表示两条路径都拿不到 usage(会话未暴露投影与事件 / 无数据)。
1306
1384
  */
1307
1385
  function accumulateSessionUsage(run) {
1386
+ const projected = projectedUsageOf(run);
1387
+ if (projected) return projected;
1388
+ return scannedUsageOf(run);
1389
+ }
1390
+ /** 事件扫描回退(投影未挂载/无数据时使用;沿用 2026-09-07 的多源回退语义,不改行为)。 */
1391
+ function scannedUsageOf(run) {
1308
1392
  const events = sessionEventsOf(run);
1309
1393
  if (events.length === 0) return null;
1310
1394
  const buckets = {
@@ -1353,8 +1437,12 @@ function totalTokensOf(usage) {
1353
1437
  * ⚠️ 状态判定(实锤 run tf-mte906e9):大文件 read-edit 循环是正常模式——模型反复 read 同一大文件
1354
1438
  * (每次 edit 后内容已变,必须重读确认)、输出高度相似的「读后分析」,逐字片段在 400 条窗口内
1355
1439
  * 可累积 ≥12 次——伴随 edit/write 变更调用时只记录观察,不中止(否则大文件修改任务全被误杀)。
1356
- * B. 挂死检测:连续 GUARD_SILENCE_MS 一个新事件都没有(provider 层挂起/连接静默死亡)
1440
+ * B. 挂死检测:连续 GUARD_SILENCE_MS 没有**任何已提交事件**(provider 层挂起/连接静默死亡)
1357
1441
  * → outcome='stalled'(走正常预算门 → 熔断转人工,不自动重试烧钱)。
1442
+ * 2026-09-10:时间来源改为**官方 `subagentTiming` 投影**(`active.through` = 该投影 cut 上
1443
+ * 最新事件时间,由宿主在已提交事件上折叠)——不再依赖「三源取最长视图」的长度启发式
1444
+ * (r1 QA 误判的根因就是那个视图会失明);投影不可用时回退旧启发式。长工具静默执行
1445
+ * (跑 12 分钟测试无输出)仍由 agent 活动守卫豁免,不做误杀。
1358
1446
  * C. 空转检测:会话仍在产出事件,但连续 GUARD_NO_TOOL_MS 没有任何工具调用
1359
1447
  * (纯推理打转/改写式循环;正常 agent 每分钟都在调工具)→ outcome='stalled'。
1360
1448
  * 兜底关系:复读判定放宽后,edit 后陷入死循环的漏网场景由 C(长时间无工具调用)兜住。
@@ -1414,36 +1502,54 @@ function eventsOf(run) {
1414
1502
  function normalizeFragment(s) {
1415
1503
  return String(s || "").toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "");
1416
1504
  }
1417
- /** 观测→执行闭环:向运行中的子代理注入轻提醒(不打断,下一轮 step 可见)。
1418
- * 通道:subagents.start 句柄无 inject——用 DSH 官方 session.append('user/message')(in-process driver 同款用法)。
1419
- * ⚠️ 安全窗口(实锤 tf-mtcnejqj):绝不能插在 assistant(tool_calls) tool/result 之间——
1420
- * provider 校验「tool 消息必须响应前序 tool_calls」,插入 user 消息会 400 invalid_request_error。
1421
- * 因此只入队(pendingInjects),在观察到 step/end(该 step tool/result 已写入)后统一 flush。 */
1422
- function injectReminder(run, text) {
1423
- const queue = run;
1505
+ /**
1506
+ * 官方 `subagentTiming` 投影读数(挂死检测首选源,2026-09-10 改)。
1507
+ * 形状 `{settledMs, active?:{since, through}}`——`through` 是该投影 cut 上**最新事件时间**,
1508
+ * 由宿主在已提交事件上折叠,不受 session.events 快照失明影响(r1 QA 误判根因)。
1509
+ * 返回 null = 投影不可用(未挂载 / 该子代理无 descriptor)→ 回退事件数增长启发式。
1510
+ */
1511
+ function timingOf(run) {
1424
1512
  try {
1425
- if (!Array.isArray(queue.__teamflowPending)) queue.__teamflowPending = [];
1426
- queue.__teamflowPending.push(text);
1427
- } catch (e) {}
1513
+ const projections = runtime.sessionProjections;
1514
+ if (!projections || typeof projections.stateOf !== "function") return null;
1515
+ const local = run && run.localAgent;
1516
+ const session = local && local.session;
1517
+ if (!session) return null;
1518
+ const timing = projections.stateOf(session, "subagentTiming");
1519
+ if (!timing || typeof timing !== "object") return null;
1520
+ const through = timing.active && timing.active.through;
1521
+ return { activeThrough: typeof through === "number" && Number.isFinite(through) ? through : void 0 };
1522
+ } catch (e) {
1523
+ return null;
1524
+ }
1428
1525
  }
1429
- function flushReminders(run) {
1526
+ /** 观测→执行闭环:向运行中的子代理注入轻提醒(不打断,下一 step 可见)。
1527
+ *
1528
+ * 通道(2026-09-10 改):`run.localAgent.inject()` —— 宿主官方 Agent 通道。next-step 队列由
1529
+ * agent loop 在 `preStep` 内、tool/result 之后整批认领,因此**不存在**「插进
1530
+ * assistant(tool_calls) → tool/result 之间触发 provider 400」的窗口;旧实现「先入队
1531
+ * `__teamflowPending`、观察到 step/end 再 session.append」的时序状态机整体删除。
1532
+ * (旧注释「subagents.start 句柄无 inject」是错的:`run.localAgent` 是活 Agent,
1533
+ * 有 `inject/steer/followup` —— `packages/core/agent/src/runtime-types.ts`。)
1534
+ *
1535
+ * 语义:`inject` 是 best-effort(可能晚一个 step),且不唤醒 idle driver——提醒只用于
1536
+ * 「仍在跑的 agent」;退化中止仍走 fire()/dispose(),不改为 steer 纠偏(后者是独立课题)。 */
1537
+ function injectReminder(run, text) {
1430
1538
  try {
1431
- const queue = run.__teamflowPending;
1432
- if (!queue || queue.length === 0) return;
1433
- const local = run.localAgent;
1434
- if (!local || typeof local.session?.append !== "function") return;
1435
- for (const text of queue.splice(0)) local.session.append("user/message", {
1436
- id: crypto.randomUUID(),
1437
- role: "user",
1539
+ const agent = run && run.localAgent;
1540
+ if (!agent || typeof agent.inject !== "function") return;
1541
+ agent.inject(createUserMessage({
1438
1542
  content: [{
1439
1543
  type: "text",
1440
1544
  text
1441
1545
  }],
1442
1546
  source: {
1443
1547
  kind: "plugin",
1444
- plugin: "teamflow"
1548
+ plugin: "dsh-plugin-teamflow",
1549
+ form: "notice",
1550
+ summary: "护栏轻提醒"
1445
1551
  }
1446
- }, { surfaceOp: "append" });
1552
+ }));
1447
1553
  } catch (e) {}
1448
1554
  }
1449
1555
  /**
@@ -1485,31 +1591,37 @@ function startStageGuard(opts) {
1485
1591
  stage.guardReason = reason;
1486
1592
  stage.guardOutcome = outcome;
1487
1593
  if (outcome === "stalled") try {
1488
- const local = run.localAgent;
1489
- const session = local && local.session;
1490
- const lens = [];
1491
- if (session) {
1492
- try {
1493
- const r = session.events;
1494
- lens.push(`events=${Array.isArray(r) ? r.length : typeof r === "function" ? r().length : "?"}`);
1495
- } catch (e) {
1496
- lens.push("events=err");
1497
- }
1498
- try {
1499
- lens.push(`snap=${typeof session.snapshotEvents === "function" ? session.snapshotEvents().length : "-"}`);
1500
- } catch (e) {
1501
- lens.push("snap=err");
1502
- }
1503
- try {
1504
- lens.push(`own=${typeof session.ownEvents === "function" ? session.ownEvents().length : "-"}`);
1505
- } catch (e) {
1506
- lens.push("own=err");
1594
+ const timing = timingOf(run);
1595
+ let detail;
1596
+ if (timing) detail = `subagentTiming.through=${timing.activeThrough === void 0 ? "(无 open turn)" : timing.activeThrough}`;
1597
+ else {
1598
+ const local = run.localAgent;
1599
+ const session = local && local.session;
1600
+ const lens = [];
1601
+ if (session) {
1602
+ try {
1603
+ const r = session.events;
1604
+ lens.push(`events=${Array.isArray(r) ? r.length : typeof r === "function" ? r().length : "?"}`);
1605
+ } catch (e) {
1606
+ lens.push("events=err");
1607
+ }
1608
+ try {
1609
+ lens.push(`snap=${typeof session.snapshotEvents === "function" ? session.snapshotEvents().length : "-"}`);
1610
+ } catch (e) {
1611
+ lens.push("snap=err");
1612
+ }
1613
+ try {
1614
+ lens.push(`own=${typeof session.ownEvents === "function" ? session.ownEvents().length : "-"}`);
1615
+ } catch (e) {
1616
+ lens.push("own=err");
1617
+ }
1507
1618
  }
1619
+ detail = `投影不可用,回退事件视图:${lens.join(" / ") || "session 不可访问"}`;
1508
1620
  }
1509
1621
  journal.logs.push({
1510
1622
  t: Date.now(),
1511
1623
  level: "warn",
1512
- message: `${label} 挂死诊断:${lens.join(" / ") || "session 不可访问"}`
1624
+ message: `${label} 挂死诊断:${detail}`
1513
1625
  });
1514
1626
  } catch (e) {}
1515
1627
  try {
@@ -1567,7 +1679,6 @@ function startStageGuard(opts) {
1567
1679
  }
1568
1680
  }
1569
1681
  observeToolCalls(newEvents);
1570
- if (newEvents.some((ev) => ev?.type === "step/end")) flushReminders(run);
1571
1682
  processed = events.length;
1572
1683
  }
1573
1684
  for (const ev of events.slice(-200)) {
@@ -1581,7 +1692,28 @@ function startStageGuard(opts) {
1581
1692
  break;
1582
1693
  }
1583
1694
  }
1584
- if (events.length !== lastEventCount) {
1695
+ const timing = timingOf(run);
1696
+ if (timing) {
1697
+ if (timing.activeThrough === void 0) lastGrowthAt = Date.now();
1698
+ else if (Date.now() - timing.activeThrough > 6e5) {
1699
+ if (lastMutationAt > 0 && isAgentBusy(run)) {
1700
+ if (!busyWarned) {
1701
+ busyWarned = true;
1702
+ try {
1703
+ journal.logs.push({
1704
+ t: Date.now(),
1705
+ level: "warn",
1706
+ message: `${label} 已提交事件静默(subagentTiming.through ${Math.round((Date.now() - timing.activeThrough) / 1e3)}s 未推进)但 agent 仍活动——视为长工具执行而非挂死,继续观察`
1707
+ });
1708
+ } catch (e) {}
1709
+ }
1710
+ lastGrowthAt = Date.now();
1711
+ } else {
1712
+ fire(`挂死(${Math.round(GUARD_SILENCE_MS / 6e4)} 分钟无任何已提交事件,来源:subagentTiming 投影)`, "stalled");
1713
+ return;
1714
+ }
1715
+ } else lastGrowthAt = Date.now();
1716
+ } else if (events.length !== lastEventCount) {
1585
1717
  lastEventCount = events.length;
1586
1718
  lastGrowthAt = Date.now();
1587
1719
  } else if (Date.now() - lastGrowthAt > 6e5) {
@@ -1644,6 +1776,45 @@ function startStageGuard(opts) {
1644
1776
  * dsh-plugin-teamflow core — 子代理执行器(并发池 / 单阶段运行 / 重试与熔断)。
1645
1777
  * 依赖:util/constants/types + core(context/metering)。
1646
1778
  */
1779
+ /**
1780
+ * 推理强度能力探测(缓存,2026-09-11):只有宿主明确声明该路由支持某档位才下发。
1781
+ * 宿主对**不支持的值硬失败且不降级**(`UNSUPPORTED_REASONING_EFFORT`),所以宁可不下发。
1782
+ * 返回 null = 探测不可用(老宿主/未声明容量)→ 调用方一律不下发,保持宿主默认。
1783
+ */
1784
+ const effortSupportCache = /* @__PURE__ */ new Map();
1785
+ async function supportedEfforts(route) {
1786
+ if (!route.provider || !route.model) return null;
1787
+ const llm = runtime.llm;
1788
+ if (!llm || typeof llm.resolveModelInfo !== "function") return null;
1789
+ const key = `${route.provider}/${route.model || ""}`;
1790
+ if (effortSupportCache.has(key)) return effortSupportCache.get(key) || null;
1791
+ let out = null;
1792
+ try {
1793
+ const info = await llm.resolveModelInfo(route.provider, route.model);
1794
+ const efforts = info && info.reasoning && info.reasoning.efforts;
1795
+ if (Array.isArray(efforts)) out = efforts.map((e) => typeof e === "string" ? e : e && typeof e === "object" && typeof e.id === "string" ? e.id : null).filter((x) => typeof x === "string" && x.length > 0);
1796
+ } catch (e) {
1797
+ out = null;
1798
+ }
1799
+ effortSupportCache.set(key, out);
1800
+ return out;
1801
+ }
1802
+ /**
1803
+ * 解析本阶段要下发的推理强度:
1804
+ * - 阶段未要求降档(effortHint 空)→ 不传,宿主默认(DeepSeek high)
1805
+ * - 第 1 次尝试用 hint;**重试回升 'high'**(质量优先,ADR-0006)
1806
+ * - 只有探测到该路由支持该档位才返回,否则不传(防 `UNSUPPORTED_REASONING_EFFORT` 硬失败)
1807
+ * - 未下发时返回原因文本 → 调用方记 warn(这类静默失败必须可见,见 2026-09-11 实锤)
1808
+ */
1809
+ async function resolveStageEffort(route, attempt, effortHint) {
1810
+ const base = effortHint && String(effortHint).trim() ? String(effortHint).trim() : null;
1811
+ if (!base) return {};
1812
+ const wanted = attempt > 1 ? "high" : base;
1813
+ const supported = await supportedEfforts(route);
1814
+ if (!supported) return { skip: `路由 ${route.provider || "?"}/${route.model || "?"} 未声明 reasoning.efforts(或探测不可用)` };
1815
+ if (supported.indexOf(wanted) === -1) return { skip: `路由不支持 ${wanted}(可用:${supported.join("/") || "无"})` };
1816
+ return { effort: wanted };
1817
+ }
1647
1818
  /** 并发池:按 max 个 worker 消费 items,返回同序结果。 */
1648
1819
  async function runPool(items, max, fn) {
1649
1820
  const results = new Array(items.length);
@@ -1702,7 +1873,7 @@ function resolveChildRoute(parent) {
1702
1873
  return out;
1703
1874
  }
1704
1875
  /** 运行单个阶段子代理:执行 + 产出实质校验 + token 双口径计量 + stage 状态流转。 */
1705
- async function runAgent(journal, parent, label, phase, prompt, signal, taskKey) {
1876
+ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey, attempt = 1, effortHint) {
1706
1877
  const maxSeq = journal.stages.length ? Math.max(...journal.stages.map((s) => s.seq)) : 0;
1707
1878
  let stageText = null;
1708
1879
  const stage = {
@@ -1726,11 +1897,24 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey)
1726
1897
  let cancelGuard = null;
1727
1898
  try {
1728
1899
  const route = resolveChildRoute(parent);
1729
- const agentOptions = route.provider || route.model ? {
1900
+ const eff = await resolveStageEffort(route, attempt, effortHint);
1901
+ const effort = eff.effort;
1902
+ const agentOptions = route.provider || route.model || effort ? {
1730
1903
  ...route.provider ? { provider: route.provider } : {},
1731
1904
  ...route.model ? { model: route.model } : {},
1732
- ...route.maxTokens ? { maxTokens: route.maxTokens } : {}
1905
+ ...route.maxTokens ? { maxTokens: route.maxTokens } : {},
1906
+ ...effort ? { reasoningEffort: effort } : {}
1733
1907
  } : void 0;
1908
+ if (effort && !journal.cancelled) journal.logs.push({
1909
+ t: Date.now(),
1910
+ level: "info",
1911
+ message: `${label} 推理强度:${effort}${attempt > 1 ? "(重试回升)" : "(机械阶段降档)"}`
1912
+ });
1913
+ else if (eff.skip && !journal.cancelled) journal.logs.push({
1914
+ t: Date.now(),
1915
+ level: "warn",
1916
+ message: `${label} 推理强度未降档:${eff.skip}——保持宿主默认`
1917
+ });
1734
1918
  run = await runtime.subagents.start(providerName(), {
1735
1919
  label,
1736
1920
  prompt: [{
@@ -1845,8 +2029,9 @@ async function runAgent(journal, parent, label, phase, prompt, signal, taskKey)
1845
2029
  } catch (e2) {}
1846
2030
  }
1847
2031
  }
1848
- /** 单阶段重试 + token 熔断(官方口径:input+cacheRead+cacheWrite+output 累计)。 */
1849
- async function withRetry(journal, parent, label, phase, prompt, signal, taskKey) {
2032
+ /** 单阶段重试 + token 熔断(官方口径:input+cacheRead+cacheWrite+output 累计)。
2033
+ * `effortHint`:机械阶段的推理强度降档提示(第 1 次尝试生效,重试自动回升 high,见 resolveStageEffort)。 */
2034
+ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey, effortHint) {
1850
2035
  let attempts = 0;
1851
2036
  let stageTokens = 0;
1852
2037
  let lastStage = null;
@@ -1855,7 +2040,7 @@ async function withRetry(journal, parent, label, phase, prompt, signal, taskKey)
1855
2040
  const labelNow = attempt > 1 ? `${label}(第 ${attempt} 次重试)` : label;
1856
2041
  const promptNow = attempt > 1 && lastStage ? prompt + buildRetryDiagnostic(attempt, lastStage) : prompt;
1857
2042
  const beforeLen = journal.stages.length;
1858
- const result = await runAgent(journal, parent, labelNow, phase, promptNow, signal, taskKey);
2043
+ const result = await runAgent(journal, parent, labelNow, phase, promptNow, signal, taskKey, attempt, effortHint);
1859
2044
  lastStage = journal.stages[beforeLen] || null;
1860
2045
  if (lastStage && lastStage.phase === phase) stageTokens += totalTokensOf(lastStage.usage);
1861
2046
  if (result) return {
@@ -2249,6 +2434,16 @@ function headTailClip(text, head, tail) {
2249
2434
  if (s.length <= head + tail) return s;
2250
2435
  return s.slice(0, head) + "\n...\n[CHANGED SECTION]\n" + s.slice(-tail);
2251
2436
  }
2437
+ /**
2438
+ * 产物交付 · policy(2026-09-11):把任务夹产物交给官方的 `present` 工具,用户在该会话里得到
2439
+ * 「交付文件卡」(预览 / 默认程序打开 / 文件管理器定位)。
2440
+ * 诚实机制说明:present 是**模型工具**,host 不强制(没调用只是少一张卡,产物文件仍是唯一交付物);
2441
+ * 卡片渲染在**本子代理会话**的轮次尾部(宿主 ui-deliverables 挂 conversation.chat.turnTail),
2442
+ * 主会话不显示——所以它是增强项,工作台侧的「📄 产物」按钮才是主路径。
2443
+ */
2444
+ const ARTIFACT_DELIVERY = (runDocs) => `[Artifact delivery · policy] After the deliverable file exists (and before your final reply, before the state block), call the \`present\` tool so the user gets a delivery card (preview / open in default app / reveal in file manager):
2445
+ present({ files: [{ path: "${runDocs}/<your deliverable>.md", description: "<one-line what it is>" }] })
2446
+ Present ONLY user-facing deliverables (≤4 files, e.g. PRD.md / TECHNICAL.md / QA-REPORT.md / ACCEPTANCE.md) — never scratch files, temp scripts or command logs. This is additive: the file stays the single source of truth, and a missing file is still a hard failure.`;
2252
2447
  const TOKEN_HYGIENE = (runId) => `[TOKEN HYGIENE · policy] Context is expensive. Budget discipline below — host enforcement is warn + live reminder only (never interrupts), follow it as self-discipline:
2253
2448
  - [File scope] Whole-file read is allowed ONLY for target files explicitly listed in the task spec. To understand other files' interfaces, use grep for keywords (do not read whole files). Never whole-file read source files outside the task scope.
2254
2449
  - [No duplicate reads] Same file: read ≤1 times. To verify a change, grep the change point instead of re-reading the whole file.
@@ -2283,6 +2478,7 @@ ${requirement}
2283
2478
  4. Output the full PRD (Markdown): background & goals, user stories (each with testable acceptance criteria), scope & non-goals, interaction flow summary, priority (P0/P1/P2), dependencies & risks, milestone suggestions. ACs must be testable/quantifiable; prefer precision & brevity. **No revision table, no version fields like「版本:vX.Y / 状态:进行中」** (the folder IS the archive; its name carries the identity).
2284
2479
  5. [Memory write-back · convention changes ONLY] Update docs/teamflow/memory.md ONLY if this requirement introduces new team conventions / tech-stack decisions (replace the same-topic line, idempotent, no changelog-style appending); otherwise do not touch memory.
2285
2480
  6. [Engineering actions carried verbatim] Engineering instructions in the raw requirement (create/switch branch, commit, tag...) MUST be preserved verbatim into the "工程约束" section of the PRD: specify the action, timing, and baseline (e.g. "branch from latest main, then implement"). If the workspace already has uncommitted changes, note how to handle them. Never silently drop or reword engineering instructions.
2481
+ ${ARTIFACT_DELIVERY(RUN(state))}
2286
2482
  7. [State] End with a state block (phase="prd"): summary covers the AC highlights + one-sentence product semantics; extra contains { "acIndex": {...}, "summary": "<product one-liner>", "techStack": "..." }.${STATE_BLOCK_INSTRUCTION}`;
2287
2483
  const designPrompt = (prd, root, runId, state) => `You are a senior UI/UX designer. The current workspace IS the target project.
2288
2484
  ${productCtx(root)}${stateSliceFor(state, "design")}
@@ -2339,6 +2535,7 @@ ${JSON.stringify(tasks)}
2339
2535
  - modules: per touched file — responsibility + deps + assembly order + **architecture rationale (why)**.
2340
2536
  - tasks: parallelizable tasks split by file boundary (disjoint files → parallel); merge or sequence where dependencies/conflicts exist.
2341
2537
  - If you find duplication or a module that should be extracted (e.g. unified storage wrapper), add it to modules with the why.
2538
+ ${ARTIFACT_DELIVERY(RUN(state))}
2342
2539
  6. [State] End with a state block (phase="tech"), extra = { "verifyScripts": [...], "modules": {"/file": "contract or one-liner"} }, summary = key architecture/contract decisions.${STATE_BLOCK_INSTRUCTION}`;
2343
2540
  /**
2344
2541
  * 架构师 prompt(M1「认知前置 + 架构落地」):全模式启用,轻量版(lite/tech/patch)只产架构蓝图 JSON,
@@ -2417,6 +2614,7 @@ ${clip(devSummary, 15e3)}
2417
2614
  | 编号 | 严重级(P0/P1/P2/P3) | 功能模块 | 复现步骤 | 期望行为 | 实际行为 | 关联验收项 |
2418
2615
  If no defects: explicitly output 「未发现缺陷」.
2419
2616
  7. Chinese Markdown, concrete & executable; write the **complete** report to ${RUN(state)}/QA-REPORT.md (write once, tight body) — **this file IS the deliverable**: scope & environment, cases & results (pass/fail/blocked), 人工补测清单, defect table (if any), conclusion (whether acceptance-ready). [Boundary] only under ${TF_DOCS}/.
2617
+ ${ARTIFACT_DELIVERY(RUN(state))}
2420
2618
  8. [State] End with a state block (phase="qa"), summary = test conclusion / blocked items, extra = { "verifyScripts": [...] }.${STATE_BLOCK_INSTRUCTION}`;
2421
2619
  /** QA 打回后的开发修复 prompt:确认缺陷是否属实 → 修复 → 复验交接(QA→dev 打回闭环用)。 */
2422
2620
  const qaFixPrompt = (defects, qa, tech, prd, root, runId, state) => `You are a senior full-stack engineer. The QA report points out several defects — **confirm each one** and fix them, then hand back for QA re-verification.
@@ -2459,6 +2657,7 @@ ${vision ? "[Visual re-check] If QA saved screenshots under the task folder, spo
2459
2657
  3. [Not-applicable judgment] If the PRD/tech-change/confirm doc already states「需求与现状不符」, or the dev result is explicitly「无需改动」, the verdict must be **「📝 需求不适用」** with reasons — do NOT mark ✅ 通过 just for "no defects".
2460
2658
  4. [Acceptance report · HOST-ENFORCED] Write the **complete** report to ${RUN(state)}/ACCEPTANCE.md (write once) — **this file IS the deliverable**: verdict line, per-criterion check table, opinions & leftovers. **The verdict line MUST be the LAST line of the file, verbatim one of: 验收结论:✅ 通过 / 验收结论:⚠️ 有条件通过 / 验收结论:❌ 不通过 / 验收结论:📝 需求不适用** — the host parses ONLY this line; missing it = contract violation → the run stops for human review; missing file = hard failure (needs-human, pipeline stops). [Memory write-back · convention changes ONLY] Update docs/teamflow/memory.md only if this requirement introduces new conventions/tech-stack decisions, or the 已知待办 list changes (same-topic line replace, idempotent, no changelog appending); otherwise don't touch memory. [Boundary] only under ${TF_DOCS}/; never modify AGENTS.md beyond the <!-- teamflow --> managed zone.
2461
2659
  5. Chinese Markdown.
2660
+ ${ARTIFACT_DELIVERY(RUN(state))}
2462
2661
  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}`;
2463
2662
  /** 需求分诊模型 prompt(模型驱动 triage;供 core/triage.runTriage 使用)。 */
2464
2663
  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.
@@ -3208,7 +3407,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
3208
3407
  label: "产品经理 · 梳理 PRD",
3209
3408
  fn: prdPrompt
3210
3409
  };
3211
- const prdR = await withRetry(journal, parent, pForm.label, "prd", pForm.fn(requirement, root, journal.id, state), signal);
3410
+ const prdR = await withRetry(journal, parent, pForm.label, "prd", pForm.fn(requirement, root, journal.id, state), signal, void 0, options.mode === "patch" ? "low" : null);
3212
3411
  if (!prdR.text) throw stageFailError("prd", prdR);
3213
3412
  prd = prdR.text;
3214
3413
  timeline.prd = prd;
@@ -3249,7 +3448,7 @@ async function executePipeline(journal, parent, requirement, options, signal, re
3249
3448
  level: "phase",
3250
3449
  message: "进入阶段:架构规划"
3251
3450
  });
3252
- const scR = await withRetry(journal, parent, "架构师 · 脚手架规划与落地", "scaffold", scaffoldPrompt(requirement, design, root, journal.id, state), signal);
3451
+ const scR = await withRetry(journal, parent, "架构师 · 脚手架规划与落地", "scaffold", scaffoldPrompt(requirement, design, root, journal.id, state), signal, void 0, "low");
3253
3452
  if (!scR.text) throw stageFailError("scaffold", scR);
3254
3453
  scaffold = scR.text;
3255
3454
  timeline.scaffold = scaffold;
@@ -4667,14 +4866,16 @@ var TeamflowService = class extends TypertRemoteService {
4667
4866
  static inject = [
4668
4867
  "agents",
4669
4868
  "subagents",
4670
- "tokenMeter",
4671
4869
  "typert",
4672
4870
  "tools",
4673
4871
  "llm"
4674
4872
  ];
4675
4873
  constructor(ctx) {
4676
4874
  super(ctx, "teamflow");
4677
- setRuntime(ctx.get("agents"), ctx.get("subagents"), ctx.get("tokenMeter"), ctx.get("workspaceRegistry"), ctx.get("agentDefaultModel"), ctx.get("llm"));
4875
+ setRuntime(ctx.get("agents"), ctx.get("subagents"), ctx.get("workspaceRegistry"), ctx.get("agentDefaultModel"), ctx.get("llm"));
4876
+ ctx.inject(["sessionProjections"], (projectionCtx) => {
4877
+ setSessionProjections(projectionCtx.get("sessionProjections"));
4878
+ });
4678
4879
  loadActiveTeams();
4679
4880
  let interruptedCount = 0;
4680
4881
  try {
@@ -4789,10 +4990,14 @@ var TeamflowService = class extends TypertRemoteService {
4789
4990
  if (!item) return null;
4790
4991
  const reqId = k === "req" ? item.id : item.reqId || null;
4791
4992
  let runDocs = null;
4993
+ let runDocsRoot = null;
4792
4994
  let runInfo = null;
4793
4995
  for (const j of runsFor(sc.projectKey)) {
4794
4996
  if (j.reqId !== reqId) continue;
4795
- if (j.runDocs && !runDocs) runDocs = j.runDocs;
4997
+ if (j.runDocs && !runDocs) {
4998
+ runDocs = j.runDocs;
4999
+ runDocsRoot = j.workspacePath || null;
5000
+ }
4796
5001
  if (!runInfo) runInfo = {
4797
5002
  runId: j.id,
4798
5003
  status: j.status,
@@ -4801,6 +5006,17 @@ var TeamflowService = class extends TypertRemoteService {
4801
5006
  endedAt: j.endedAt || null
4802
5007
  };
4803
5008
  }
5009
+ const runArtifacts = [];
5010
+ if (runDocs && runDocsRoot && typeof sessionId === "string" && sessionId) try {
5011
+ const present = new Set(readdirSync(join(runDocsRoot, runDocs)));
5012
+ for (const name of TEAMFLOW_ARTIFACT_ORDER) {
5013
+ if (!present.has(name)) continue;
5014
+ runArtifacts.push({
5015
+ name,
5016
+ address: fileAddressFor(sessionId, void 0, `${runDocs}/${name}`)
5017
+ });
5018
+ }
5019
+ } catch (e) {}
4804
5020
  const byRole = item.byRole || null;
4805
5021
  let subtasks = [];
4806
5022
  let bugs = [];
@@ -4870,6 +5086,7 @@ var TeamflowService = class extends TypertRemoteService {
4870
5086
  byRole: k === "task" ? byRole : null,
4871
5087
  reqId: reqId || null,
4872
5088
  runDocs,
5089
+ artifacts: runArtifacts,
4873
5090
  runInfo,
4874
5091
  subtasks,
4875
5092
  bugs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-teamflow",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "TeamFlow 团队研发流水线(TypeScript):需求→PRD→设计→架构→技术方案→并行开发→QA→验收;backlog 持久化 + 断点续跑 + 完成汇报 + 防假交付;Web 团队工作台(conversation.view tab + 拖拽看板)",
5
5
  "type": "module",
6
6
  "main": "./lib/host.mjs",
@@ -13,9 +13,11 @@
13
13
  "./package.json": "./package.json"
14
14
  },
15
15
  "engines": {
16
- "node": ">=22.18"
16
+ "node": ">=22.18",
17
+ "dsh": ">=0.1.5-rc.2 <0.2.0"
17
18
  },
18
19
  "dsh": {
20
+ "manifestVersion": 1,
19
21
  "bundle": {
20
22
  "patch": "./cordis.patch.yml"
21
23
  },
@@ -73,7 +75,8 @@
73
75
  "@deepseek-ai/dsh-client-ui-conversation": "*",
74
76
  "@deepseek-ai/dsh-llm": "*",
75
77
  "@deepseek-ai/dsh-tools": "*",
76
- "@deepseek-ai/dsh-typert-protocol": "*"
78
+ "@deepseek-ai/dsh-typert-protocol": "*",
79
+ "@deepseek-ai/dsh-util-workspace-path": "*"
77
80
  },
78
81
  "peerDependenciesMeta": {
79
82
  "@deepseek-ai/dsh-base": {
@@ -97,6 +100,9 @@
97
100
  "@deepseek-ai/dsh-typert-protocol": {
98
101
  "optional": true
99
102
  },
103
+ "@deepseek-ai/dsh-util-workspace-path": {
104
+ "optional": true
105
+ },
100
106
  "react": {
101
107
  "optional": true
102
108
  }