# Web Terminal 实施计划 (Phased Plan)

> 版本: v0.1 · 定位: 把 [ARCHITECTURE.md](./ARCHITECTURE.md) 的契约拆成**细粒度、低耦合**的任务,
> 供**多 agent 并行开发**。完成情况记录在 [PROGRESS_LOG.md](./PROGRESS_LOG.md)。
> 工作流约束见 [CLAUDE.md](../CLAUDE.md)(查 PLAN → 做子任务(TDD) → 验证 → 更新 LOG)。

---

## 0. 多 Agent 并行规则(开工前必读)

并行的三条铁律,违反会导致 agent 互相踩踏:

1. **文件所有权独占**:每个任务标注 `Owns:`,只有该任务可创建/修改这些文件。
   `src/types.ts`(共享契约)由 **T2 独占并冻结**,其余任务**只读** import;若需新增类型,
   先回到 T2 改契约(协调点),不得各自在自己文件里另立类型。
2. **只依赖接口,不依赖实现**:任务间通过 `src/types.ts` 的 interface / 函数签名解耦。
   `Depends:` 仅表示"需要对方的**接口或产物**",多数情况接口在 W0 就已就绪,故可与"实现方"并行编码,
   仅在**集成/测试时**才真正汇合。
3. **LOG 由 orchestrator 独写,subagent 不碰**(G1):`docs/PROGRESS_LOG.md` 是共享文件,
   **不在任何任务的 `Owns:` 里**。被派的 subagent **不要写 LOG**,而是把日志条目作为**最终返回结果**交回;
   由主会话(orchestrator)在每批 agent 返回后统一追加。详见 [CLAUDE.md](../CLAUDE.md)。

每个任务自带 TDD(测试与实现同属一个 agent、同一组文件),所以**不要**把"写测试"和"写实现"
拆给不同 agent —— 那会制造同文件冲突。任务内的 `Steps:` 清单就是细粒度拆分。

**执行用的项目 agent**(见 [`.claude/agents/`](../.claude/agents)):`module-builder`(TDD 实现单个任务)、
`module-reviewer`(只读复查/验收)。orchestrator = 主会话,按 §3 波次派活、按 §4 选模型/隔离。
subagent **不能问用户、不能互相通信**:遇到文档未定义的歧义 → 停下返回 `[!] BLOCKED`,**绝不猜**。

### 参考文档

**必读文档清单、`ARCH:` 字段用法、`M*/L*` 代号含义 —— 见 [CLAUDE.md → Development Workflow](../CLAUDE.md)。**
该规则对所有任务(尤其上下文隔离的子 agent)统一生效,此处不重复,以 CLAUDE.md 为准。
要点:每个任务读其 `ARCH:` 指向的章节 + Steps 里的 `M*/L*` 锚点(在 ARCHITECTURE.md 内搜同名代号)。

---

## 1. 依赖关系与并行波次 (Waves)

```
W0 基础(串行, 阻塞全部)
  T1 脚手架+装齐依赖 → T2 共享契约 types.ts → T3 mock IPty
        │
        ▼
W1 叶子模块(全部并行, 互不依赖 —— 并行度最高)
  后端纯函数: T4 config · T5 protocol · T6 ring-buffer · T7 origin
  前端(独立于后端整条链): T8 index.html · T9 style.css · T10 keybar · T11 main
        │
        ▼
W2  T12 session(依赖 ring-buffer 实现 + mock IPty)
        │
        ▼
W3  T13 manager(依赖 session)
        │
        ▼
W4 集成(汇合点)  T14 server 接线 · T15 集成/E2E 测试
        │
        ▼
W5 验收与收尾(按 F# 并行)  T16–T20 F1–F9 实测 · T21 安全+README+LOG 收尾
```

**关键并行事实**:前端 T8–T11 不依赖任何后端模块(只依赖 §4 线协议形状,已在 types.ts),
可与 W1–W4 整条后端链**全程并行**,直到 W5 才与运行中的 server 汇合实测。

> ARCH §6 的 S1–S8 是线性视角;本计划是其**并行重排**。每个任务标了 `ARCH:` 做溯源。

**前端构建约定(T1 已定,T8/T11/T14 遵守)**:打包器 = **esbuild**。
- 入口 `public/main.ts`;`npm run build:web` → 打包到 **`public/build/main.js`**(若 main.ts `import '@xterm/xterm/css/xterm.css'`,
  esbuild 同时出 `public/build/main.css`)。开发用 `npm run dev:web`(--watch)。`public/build/` 已 gitignore。
