# AGENTS.md

给在本仓库里干活的代理（和人）看的说明。

## 常用命令

| 命令 | 作用 |
|---|---|
| `pnpm install` | 安装依赖 |
| `pnpm run build` | 构建 `lib/`（改了前端或主机代码后，测试实例要重启才看得到） |
| `pnpm run typecheck` | 类型检查 |
| `pnpm run lint` | 代码检查（biome） |
| `pnpm test` | 单元测试（vitest） |
| `pnpm run hero` | 重做 README 封面图（中英两张，见下文） |

## 发布与推送

```sh
# 1. 先构建再验证：打包守卫要读 lib/，没构建过它整块跳过（那就等于没查）
pnpm run build && pnpm run typecheck && pnpm run lint && pnpm test

# 2. CHANGELOG.md：把 Unreleased 那节落成 ## [<ver>] - <日期>，随本版一起提交；不更新不得发版

# 3. 版本自增（会同时提交并打 tag；只想改版本号就加 --no-git-tag-version）
pnpm version minor        # 按下面的判据选 patch / minor / major

# 4. 提交 + 打标签（上一步已经做了就跳过）
git commit -am "release: workflow-lite v<ver>"
git tag workflow-lite-v<ver>

# 5. 推 git + 发 npm
git push && git push --tags
npm publish                # publishConfig 已声明 access: public

# 6. GitHub Release（手动，没有 workflow；notes 用 CHANGELOG 本节内容）
gh release create workflow-lite-v<ver> --title "v<ver>" --notes-file <notes 文件>
```

发版相关的几条：

- **版本号按严格 SemVer 判，不看改动大小**：新增功能（Added）→ `minor`；修 bug、只改措辞与内部实现（Fixed / 纯重构）→ `patch`；不兼容变更（删或改工具 action 与参数、数据格式、RPC 目标，或让旧版读不了新写的文件）→ `major`。判据落在**公共 API**上：工具接口、工作流 JSON / state.yaml 的数据格式、RPC 目标、peer 区间。**只改客户端呈现（排版、动效、配色、文案）不算公共 API 变更**，按它实际做了什么归类——新增了能力就是 minor，不是 patch。
- 不兼容变更要**在 CHANGELOG 里明说**：哪一侧不兼容（新版读旧版 / 旧版读新版）、会丢什么、别的部分是否不受影响。
- `.npmrc` 把 `@xiaoso` 作用域指到 npmjs 官方源；镜像站发布不了。
- `publishConfig.access: public`：scoped 包不加这条，`npm publish` 默认按 restricted 发。
- 插件版本与 dsh 版本**不锁步**：peer 是一条 `>=0.2.0-0 <0.2.1-0 || >=0.2.1-0 <0.3.0-0` 的区间（`engines.dsh` 与 peer 同串），插件自己按 SemVer 走；只有换支持版本线时才动它。
- peer 对不上运行版本时宿主**静默跳过**该 bundle（只记进 `skippedBundles`）：症状是工具突然消失、补丁层不生效，而不是启动失败。排查看 `<profile>/cordis.yml` 里有没有自己的行。
- `THIRD_PARTY_NOTICES.md` 由 `scripts/notices.mjs` 生成，别手改。改过依赖跑一次 `pnpm run build`，它变了就一起提交；`pnpm run notices:check` 与 `tests/package.spec.ts` 盯着它有没有过期。

## CHANGELOG 写法

CHANGELOG 每节会原样贴进 GitHub Release，写给用户看，不是开发记录。

