Files
web-terminal/docs/DISPATCH.md
Yaojia Wang 409b208928 build: adopt esbuild for frontend bundling (build:web → public/build/)
- esbuild dev dep; build:web/dev:web bundle public/main.ts → public/build/main.js
- gitignore public/build/; tsconfig.web.json now typecheck-only
- frontend build convention documented in PLAN §1, DISPATCH, ARCHITECTURE §5
- resolves the T1 deferred decision
2026-06-16 07:36:01 +02:00

263 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Subagent 派活套件 (Dispatch Kit)
> orchestrator(主会话)按 [PLAN.md §3 波次](./PLAN.md) 派活时,复制下面对应任务的 prompt。
> 角色/通用规则已写在 [`.claude/agents/`](../.claude/agents)(`module-builder` / `module-reviewer`)的定义里,
> 此处只给**每个任务的变量部分**。`model` / `isolation` 在 `Agent(...)` 调用参数里设(见 PLAN §4),不写进 prompt。
## 使用方法
1. 选定本波要派的任务(同波、文件不交叉,单批 ~35 个)。
2. 对每个任务调用:`Agent(subagent_type:"module-builder"|"module-reviewer", model:<§4>, isolation:<§4>, prompt:<下面对应块>)`
3. **worktree 隔离的 builder 派出前,先把上一波 commit**(否则隔离工作树里没有 `types.ts`)。
4. 每个 agent 返回后:orchestrator 把它交回的日志条目**追加进 `PROGRESS_LOG.md`**,更新"当前焦点",再派下一波。
5. agent 返回 `[!] BLOCKED` → orchestrator 解歧义(必要时问用户)→ 重新派活。
---
## 模板 A · module-builder(实现任务)
```
执行 web-terminal 项目的 PLAN 任务 <ID>(<一句话标题>)。
先读(你的上下文是空白的):CLAUDE.md 的 "Development Workflow";docs/PLAN.md 里的任务 <ID>
(看清 Owns/Depends/Steps/Accept);docs/ARCHITECTURE.md 的 <ARCH §>;并在 ARCHITECTURE.md 内
搜索这些锚点逐条照做:<M*/L* 列表或"无">;背景看 docs/TECH_DOC.md <§>。
从 src/types.ts import 共享类型,**不要**本地另立类型。
硬边界 —— 只能创建/修改这些文件:<Owns 列表>。其它一律不碰(尤其 src/types.ts 和别的模块)。
TDD:先写覆盖每个 Step + Accept 的失败测试 → 实现到全绿 → 运行 <Accept 命令> 确认。
本任务最易错的点:<task-specific 要点>。
文档若留有真歧义 → 停下返回 [!] BLOCKED,**绝不猜**。不要写 docs/PROGRESS_LOG.md。
最终消息以可粘贴的日志条目块结尾(状态/改动/验证/决策偏离/遗留/阻塞/commit)。
```
## 模板 B · module-reviewer(只读验收/复查)
```
复查/验收 web-terminal 项目的 <目标>。只读,不改任何源码。
先读:docs/ARCHITECTURE.md 的 <§> + §8 不变量清单 + 锚点 <M*/L*>;docs/PLAN.md 任务 <ID> 的
Steps/Accept;docs/TECH_DOC.md <§>。
按优先级核:不变量 → 相关 M*/L* 修订是否真落地 → Accept 与 Steps 覆盖 → 编码风格(不可变/小文件/
错误处理);对抗性地找能打破它的输入/时序/边界。只跑只读或测试命令(vitest run / git diff / grep)。
最终给 findings 报告:verdict PASS / CHANGES NEEDED;每条 finding 标 severity + file:区域 +
违反了哪条不变量/M*/Accept + **路由给哪个 owning task** 去修。无真问题就判 PASS,不要凑数。
```
---
## W0 · 基础(串行,主会话直接做或单个 builder)
### T1 — 脚手架 + 装齐依赖 · `Agent(module-builder, model:"sonnet")`
```
执行 PLAN 任务 T1(脚手架 + 一次性装齐依赖)。先读 CLAUDE.md 工作流、PLAN 任务 T1、ARCHITECTURE §6(S1)。
硬边界 —— 只创建:package.json、tsconfig.json、tsconfig.web.json、.gitignore、vitest.config.ts、空目录骨架。
要点:**一次性**声明全部依赖(运行时 express ws node-pty @xterm/xterm @xterm/addon-fit;开发 typescript
tsx vitest @types/node @types/ws @types/express),避免后续并发 npm install 冲突;后端 tsconfig(NodeNext/strict)
+ 前端 tsconfig.web(lib DOM);.gitignore 含 node_modules/dist;**验证 node-pty 原生编译通过**
(npm install 无错 + node -e "require('node-pty')")。Accept:npm install 成功、npm test 能跑、node-pty 可 require。
歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
### T2 — 冻结共享契约 · `Agent(module-builder, model:"sonnet")`
```
执行 PLAN 任务 T2(共享类型契约 src/types.ts)。先读 PLAN 任务 T2、ARCHITECTURE §3 全部接口定义。
硬边界 —— 只创建:src/types.ts(全局唯一契约源,完成即冻结)。
要点:逐个从 ARCHITECTURE §3 誊写为可编译 TS:Config(含 maxPayloadBytes、allowedOrigins)、
ClientMessage/ServerMessage/ParseResult、Dims、SessionMeta、Session(含 lastOutputAt/exitedAt/exitCode)、
RingBuffer、IPty 子集(pid,cols,rows,onData,onExit,write,resize,kill)、前端 mountKeybar(onSend) 类型;
各模块对外函数签名以注释列出做 import 锚点。Accept:tsc --noEmit 通过,纯类型无实现。
歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
### T3 — mock IPty · `Agent(module-builder, model:"haiku", isolation:"worktree")`
```
执行 PLAN 任务 T3(mock IPty 测试替身)。先读 PLAN 任务 T3、ARCHITECTURE §7,从 src/types.ts import IPty。
硬边界 —— 只创建:test/helpers/mock-pty.ts。
要点:createMockPty() 实现 IPty,可手动触发 emitData(s)/emitExit(code);记录收到的 write/resize/kill 供断言。
Accept:自带 smoke 测试(emitData 后监听者收到、kill 被记录)。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
> **派 W1 前:`git add -A && git commit` W0 产物**,worktree builder 才能看到 types.ts。
---
## W1 · 叶子模块(并行,单批 ~35 个)
### T4 — config · `Agent(module-builder, model:"sonnet", isolation:"worktree")`
```
执行 PLAN 任务 T4(config.ts)。先读 PLAN 任务 T4、ARCHITECTURE §3.1,并在 ARCHITECTURE.md 搜 "M1" 照做;
从 src/types.ts import Config。硬边界 —— 只创建:src/config.ts、test/config.test.ts。
要点(M1):loadConfig 读 env 给默认值;**allowedOrigins 由 os.networkInterfaces() 各网卡 IPv4 + localhost +
可选主机名推导(绝不从 bindHost,结果不含 0.0.0.0)**,再并入 ALLOWED_ORIGINS;非法值 fail-fast 抛错;
返回对象 Object.freeze。测试须断言 allowedOrigins 不含 0.0.0.0、含注入的假网卡 IP(mock networkInterfaces)。
Accept:vitest run config 全绿。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
### T5 — protocol · `Agent(module-builder, model:"sonnet", isolation:"worktree")`
```
执行 PLAN 任务 T5(protocol.ts)。先读 PLAN 任务 T5、ARCHITECTURE §3.2,搜 "M7" 照做;背景 TECH_DOC §4;
从 src/types.ts import ClientMessage/ServerMessage/ParseResult。硬边界 —— 只创建:src/protocol.ts、test/protocol.test.ts。
要点:export SESSION_ID_RE(UUID v4);parseClientMessage **永不抛异常**,错误全走 ParseResult.ok=false;
校验 JSON→type 白名单→resize cols/rows 11000 整数→input.data 必为 string 原样→attach.sessionId 为 null 或
匹配 SESSION_ID_RE(M7:abc123 必须被拒、合法 UUID 通过);serialize 三种 server 消息。
Accept:vitest run protocol 全绿、模糊输入不抛。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
### T6 — ring-buffer · `Agent(module-builder, model:"opus", isolation:"worktree")`
```
执行 PLAN 任务 T6(ring-buffer.ts)。先读 PLAN 任务 T6、ARCHITECTURE §3.4 的 RingBuffer 段,**重点搜 "M2" 全文照做**;
从 src/types.ts import RingBuffer。硬边界 —— 只创建:src/session/ring-buffer.ts、test/ring-buffer.test.ts。
要点(M2,最易错):内部用 Buffer('utf8')按**真实字节**计量(非 string.length);超容量时**按 chunk 边界整体淘汰
最旧块**,绝不在多字节 UTF-8 码点或 ANSI 转义序列中间截断;snapshot() 开头补软复位 \x1b[0m。
测试须覆盖:容量内全量回放、超容量淘汰、多字节中文不被截断、ANSI 序列不被切断、按字节计量。
Accept:vitest run ring-buffer 全绿。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
### T7 — origin · `Agent(module-builder, model:"sonnet", isolation:"worktree")`
```
执行 PLAN 任务 T7(http/origin.ts)。先读 PLAN 任务 T7、ARCHITECTURE §3.3、TECH_DOC §7(CSWSH)。
硬边界 —— 只创建:src/http/origin.ts、test/origin.test.ts。
要点:isOriginAllowed(origin, allowed) **host+port 双匹配**;undefined Origin 默认拒绝(集中决定,不散落 if)。
测试:白名单命中放行、域名/端口不符拒绝、undefined 拒绝、evil.com 拒绝。
Accept:vitest run origin 全绿。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
### T8 — index.html · `Agent(module-builder, model:"haiku", isolation:"worktree")`
```
执行 PLAN 任务 T8(public/index.html)。先读 PLAN 任务 T8、ARCHITECTURE §5。
硬边界 —— 只创建:public/index.html。要点:终端容器 #term、键栏挂载点 #keybar、移动端 viewport meta;
**按 PLAN §1 前端构建约定**用 `<script type="module" src="./build/main.js">` 加载(及 `<link>` 引 ./build/main.css 与 style.css)。
Accept:浏览器打开无控制台报错。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
### T9 — style.css · `Agent(module-builder, model:"haiku", isolation:"worktree")`
```
执行 PLAN 任务 T9(public/style.css)。先读 PLAN 任务 T9、ARCHITECTURE §5。
硬边界 —— 只创建:public/style.css。要点:全屏终端布局、键栏固定底部且 >768px 隐藏、深色背景。
Accept:桌面/窄屏两种宽度布局正确。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
### T10 — keybar · `Agent(module-builder, model:"haiku", isolation:"worktree")`
```
执行 PLAN 任务 T10(public/keybar.ts)。先读 PLAN 任务 T10、ARCHITECTURE §6.3,从 src/types.ts import mountKeybar 类型。
硬边界 —— 只创建:public/keybar.ts、test/keybar.test.ts。
要点:按键→字节表(Esc \x1b、Shift+Tab \x1b[Z、↑↓←→、Enter \r(不是 \n)、Ctrl+C \x03、Tab \t);
mountKeybar(onSend) 在 touchstart 直接 onSend(bytes) 并 preventDefault 防软键盘;字节表导出为纯数据便于单测。
Accept:vitest run keybar 全绿。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
### T11 — main.ts · `Agent(module-builder, model:"sonnet", isolation:"worktree")`
```
执行 PLAN 任务 T11(public/main.ts)。先读 PLAN 任务 T11、ARCHITECTURE §5/§6,搜 "M6" 照做;背景 TECH_DOC §6/§9。
依赖 src/types.ts 的 mountKeybar 签名(仅签名,可并行)。硬边界 —— 只创建:public/main.ts。本文件是 **esbuild 打包入口**(PLAN §1 约定)。
要点:xterm + FitAddon + `import '@xterm/xterm/css/xterm.css'`,fit() 必须在容器有真实尺寸后调用(display:none 得 NaN);
**WS scheme 随页面协议 https→wss(M6)**,URL …/term,首帧发 attach(localStorage 取 sessionId);
term.onData→ws.send {input};onmessage 按 type 处理(attached 存 id、output→term.write、exit→提示重连);
ResizeObserver→fit→发 resize(防抖 100ms);断线指数退避重连 1/2/4…≤30s 携带 sessionId;mountKeybar(d=>ws.send)。
Accept:tsc -p tsconfig.web.json 通过(行为留 W5 联调)。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
> 派 W2 前:commit W1 产物。
---
## W2 / W3 / W4 · 会话与集成(基本串行)
### T12 — session · `Agent(module-builder, model:"opus")`
```
执行 PLAN 任务 T12(session/session.ts)。先读 PLAN 任务 T12、ARCHITECTURE §3.4,逐条搜 "M4" "M5" "L1" "L4" 照做;
从 src/types.ts import Session/SessionMeta/IPty,用 src/session/ring-buffer.ts 的 createRingBuffer、
test/helpers/mock-pty.ts 测试。硬边界 —— 只创建:src/session/session.ts、test/session.test.ts。
要点:createSession(cfg,dims,now,onExit) 接 onData(append+刷新 lastOutputAt+转发 attachedWs)、
接 onExit(置 exitedAt/exitCode→有 ws 则发 exit→调注入 onExit);**spawn 失败不吞,向上抛(M4)**;
attachWs 先重置 attachedWs 指针再回放 snapshot 再续流、返回被踢旧 ws;detachWs 置 detachedAt **不杀 PTY**;
writeInput/resize 在 exitedAt!=null 时直接 return(L4)、resize 幂等;send 前查 readyState(M5)。
测试(mock IPty):detach 不杀 PTY、attach 回放、后到踢前者、detach 后 emitExit 保留会话、已退出 PTY 的 write/resize 被忽略。
Accept:vitest run session 全绿。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
### T13 — manager · `Agent(module-builder, model:"sonnet")`
```
执行 PLAN 任务 T13(session/manager.ts)。先读 PLAN 任务 T13、ARCHITECTURE §3.5,逐条搜 "M3" "M4" "L1" "L2" 照做;
依赖 src/session/session.ts。硬边界 —— 只创建:src/session/manager.ts、test/manager.test.ts。
要点:handleAttach:null→新建;命中存活→attachWs;**命中已退出→回放+补发 exit 后移除(L1)**;查不到→新建返回新 id;
createSession 抛错向上抛(M4)。reapIdle:**now - max(detachedAt,lastOutputAt) > idleTtl 才回收(M3)**。
shutdown kill 全部。注入给 createSession 的 onExit 从会话表删除(L2)。
测试(mock IPty):三种 attach 路径、reapIdle 有新输出不回收/超时无输出回收、shutdown 全 kill。
Accept:vitest run manager 全绿。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
### T14 — server 接线 · `Agent(module-builder, model:"sonnet")`
```
执行 PLAN 任务 T14(server.ts)。先读 PLAN 任务 T14、ARCHITECTURE §3.6,逐条搜 "M4" "M5" "L3" "L5" 照做;
用 config/protocol/origin/manager。硬边界 —— 只创建:src/server.ts。
要点:Express 静态托管 public/(含 esbuild 产物 public/build/);**WebSocketServer({noServer:true})**;HTTP upgrade:非 /term→destroy、Origin 不过→
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')+destroy、通过→handleUpgrade→emit connection(L3);
connection 等首帧 attach→try handleAttach 回 attached,**catch→send exit(-1,reason)+close 仅此连接,不 fail-fast(M4)**;
message:parse,ok:false 丢弃记日志,否则路由 writeInput/resize;close→detach;SIGINT/SIGTERM→shutdown;
**ws.send 前查 OPEN(M5)**;ws.maxPayload=cfg.maxPayloadBytes(L5);/term 路径常量入 config。
Accept:本机浏览器连通拿到 shell、坏 Origin 被 401(自动化留 T15)。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
### T15 — 集成/E2E · `Agent(module-builder, model:"sonnet")`
```
执行 PLAN 任务 T15(集成/E2E 测试)。先读 PLAN 任务 T15、ARCHITECTURE §7,搜 "M4" "M2" "L3" "L5";依赖 T14。
硬边界 —— 只创建:test/integration/*.test.ts。
要点(起真实 WS):attach→attached→output 时序;reconnect 回放并断言 ANSI/中文不乱(F5/F6);
spawn 失败→exit(-1)+仅关连接(M4);坏 Origin/非 /term→拒(F9/L3);超 maxPayload 帧被拒(L5)。
Accept:vitest run integration 全绿。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
> 派 W5 前:commit W4 产物。
---
## W5 · 验收(只读,reviewer)+ 收尾(orchestrator)
> 验收 agent **不改源码**;发现问题标 severity + owning task,修复**派回该模块的 module-builder**。
### T16 — 验收 F1F3 · `Agent(module-reviewer, model:"sonnet")`
```
验收 web-terminal 的 F1F3(终端基础/TUI/resize),对照 TECH_DOC §8。只读不改源码。
核:ls/补全/Ctrl+C/方向键历史(F1);vim/top/claude 颜色与重绘(F2);拖窗口行列实时调整、TUI 不错位(F3)。
给 findings 报告:verdict + 每条 severity + 现象 + 违反的 Accept/不变量 + owning task。无问题判 PASS。
```
### T17 — 验收 F4 + F9 · `Agent(module-reviewer, model:"sonnet")`
```
验收 F4(局域网访问)+ F9(Origin)。先读 ARCHITECTURE §3.1/§3.3 + 搜 "M1"、TECH_DOC §7。只读不改源码。
核:手机/他机 http://<IP>:3000 可用(F4,**重点确认 allowedOrigins 含本机网卡 IP**);跨源 WS 握手被 401、非 /term 被拒(F9/L3)。
给 findings 报告(severity + owning task)。无问题判 PASS。
```
### T18 — 验收 F5 + F6 · `Agent(module-reviewer, model:"sonnet")`
```
验收 F5(会话保活)+ F6(重连回放)。先读 ARCHITECTURE §3.4/§3.5 + 搜 "M2" "L1"、TECH_DOC §5.2。只读不改源码。
核:发长任务→刷新/换设备→仍在跑、内容不丢(F5);离线期输出重连回放、**ANSI/中文不乱码(F6/M2)**;detach 后退出仍见末屏+exit(L1)。
给 findings 报告(severity + owning task)。无问题判 PASS。
```
### T19 — 验收 F7 · `Agent(module-reviewer, model:"sonnet")`
```
验收 F7(移动快捷键栏)。先读 ARCHITECTURE §6.3。只读不改源码。
核:真机按 Esc/Shift+Tab/方向/Enter/Ctrl+C 生效、不弹软键盘。给 findings 报告(severity + owning task)。无问题判 PASS。
```
### T20 — 验收 F8 · `Agent(module-reviewer, model:"sonnet")`
```
验收 F8(进程退出处理)。先读 ARCHITECTURE §3.4/§3.5 + 搜 "L1"、TECH_DOC §8。只读不改源码。
核:shell 退出提示重连、回车开新会话;**detach 后退出**回来仍见末屏+exit(L1)。给 findings 报告(severity + owning task)。无问题判 PASS。
```
### T21 — 安全核对 + README + LOG 收尾 · **orchestrator 自做**(涉及写 LOG/README,不派并行 subagent)
- 可先派 `module-reviewer` 做只读安全核对:
```
对照 TECH_DOC §7 风险表逐条核 web-terminal 的安全实现(Origin 校验、明文嗅探提示、严禁公网、maxPayload/背压)。
只读不改源码。给 findings 报告(severity + owning task)。无问题判 PASS。
```
- orchestrator 据报告写 README(npm install/start/test、查 IP、Tailscale 部署、安全警告)、标 LOG v0.1 完成、确认 npm run build 产物可 npm start。