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

17 KiB
Raw Permalink Blame History

Subagent 派活套件 (Dispatch Kit)

orchestrator(主会话)按 PLAN.md §3 波次 派活时,复制下面对应任务的 prompt。 角色/通用规则已写在 .claude/agents/(module-builder / module-reviewer)的定义里, 此处只给每个任务的变量部分model / isolationAgent(...) 调用参数里设(见 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。