- **分类**：小节沿用 Added / Changed / Fixed，需要时加 Other；Release 里对应写成「新增功能 / 体验优化 / 问题修复 / 其他变更」。Other 放不兼容变更、需要迁移的事、依赖升级和面向开发者的接口变化。
- **一条一句**：一个改动写成一句完整的话，句末带句号；要补一层意思就用分号接一个分句，不再往下分子条目，也不加粗标题。一般一两行就写完，写不完的说明是在讲实现。
- **写用户能感知的结果**：现在能做什么、看到什么、哪个问题没了。不写组件名、CSS、RPC、源码路径、动效的实现手法；界面上的名字、命令、设置项、环境变量这类用户会碰到的照写。
- **开头的词定基调**：新增功能用「新增…」「支持…」「…可以…」；体验优化用「优化…」「改善…」「…更…」；问题修复写成「修复〈什么情况下〉〈出了什么问题〉的问题」，或者直接说现在的正确表现。不写「以前……现在……」的前后对比。
- **限制与实验性写进同一句**：实验性的标「实验性」，已知限制用一个分句交代（如「目前只支持……」），不另起一段免责说明。
- **不兼容变更**：同一句里写清谁受影响、要做什么（如「用旧字段的工作流包需要重新导出」）；按上文要求明说哪一侧不兼容、会丢什么。
- **合并与取舍**：同一处界面的几项小改合成一条；纯重构、测试、只改了内部措辞的不写；这一版里新加、又在这一版里修掉的问题不写进「问题修复」，那只是这一版功能本来的样子。
- 一节开头可以留一两句概述这一版的重点，和一句「升级要不要做事」。

例子（同一条改动）：

- 差：工作流中心「运行实例」「工作流」页的右栏、编辑页的「版本」对话框，内容一多是整栏连标题一起滚。现在标题、存版本那一栏钉在上面，只有实例表、版本表在自己的框里滚
- 好：实例、版本较多时只有列表在框内滚动，标题和保存栏保持不动。


## DSH 版本

- 兼容范围写成一条三段版本号的区间（预览版也要能装），放在每个 `@deepseek-ai/dsh-*` 的 `peerDependencies` 里；DSH 检查 peer 时带 `includePrerelease`，所以不能用 `^`。
- 编译用的 `devDependencies`（`@deepseek-ai/dsh-*`、`@deepseek-ai/cordis`）写死精确版本，配合 `pnpm-lock.yaml` 防依赖漂移。
- README 两版各带一个同区间的 DSH 徽标和一句适配说明。
- 换支持的版本线时四处一起改：peer 区间、dev 精确版本、两版 README 的徽标与说明；`tests/package.spec.ts` 检查它们一致。

## 编译口径（DSH / 通用）

同一张图编译成两种口径：插件里执行用 **DSH 口径**，导出成 skill 用**通用口径**（`PlanOptions.skill` 给了就是通用口径）。

- 跟执行环境有关的话——执行方式、复用执行者、设定目标、怎么加载 skill、运行状态——只写在 `src/shared/dialect.ts`，每句两套：DSH 口径可以点名工具与插件动作（`subagent`、`spawn_teammate`、`create_goal`、`workflow_lite` 的 `state` / `resume`……）；通用口径不点名任何工具，只说要做成什么。`compile.ts` 只写和环境无关的部分（图、流程、资源、交接），不要在里面写工具名。
- 运行状态：DSH 口径由插件建实例、模型经 `state` 动作改；通用口径由模型自己建、自己改工作区里的 `.workflow-lite/runs/{instance}/state.yaml`，计划里按图给出初始内容。
- 加一条工作流设置或改一句措辞时两套一起写。`tests/shared/dialect.spec.ts` 把所有设置打开、用通用口径编译，扫计划和每份任务描述里有没有 `DSH_ONLY_TERMS`；新出现的工具名、插件说法要补进这张表。

## 配色主题