- `index.html` 以 `<script type="module" src="./build/main.js">` 加载,并 `<link rel="stylesheet" href="./build/main.css">`(若有)+ 自己的 `style.css`。
- `server.ts` 静态托管整个 `public/`(含 `public/build/`)。`tsconfig.web.json` 仅类型检查(noEmit)。

---

## 2. 任务清单

> 状态图例同 LOG:`[ ]` TODO · `[~]` 进行中 · `[x]` 完成 · `[!]` 受阻。
> 本文件只定义任务;勾选与详细记录由 **orchestrator** 写入 PROGRESS_LOG.md(subagent 只返回条目,见 §0 铁律 3)。
> 每个任务的建议**模型 / 隔离**见 §4。

### W0 · 基础(串行)

#### T1 · 脚手架 + 一次性装齐依赖 `[ ]`
- **Wave/ARCH**: W0 / S1 · **Owns**: `package.json`、`tsconfig.json`、`tsconfig.web.json`、`.gitignore`、`vitest.config.ts`、目录骨架
- **Depends**: 无 · **Parallel-safe**: 无(必须最先)
- **Steps**:
  - [ ] `npm init`;**一次性**声明全部依赖(避免后续并发 `npm install` 改 package.json 冲突):
        运行时 `express ws node-pty @xterm/xterm @xterm/addon-fit`;开发 `typescript tsx vitest @types/node @types/ws @types/express`
  - [ ] 后端 `tsconfig.json`(target ES2022, module NodeNext, strict, outDir dist)+ 前端 `tsconfig.web.json`(lib DOM, 给 public/)
  - [ ] scripts:`start`(tsx src/server.ts)、`build`(tsc)、`test`(vitest run)、`test:watch`、`build:web`
  - [ ] 建空目录:`src/`、`src/http/`、`src/session/`、`public/`、`test/`、`test/helpers/`
  - [ ] 验证 **node-pty 原生编译通过**(macOS 需 Xcode CLT):`npm install` 无错 + `node -e "require('node-pty')"`
- **Accept**: `npm install` 成功、`npm test` 能跑(0 测试也算通过)、node-pty 可 require

#### T2 · 共享类型契约 `src/types.ts` `[ ]`
- **Wave/ARCH**: W0 / §3 · **Owns**: `src/types.ts` (**全局唯一契约源,完成后冻结**)
- **Depends**: T1 · **Parallel-safe**: 无(它是所有人的依赖根)
- **Steps**(逐个从 ARCHITECTURE §3 誊写为可编译 TS):
  - [ ] `Config`(含 `maxPayloadBytes`、`allowedOrigins`;§3.1)
  - [ ] `ClientMessage` / `ServerMessage` / `ParseResult`(§3.2)
  - [ ] `Dims = { cols: number; rows: number }`
  - [ ] `SessionMeta` / `Session`(含 `lastOutputAt`/`exitedAt`/`exitCode`;§3.4)
  - [ ] `RingBuffer` interface(§3.4)
  - [ ] `IPty` 子集 interface(`pid, cols, rows, onData, onExit, write, resize, kill`)—— 供实现与 mock 共用
  - [ ] 前端契约:`mountKeybar(onSend: (data: string) => void): void` 的类型(供 T10/T11 解耦)
  - [ ] 各模块对外函数签名以 `declare`/注释列出(creatSession/attachWs/handleAttach/...),作为 import 锚点
- **Accept**: `tsc --noEmit` 通过;无实现、纯类型

#### T3 · mock IPty 测试替身 `[ ]`
- **Wave/ARCH**: W0 / §7 · **Owns**: `test/helpers/mock-pty.ts`
- **Depends**: T2(`IPty` 接口) · **Parallel-safe**: 与 W1 全部
- **Steps**:
  - [ ] `createMockPty()` 实现 `IPty`,可**手动触发** `emitData(s)` / `emitExit(code)`
  - [ ] 记录收到的 `write`/`resize`/`kill` 调用,供断言
- **Accept**: 自带 1 个 smoke 测试:emitData 后监听者收到、kill 被记录

---

### W1 · 叶子模块(全部并行)