- 亮色、暗色各四套（晴白、瑞士、雾蓝、林间 / 石板、墨黑、深海、暖夜），色值全在 `src/client/model/themes.ts`；默认晴白 + 石板，瑞士排第二。
- 「瑞士」除了颜色还带一套「形」：亮色档选了它、此刻又是亮色时，视图根写 `data-look="swiss"`（`themeLook()`），`shell.module.css` 在这个开关下把 `--wl-round`（所有圆角的乘数，各 CSS 里写 `calc(Npx * var(--wl-round, 1))`；圆点的 50% 不乘）归零、去掉阴影、换 Helvetica 系字体；分组小标题的大写加粗写在各模块里的 `:global([data-look="swiss"]) .x`。新写圆角用 `calc(Npx * var(--wl-round, 1))`，别写死 px，否则瑞士下会漏圆角。瑞士下的其余做法同样写在各模块末尾的 `:global([data-look="swiss"]) .x` 里：成组列表只留上下线、输入框只留底线、分段反白、分组标题压黑线；淡底（`--wl-*-soft`）在 `shell.module.css` 的瑞士块里调淡。新加成组的盒子或卡片时顺手补一条。
- 视图根节点把选中的两套写成 `--wl-l-*`、`--wl-d-*` 两组内联变量，`shell.module.css` 用 `light-dark()` 跟着宿主的 `color-scheme` 拼成 `--wl-*`；CSS 里不再写死色值。
- 选择记在浏览器（`workflow-lite.theme`），在工作流中心「设置」页的「外观」里换，点了就生效，不走插件设置的保存。
- 一套主题是一整套颜色：线色（光带、图例、线上牌子跟着它）、强调色、步骤图标色、状态色一起换；非默认的几套照着成熟配色方案配（雾蓝 Nord、林间 Everforest、墨黑 Geist、深海 Tokyo Night、暖夜 Gruvbox），主干线用强调色。
- 改色或加主题要过 `tests/client/themes.spec.ts`：文字、语义色、线色、步骤色在面板上过 4.5:1，线色在画布上过 3:1，七种线两两 OKLab ΔE ≥ 8（默认两套 ≥ 13）。

## 浏览器验收（CDP）

`tests/cdp-*.mjs` 在真浏览器里走一遍功能，结论以磁盘 / RPC 为准。需要：

1. 一个装了本插件的 DSH 网页实例（默认 `http://127.0.0.1:3190`），从它打印的 URL 里取 `?token=`；
2. 一个带 DevTools 端口的 Chrome：`node tests/lib/cdp-chrome-launch.mjs 9222`。

```bash
DSH_WEB_TOKEN=<token> DSH_BASE=http://127.0.0.1:3190 node --experimental-strip-types tests/cdp-ui.mjs
```

`DSH_BASE` 是基址（`tests/lib/web-session.mjs` 每次现读环境变量，不固化成模块常量——截图管线是 import 之后才起实例、才知道端口）；`DSH_AUTH_COOKIE` 可以跳过 token 换 cookie 那一步。

| 脚本 | 覆盖 |
|---|---|
| `cdp-ui.mjs` | 画布编辑、资源、工作流切换 / 改名 / 删除 |
| `cdp-runs.mjs` | 实例与运行状态 |
| `cdp-inputs.mjs` | 执行前提问 |
| `cdp-versions.mjs` | 版本、工作流中心「工作流」页 |
| `cdp-config.mjs` | 工作流中心「设置」页（含外观：配色主题） |
| `cdp-pack.mjs` | 工作流中心「工作流」页的导出、导入（另存为新名字、并入已有的）、导出成 Skill（预览整个包、下载 zip、装到本机；装的那份用一次性名字，验完删掉） |

## 插件图标与词标

- 两张 448 见方的图分工不同，**别互相替换**（`tests/package.spec.ts` 钉着这个分工）：
  - `assets/icon.webp` = **插件图标**，`package.json` 顶层 `icon` 指它。**先裁到主体再缩**：脸占画面四成、头加头发占八成，标志物（黑猫发卡）完整，下缘留一点衣领和花束当锚点；裁框 950 见方、左上角 `(115, 0)`。整幅缩进去脸只剩三成，缩略图上认不出人——取景参照隔壁 tool-plus 的头像。
  - `assets/icon-full.webp` = **README 头图**，两版开头的 `<img width="140">` 指它。整幅不裁，保留完整构图。