#### T4 · `config.ts` `[ ]`
- **Wave/ARCH**: W1 / S2 §3.1 · **Owns**: `src/config.ts`、`test/config.test.ts`
- **Depends**: T2 · **Parallel-safe**: T5–T11
- **Steps**:
  - [ ] `loadConfig(env)`:读取 `PORT/SHELL_PATH/BIND_HOST/IDLE_TTL/SCROLLBACK_BYTES/MAX_PAYLOAD_BYTES/ALLOWED_ORIGINS`,缺失给默认值
  - [ ] **`allowedOrigins` 由 `os.networkInterfaces()` 各网卡 IPv4 + localhost + 可选主机名推导**(M1,**不**从 bindHost),`http(s)://<host>:<port>`,再并入 `ALLOWED_ORIGINS`
  - [ ] 非法值(端口越界、TTL 非数)→ 抛错 fail-fast
  - [ ] 返回对象 `Object.freeze`(不可变)
- **Steps(测试)**:
  - [ ] 默认值;各 env 覆盖;非法值抛错;allowedOrigins **不含 `0.0.0.0`**、含注入的假网卡 IP(mock networkInterfaces)
- **Accept**: `vitest run config` 全绿

#### T5 · `protocol.ts` `[ ]`
- **Wave/ARCH**: W1 / S3 §3.2 · **Owns**: `src/protocol.ts`、`test/protocol.test.ts`
- **Depends**: T2 · **Parallel-safe**: T4,T6–T11
- **Steps**:
  - [ ] `export const SESSION_ID_RE`(UUID v4)
  - [ ] `parseClientMessage(raw)`:JSON 守卫 → type 白名单 → `resize` cols/rows 1–1000 整数 → `input.data` 必为 string(原样) → `attach.sessionId` 为 `null` 或匹配 `SESSION_ID_RE`,否则 `{ok:false}`
  - [ ] **永不抛异常**,所有错误走 `ParseResult.ok=false`
  - [ ] `serialize(msg)` → JSON 文本
- **Steps(测试)**:
  - [ ] 三种合法 client 消息往返;每条非法分支(坏 JSON、未知 type、cols 越界/非整、data 非 string、sessionId=`abc123` 被拒、合法 UUID 通过);serialize 三种 server 消息
- **Accept**: `vitest run protocol` 全绿;模糊输入不抛

#### T6 · `ring-buffer.ts` `[ ]`
- **Wave/ARCH**: W1 / S3 §3.4 · **Owns**: `src/session/ring-buffer.ts`、`test/ring-buffer.test.ts`
- **Depends**: T2 · **Parallel-safe**: T4,T5,T7–T11
- **Steps**:
  - [ ] `createRingBuffer(maxBytes)`:内部以 `Buffer`('utf8')按**真实字节**计量(M2)
  - [ ] `append(chunk)`:超容量时**按 chunk 边界整体淘汰最旧块**(不切碎 UTF-8 码点 / ANSI 序列)
  - [ ] `snapshot()`:开头补软复位 `\x1b[0m` 兜底,再拼当前缓冲
- **Steps(测试)**:
  - [ ] 容量内全量回放;超容量淘汰最旧;**多字节中文不被截断**;**ANSI 序列不被从中间切断**;字节计量正确(非 string.length)
- **Accept**: `vitest run ring-buffer` 全绿

#### T7 · `http/origin.ts` `[ ]`
- **Wave/ARCH**: W1 / S4 §3.3 · **Owns**: `src/http/origin.ts`、`test/origin.test.ts`
- **Depends**: T2 · **Parallel-safe**: T4–T6,T8–T11
- **Steps**:
  - [ ] `isOriginAllowed(origin, allowed)`:host+port 双匹配;白名单命中 → true
  - [ ] `undefined` Origin 默认拒绝(集中决定,不散落 if)
- **Steps(测试)**:
  - [ ] 白名单命中放行;域名/端口不符拒绝;`undefined` 拒绝;evil.com 拒绝
- **Accept**: `vitest run origin` 全绿

#### T8 · `public/index.html` `[ ]`
- **Wave/ARCH**: W1 / S7 §5 · **Owns**: `public/index.html`
- **Depends**: 无(可在 T2 前都行) · **Parallel-safe**: 全部
- **Steps**: [ ] 终端容器 `#term`、键栏挂载点 `#keybar`;`<script type="module" src="./build/main.js">` + `style.css`(及 `./build/main.css` 若有);viewport meta(移动端)。**按§1 前端构建约定**
- **Accept**: 浏览器打开无控制台报错(静态)

#### T9 · `public/style.css` `[ ]`
- **Wave/ARCH**: W1 / S7 §5 · **Owns**: `public/style.css`
- **Depends**: 无 · **Parallel-safe**: 全部
- **Steps**: [ ] 全屏终端布局;键栏固定底部、`>768px` 隐藏;深色背景
- **Accept**: 桌面/窄屏两种宽度布局正确(静态目测)

#### T10 · `public/keybar.ts` `[ ]`
- **Wave/ARCH**: W1 / S7 §6.3 · **Owns**: `public/keybar.ts`、`test/keybar.test.ts`
- **Depends**: T2(`mountKeybar` 契约) · **Parallel-safe**: 全部
- **Steps**:
  - [ ] 按键→字节表:Esc `\x1b`、Shift+Tab `\x1b[Z`、↑↓←→、Enter `\r`、Ctrl+C `\x03`、Tab `\t`
  - [ ] `mountKeybar(onSend)`:`touchstart` 直接 `onSend(bytes)`(绕过 xterm),`preventDefault` 防软键盘
  - [ ] 字节表导出为纯数据,便于单测
- **Steps(测试)**: [ ] 每个按钮映射到正确字节(对纯字节表单测,DOM 部分手动验)
- **Accept**: `vitest run keybar` 全绿

#### T11 · `public/main.ts` `[ ]`
- **Wave/ARCH**: W1 / S7 §5/§6 · **Owns**: `public/main.ts`
- **Depends**: T2 + T10(仅 `mountKeybar` 签名,可并行) · **Parallel-safe**: 后端全部
- **Steps**:
  - [ ] (本文件是 esbuild 打包入口,见§1 约定)xterm + FitAddon 初始化 + `import '@xterm/xterm/css/xterm.css'`;`fit()` 在容器有真实尺寸后调用(§9 坑)
  - [ ] WS:**scheme 随页面协议** `https→wss`(M6),URL `…/term`,首帧发 `attach`(localStorage 取 sessionId)
  - [ ] `term.onData → ws.send {input}`;`onmessage` 按 type 处理(attached 存 id、output→`term.write`、exit→提示重连)
  - [ ] `ResizeObserver`/resize → `fit()` → 发 `resize`(防抖 100ms)
  - [ ] 断线指数退避重连 1/2/4…≤30s,携带 sessionId;彩色状态行
  - [ ] `mountKeybar(data => ws.send(...))`
- **Accept**: W5 联调时验(本任务先完成编码 + `tsc -p tsconfig.web.json` 通过)

---

### W2 · 会话核心

#### T12 · `session/session.ts` `[ ]`
- **Wave/ARCH**: W2 / S5 §3.4 · **Owns**: `src/session/session.ts`、`test/session.test.ts`
- **Depends**: T2、T6(createRingBuffer)、T3(mock IPty);spawn 用 node-pty · **Parallel-safe**: T13 可在本任务**接口**就绪后并行起草
- **Steps**:
  - [ ] `createSession(cfg, dims, now, onExit)`:spawn PTY、建 ring buffer、接 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 幂等(值未变跳过)
  - [ ] `kill`
- **Steps(测试,mock IPty)**:
  - [ ] onData 进 buffer 并转发;**detach 不杀 PTY**;attach 回放快照;后到踢前者(指针先换再 close);detach 后 emitExit 保留会话;对已退出 PTY 的 write/resize 被忽略;send 前查 readyState(用 mock ws)
- **Accept**: `vitest run session` 全绿

---

### W3 · 会话表

#### T13 · `session/manager.ts` `[ ]`
- **Wave/ARCH**: W3 / S5 §3.5 · **Owns**: `src/session/manager.ts`、`test/manager.test.ts`
- **Depends**: T12 · **Parallel-safe**: —(可与 W1 剩余/前端并行)
- **Steps**:
  - [ ] `handleAttach`:null→新建;命中存活→attachWs;命中**已退出**→回放+补发 exit 后移除(L1);查不到→新建返回新 id;createSession 抛错向上抛(M4)
  - [ ] `get(id)`
  - [ ] `reapIdle(now)`:`now - max(detachedAt, lastOutputAt) > idleTtl` 才回收(M3),返回回收数
  - [ ] `shutdown()`:kill 所有会话
  - [ ] 注入给 createSession 的 onExit:从会话表删除(L2)