- 两张都：LANCZOS 缩到 448、圆角半径 104、8 倍超采样（`rounded_rectangle` 画在 448×8 上再缩）、WebP 无损（`lossless=True, quality=100, method=6`），都在 `files` 里。
- 宿主只收**清单目录下的相对路径**的图标，格式 SVG / PNG / JPEG / WebP，上限 256 KiB；越界界面静默回退默认图。`tests/package.spec.ts` 的「插件图标」盯着：路径合法且在包内、不超过 256 KiB、本体与扩展名对得上、`files` 收得下。
- `assets/icon-source.png`（最初的源图）、`assets/icon-art.png`（重绘成赛璐璐平涂，1254 见方）只留在仓库里做参照，不随包发布。
- `assets/wordmark.png`：「dsh workflow lite」艺术字，Jua 400 Regular 逐词纯色，1147×130——蓝 `#3e7ce4`（项目色）、绿 `#60ba7e`（人物眼睛）、金 `#cd9e70`（金发压深）。README 里按 `width="360"` 摆，图标 140。
- 词标重做：Pillow 把三个词按 4 倍尺寸画到同一基线上，整幅 LANCZOS 缩到 1/4，裁到墨迹外留 6% 边距；字体不进仓库，用时从 Google Fonts 取 `ofl/jua/Jua-Regular.ttf`。
- README 两版开头都是「图标 + 词标」居中的一段 HTML，没有 H1；改文案两版同步。

## README 版本线

README 里、封面图下面的「版本线」是一张图（`assets/versions.{zh,en}.png`），挑重要变化，一条一站，不逐版本列。

- 内容在 `tests/readme/versions.json`：每站写落在哪个版本（要显示成别的就写 `label`，改过几个版本的写 `to`）、标题、两行短话（中英各一份），标题旁要挂「实验性」这类小签写 `tag`（英文要短，`Beta`，长了会折到下一行）；还没发的写 `"next"`，还没定版本的规划写 `"planned"`（排在最后，显示成 `later`，不核对 CHANGELOG）。排版在 `tests/readme/versions.html`。
- 出图：`pnpm run timeline`（要一个带 DevTools 端口的 Chrome：`node tests/lib/cdp-chrome-launch.mjs 9222`，不用起 dsh）。脚本对照 `CHANGELOG.md`，json 里的版本不存在、或有新的小版本没写进 json 就报错。
- 发版时：把 `"next"` 改成这个版本号（有新的重要变化就加一站），重跑，新图一起提交。
- 6 站时一栏约 150px：标题 ≤ 7 个汉字 / 15 个英文字符（带小签的再短些），短话每行 ≤ 9 个汉字 / 22 个英文字符；站数 6 站左右，多了就把早期几个版本合成一站（写 `to`）；出图后看一眼两张图。

## README 封面图

README 有中英两版：`README.md`（中文，默认）与 `README.en.md`（英文），开头互相链接。各用一张封面图：`assets/hero.zh.png`、`assets/hero.en.png`（2 倍像素），由同一页 HTML 按语言排版后截图得到，图里的界面都是真实截图（英文版用英文界面和英文示例）。

| 文件 | 作用 |
|---|---|
| `tests/support/shots-env.mjs` | 封面管线的**自备环境**：起一个假模型（`tests/support/mock-llm.mjs`）、生成截图专用 profile `workflow-lite-shots`、在**随机端口**起实例、换好 cookie，再备出那条固定标题的夹具会话；`startShotsEnv()` 一把起完，拆的时候等实例真的关掉 |
| `tests/readme-shots.mjs` | 自己把上面那套环境起起来，按语言切换浏览器语言，建示例工作流和一个实例，按封面里的大小拍素材到 `tests/runs/hero/<lang>/`（`canvas.png` 编辑页、`run-graph.png` 点亮的图、`run-position.png` / `run-timeline.png` 右栏两小块、`run-dashboard.png` 查看框里打开的 HTML 看板、`plan.txt` 编译出的计划），拍完自动清理并拆掉实例；`node … tests/readme-shots.mjs en` 只拍一种 |
| `tests/readme/hero.html` | 封面排版：标题、三段流程（设计流程 → 生成计划提示词 → 跟踪执行进度）、软工作流说明、功能标签；`?lang=zh` / `?lang=en` 切换，文案都在页尾的 `TEXT` 里 |
| `tests/readme-hero.mjs` | 在 Chrome 里另开一个标签页按 1040 宽渲染 `hero.html`，截成 `assets/hero.<lang>.png`；`node tests/readme-hero.mjs en` 只出一种 |