- **Steps(测试,mock IPty)**:
  - [ ] 新建/命中/已退出三路径;reapIdle:有新输出**不**回收、超时无输出回收;shutdown 全 kill
- **Accept**: `vitest run manager` 全绿

---

### W4 · 集成(汇合点)

#### T14 · `server.ts` 接线 `[ ]`
- **Wave/ARCH**: W4 / S6 §3.6 · **Owns**: `src/server.ts`
- **Depends**: T4、T5、T7、T13 · **Parallel-safe**: T15 可并行起草用例
- **Steps**:
  - [ ] Express 托管 `public/`(含 esbuild 产物 `public/build/`,见§1 约定);提示:start 前需 `npm run build:web`(或 `dev:web` 监听)
  - [ ] `WebSocketServer({ noServer:true })`(L3);HTTP `upgrade`:非 `/term`→destroy、Origin 不过→`401\r\n\r\n`+destroy、通过→`handleUpgrade`→emit connection
  - [ ] `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);路径常量入 config
- **Accept**: 本机浏览器能连通拿到 shell;坏 Origin 被 401(交 T15 自动化)

#### T15 · 集成 / E2E 测试 `[ ]`
- **Wave/ARCH**: W4 / S6 §7 · **Owns**: `test/integration/*.test.ts`
- **Depends**: T14 · **Parallel-safe**: —
- **Steps**(起真实 WS):
  - [ ] attach→attached→output 时序;reconnect 回放(F5/F6,断言 ANSI/中文不乱)
  - [ ] spawn 失败→exit(-1)+仅关连接(M4);坏 Origin/非 `/term`→拒(F9/L3);超 maxPayload 帧被拒(L5)
- **Accept**: `vitest run integration` 全绿

---

### W5 · 验收与收尾(按 F# 并行)

> 每个 F 验收独立,可分派不同 agent(用 `module-reviewer`);均对照 TECH_DOC §8。需要真机/真浏览器,以手动+脚本结合。
> **G4 — 验收任务只报告、不改源码**:reviewer 发现问题 → 返回 findings(标 severity + **owning task**);
> 修复**派回该模块的 `module-builder`**(或 orchestrator 处理),避免验收 agent 跨 `Owns:` 改文件。
> 验收 agent **Owns: 无源码**(只产出 findings 报告;orchestrator 据此回写 LOG)。

#### T16 · 验收 F1–F3:终端基础 / TUI / resize `[ ]`
- **Depends**: T14、T11 · **Owns**: 无(report-only)
- **Steps**: [ ] ls/补全/Ctrl+C/方向键历史(F1) [ ] vim/top/claude 颜色与重绘(F2) [ ] 拖窗口行列实时调整、TUI 不错位(F3)

#### T17 · 验收 F4 + F9:局域网访问 + Origin `[ ]`
- **Depends**: T14 · **Steps**: [ ] 手机/他机 `http://<IP>:3000` 可用(F4) [ ] 跨源 WS 握手被 401、非 `/term` 被拒(F9)
- **注**: 重点确认 allowedOrigins 含本机网卡 IP(M1 回归)

#### T18 · 验收 F5 + F6:会话保活 + 回放 `[ ]`
- **Depends**: T14 · **Steps**: [ ] 发长任务→刷新/换设备→仍在跑、内容不丢(F5) [ ] 离线期输出重连回放、ANSI/中文不乱码(F6/M2)

#### T19 · 验收 F7:移动快捷键栏 `[ ]`
- **Depends**: T11、T10 · **Steps**: [ ] 真机按 Esc/Shift+Tab/方向/Enter/Ctrl+C 生效、不弹软键盘

#### T20 · 验收 F8:进程退出处理 `[ ]`
- **Depends**: T14 · **Steps**: [ ] shell 退出提示重连、回车开新会话 [ ] **detach 后退出**回来仍见末屏+exit(L1)

#### T21 · 安全核对 + README + LOG 收尾 `[ ]`
- **Depends**: T14–T20 · **Owns**: `README.md` · **由 orchestrator 执行**(因涉及写 `docs/PROGRESS_LOG.md` 收尾,不派并行 subagent)
- **注**: 安全核对部分可派 `module-reviewer`(只读)产出报告,orchestrator 据此改 README/LOG
- **Steps**:
  - [ ] 对照 TECH_DOC §7 风险表逐条核(Origin、明文嗅探提示、严禁公网)
  - [ ] README:`npm install/start/test`、查 IP、Tailscale 部署、安全警告
  - [ ] LOG 标记 v0.1 完成;`npm run build` 产物可 `npm start`

---

## 3. 建议的并行分派(参考)

> 每个任务**填好的 dispatch prompt**(可直接复制调用 subagent)见 [DISPATCH.md](./DISPATCH.md)。

| 批次 | 可同时开工的任务 | 说明 |
|------|------------------|------|
| 第 1 批 | T1 | 串行,装齐依赖 |
| 第 2 批 | T2 | 串行,冻结契约 |
| 第 3 批 | **T3,T4,T5,T6,T7,T8,T9,T10,T11** | 9 个并行(后端纯函数 + 全部前端) |
| 第 4 批 | T12 | ring-buffer 就绪后 |
| 第 5 批 | T13 | session 就绪后 |
| 第 6 批 | T14 →(就绪后)T15 | 集成 |
| 第 7 批 | **T16,T17,T18,T19,T20** 并行,后 T21 | 验收 |

> 第 3 批 9 个任务零交叉,但**单批并行控制在 ~3–5 个 agent**(官方甜区;>5 收益递减、烧 token)。
> agent 不足时拆成两轮跑(如先 T4–T7 纯函数,再 T8–T11 前端),仍远快于线性。

---

## 4. 模型与隔离分配(多 agent 用)

> 路由原则(官方):轻活 Haiku 省成本、攻坚 Opus、其余 Sonnet。`isolation: worktree` 仅用于
> **真正并发编辑**的 builder(避免同一工作树上 `tsc`/`vitest`/写文件互相干扰);串行/只读任务不需要。
> 用法:`Agent(subagent_type:"module-builder", model:<下表>, isolation:<下表>)`。
>
> **worktree 前提(需 git)**:worktree 从某个 commit 派生,所以必须**先 `git init`,并把 W0(T1/T2/T3)
> 提交**,再派 worktree 隔离的 W1 builder —— 否则它们的隔离工作树里没有 `types.ts`,会编译失败。
> 每完成一波、合并回主分支后再派下一波。(本仓库已 `git init`。)

| 任务 | 类型 | 建议模型 | 隔离 | 理由 |
|------|------|----------|------|------|
| T1 脚手架 | builder | sonnet | — | node-pty 原生编译可能要排错;串行 |
| T2 types 契约 | builder | sonnet | — | 契约精度要紧;串行,全员依赖 |
| T3 mock IPty | builder | haiku | worktree | 小而独立 |
| T4 config | builder | sonnet | worktree | 网卡 IP 推导 + M1 需推理 |
| T5 protocol | builder | sonnet | worktree | 校验分支多,正确性敏感 |
| T6 ring-buffer | builder | **opus** | worktree | M2 字节/UTF-8/ANSI 边界是最易错的纯模块 |
| T7 origin | builder | sonnet | worktree | 安全相关,宁稳 |
| T8 index.html | builder | haiku | worktree | 静态骨架 |
| T9 style.css | builder | haiku | worktree | 纯样式 |
| T10 keybar | builder | haiku | worktree | 字节表 + 简单 DOM |
| T11 main.ts | builder | sonnet | worktree | xterm + 重连状态机,前端最重 |
| T12 session | builder | **opus** | — | 并发/生命周期最硬(detach/onExit/踢 ws/L1/L4) |
| T13 manager | builder | sonnet | — | reapIdle/已退出路径;串行接 T12 |
| T14 server 接线 | builder | sonnet | — | noServer/handleUpgrade/M5;汇合点,串行 |
| T15 集成/E2E | builder | sonnet | — | 真实 WS 时序断言 |
| T16–T20 验收 | **reviewer** | sonnet | — | 只读报告;修复回流给 owner builder |
| T21 收尾 | orchestrator | sonnet | — | 写 README/LOG,主会话执行 |

> Sonnet 协调 + Haiku/Opus 分工的混编,比全程单模型更省也更准(官方:混编约 +12%)。
> 不确定就用 sonnet;模型只是建议,可按实测调整。