重做步骤：

```bash
# 界面有变化：重拍素材并出图。前置是一台装了 dsh 的机器 + 一个带 DevTools 端口的 Chrome
# （`node tests/lib/cdp-chrome-launch.mjs 9222`），不用先起 dsh web、不用给 token
pnpm run hero

# 只改了 hero.html 的文案或排版：直接出图（素材已在 tests/runs/hero/）
pnpm run hero:render
```

封面管线的前提：

- **自己一个家**：整跑用 `tests/runs/shots-home` 当 `DSH_HOME`，profile、会话库、工作区库、投影缓存全在它底下，跑前清空、跑完删掉。这几样在 DSH 里按 `DSH_HOME` 算、**不按 profile 隔离**——共用用户自己的家的话，那条"封面夹具"会永久留在他侧栏里。
- **不依赖真实环境数据**：封面拍的是脚本自己准备的那份数据（假模型 + 生成 profile + 随机端口 + 夹具会话）；不发真实模型请求，也不受"当时 3190 上跑着什么"影响。
- **夹具是"认领"出来的**：会话按固定 id 复用，而会话跟着它的 cwd 走（换个目录再建会被 host 回 `session/conflict`，host 又没有删会话的接口）。所以 `prepareShotsFixture` 找到就用它自己的 cwd 当工作区，并把 `adoptedWorkspace` 交出来——**收尾时不许删认领来的目录**。
- **进会话靠搜索，不靠分组展开**：工作区分组的展开状态记在浏览器里（`dsh.workspace.view.v5` 的 `groupExpansion`），按 origin 存——换个端口就是新分组状态，于是所有分组都是折叠的，会话行压根不在 DOM 里。`enterSession` 因此走会话搜索框。
- **关实例要等它真的关掉**：随手 `spawn('taskkill')` 就走人，调用方一退出 taskkill 没机会跑，实例留在后台占着会话写句柄，下一跑撞 `session/writer-held`。所以 `stop()` 等 `close`，等不到**就抛**（不许报成功）。
- **"没发真实模型请求"有判据**：夹具发 prompt 的那一轮，必须等到**假模型真的收到请求**才算数。别拿 `blank` 变 false 当完成信号——它只说明消息记下了，模型调用还在后面，拿它当信号会抢跑报假红。

改封面时注意：

- 计划节选是从 `tests/runs/hero/zh/plan.txt` 里摘的原文（计划只有中文，英文版是对应的译文，图里标了 translated），编译器的措辞变了要同步改；
- 两种语言的文案改一处就要改另一处；
- 文案用平实的陈述句，不用「不是……而是……」这类对比句式，也不用过于口语的说法；
- 清晰：素材拍的时候就按封面里的大小换算像素比，`hero.html` 一律按「原图宽 ÷ 2」摆，一个像素对一个像素，不在封面里缩放（缩放就糊）；右栏这类窄面板单独裁出来，别整块界面缩小塞进封面；
- 字要大：封面 1040 宽，GitHub 上大约显示成 880 宽；示例工作流只留四步，图越窄字越大。改页宽、页边要同步改 `readme-shots.mjs` 的 `GRAPH_WIDTH`；
- 层次：工作流的图是主角；右栏小块和看板按图相对原大的比例拍（不小于 `MIN_SCALE` 0.85），字号和图上的字接近，别比图还抢眼；
- 不留空：第三段下半截左列两小块叠起来，右边看板的高度按左列算好再拍，两边齐平；
- 出图后打开 `assets/hero.zh.png`、`assets/hero.en.png` 看一眼，确认没有错位、截断，也没有拍进本机路径或私人工作区名。
