198 KiB
Web Terminal 实施进度日志 (Progress Log)
用途: 记录 PLAN.md 各任务的完成情况,作为跨会话记忆文件。 新会话开工前必读本文件。只记已发生的事实(含失败、跳过),不预填未来,不写愿望。 谁来写(G1): 本文件仅由 orchestrator(主会话)写入 —— 它不在任何任务的
Owns:里, 防止并行 subagent 并发写冲突。被派的 subagent 不写本文件,而是把日志条目作为返回结果交回, 由 orchestrator 在每批 agent 返回后立即追加(不要攒到最后)。 完整维护规则见 CLAUDE.md → Development Workflow。
状态图例
| 标记 | 含义 |
|---|---|
[ ] |
TODO — 未开始 |
[~] |
IN PROGRESS — 进行中 |
[x] |
DONE — 完成且已验证 |
[!] |
BLOCKED — 受阻(须在详细日志写明阻塞原因) |
当前焦点 (Current Focus)
新会话读到的第一块。保持准确,只描述"此刻"。
🗂 [x] A · 让服务器"看不见"的 tmux session 重新可达(2026-07-30,worktree orphan-tmux-sessions)
- 起因: 用户问"关掉桌面 App 会不会把 tmux session 一起关掉"。查证过程中发现一个功能缺口:
tmux ls有 69 个web_*session,只有 5 个有 client —— 另外 64 个从 UI 完全不可达,其中一个还跑着 7 个 agent 的 Claude Code。最老的到 6 月 26 日。 - 根因:
src/session/tmux.ts只有hasSession,没有 list。manager.list()只遍历内存表,reapIdle也只遍历内存表。所以一个"活过了创建它的进程"的 tmux session:列不出、看不到缩略图、join 不了、UI 里杀不掉,连IDLE_TTL都碰不到它 → 永久堆积。唯一恢复路径是manager.ts的 Case 3.5(客户端带着 localStorage 里的 id 来 attach),必须先知道 id。 - 选型 A vs B(与用户讨论后定 A): A = 目录式,只读枚举 +
capture-pane预览 + 杀,不收养。B = 收养式,启动时把每个都createSession进表。选 A 的决定性理由:B 要为 69 个 session 各起一个 tmux client + 2MB RingBuffer(≈138MB),而且 attach 会按客户端尺寸重设窗口 → 对 64 个跑着 TUI 的 session 同时 SIGWINCH,会把那个 7-agent 的会话强行重排版。B 其实是 A 的自然结果:在 A 的列表里点开某个孤儿,走现成的 Case 3.5 就把它变成表里的一等公民 —— 按需付费,而不是启动时一次性付清。 - 实现:
tmux.ts加listSessions()+parseSessionList()(纯函数,过滤规则可单测)+capturePane()(capture-pane -p -e,不 attach、不 resize、保留颜色);manager.ts加listOrphans/captureOrphan/killOrphan/killOrphansIdleSince/countOrphansIdleSince;server.ts加GET /orphan-sessions、GET /orphan-sessions/count-idle、GET /orphan-sessions/:id/preview、DELETE /orphan-sessions/:id、DELETE /orphan-sessions?idleDays=N;前端在首页 grid 下面加一个 RECOVERABLE 区,通过orphanAsLive()适配器复用现成的卡片/缩略图机制(不另写一套)。 - 安全护栏(全部在 manager 里,路由无法漏掉): ① tmux 名必须是
web_+ UUID v4(复用SESSION_ID_RE,与 attach 协议同一道闸)—— 这既挡住恶意/畸形名字打到tmux -t(字面量-t、路径穿越),也保证用户自己建的 tmux session 永不被枚举、预览或提供删除;② 内存表已有的 id 一律拒绝 —— 用 tmux 杀掉会在活着的 PTY 背后终结 shell 并留下僵尸表项,那条路必须走killById;③useTmux关闭时全部惰性,非 tmux 部署逐字节不变;④ 批量清理跳过 attached(tmux attach和第二个 server 进程都算 attached,"本 server 不管它"不等于"没人用")且idleDays >= 1,没有"删除全部"的形态;⑤ 读路由无 Origin 守卫(与/live-sessions同威胁模型),两个 DELETE 都有;⑥ 预览有独立限流桶。 - 对抗式审查发现 10 个缺陷,其中一个是 CRITICAL —— 必须记住的坑:
#{session_activity}是 CLIENT 时钟,不是输出时钟。它只在 attach 和按键时前进,detached 的 session 无论 shell 打印多少都永不前进。tmux 3.6a 实测:detached session 跑while true; do echo tick; sleep 1; done,6 秒后session_activity仍等于session_created,而#{window_activity}精确跟上了输出。于是"闲置 7 天"实际含义是"7 天没人打字",Clean up idle 7d+会杀掉正在干活的长跑无人值守会话 —— 正是本 App 存在的理由(vibe coding:丢个任务走开)。**在本机 69 个 session 上实测:旧时钟判死 36 个,新时钟判死 33 个 —— 3 个正在产出输出的会话离被删只差一次点击。**已改用window_activity,并加了一条会在格式串回退时失败的测试。- HIGH 无 timeout:
maxBuffer只管洪水不管等待,而同步 exec 无法被事件循环打断 → tmux server 被 SIGSTOP 就永久挂死整个 server。5 处调用(含本功能之前就有的 3 处)全部加上。 - HIGH 同步阻塞轮询路径:
listSessions实测 73ms、capturePane58ms,而每台设备首页 5 秒刷一次 —— 每次都是 73ms 事件循环停转,期间任何设备的任何终端都收不到一个字节。改为 async(manager 三个函数连带改 async)。服务器首先是 byte-shuttle,轮询路径不许阻塞。 - HIGH 清理确认框数错了:数的是卡片(封顶 24),而清理匹配的是服务端枚举的全部 —— 本机会显示"24"然后终结 33 个 shell。新增
count-idle路由回答真实数字,拿不到就拒绝执行。 - MEDIUM ×4:失败与空被折叠成同一个
[](一次 429 就读成"孤儿全没了",拆掉所有已挂载的 xterm);预览闭锁条件是"卡片建过"而非"预览成功"(丢一次请求就永久空白);refresh 从 3 处进入且中间有 await 却无守卫(慢轮询会复活已杀的卡片、或往已清空的 root 里重挂)—— 加 generation 计数器,每个 await 后校验;renderPreview可能写入已 dispose 的 xterm(xterm 抛Object has been disposed)—— 给PreviewCard加显式disposed标记 +disposeCard()统一入口。刻意不用el.isConnected:尚未 append 的卡片是活的,混淆二者会丢掉首次渲染 —— 这一点是被两个既有的 preview-grid 测试正确抓出来的。 - LOW:kill/cleanup 丢弃返回值,拒绝(代理下 Origin 不符的 403、竞态的 404)看起来像按钮坏了 —— 改为回显。
- 性能/规模决策: 预览每张卡只取一次(不随 5s 刷新重取),网格封顶 24 张(每张缩略图是一个 xterm 实例),超出部分在 UI 里明说并给出
tmux attach -t web_<id>兜底 —— 不静默截断。 - 验证:
tsc双 project 干净;单测 2209 全过(+42);e2e 27/27;新增孤儿集成测试 8/8 跑真 tmux(建一个一次性 session → 列出 → capture 并断言它仍是 attached=0 → 拒非 UUID id → 拒外来 Origin → 杀掉 → 第二次 404)。该集成文件刻意从不发起批量 DELETE —— 它跑在宿主机真实 tmux server 上,会终结开发者自己的会话。window_activity的验证在**独立 socket(tmux -L)**上做,全程未触及用户的 69 个 session,事后确认仍是 69。 - 遗留 / 待办: B(启动收养 + 让
reapIdle管到孤儿)未做,需要配"按需 attach"(Session.pty可空)才划算 —— 那要动 Session 形状和不变量 #2。另:桌面 App 默认USE_TMUX=0(desktop/src/server-config.ts:41),所以在 App 里起的 session 不受 tmux 保活,退出 App 就会被杀;这与"走开再回来"的初衷冲突,已向用户指出,是否改默认值待定。 - commit:
4892fa7(功能)+f6ef19e(审查修复),合并22a7929。
🎨 [x] w6 收尾 · 设计稿逐条对照,补掉 Git 面板的落地偏差(2026-07-29/30,worktree panel-design-fidelity + panel-fidelity-leftovers)
- 起因: 用户截图说"按钮和文字框还是很奇怪"。先查客户端再查代码,结论是两件事都成立:
- 截图那个客户端根本没拿到改动。它是
/Applications/Web Terminal.app,前端打包进 bundle(extraResources),那份资源冻结在 17:59,早于全部样式提交;内嵌style.css里.proj-wt-branch-input/.proj-diff-toggle一条规则都没有,所以退回系统默认白框。提交列表却是新的 —— 数据走 HTTP API 实时,CSS 是静态文件,于是出现"数据新、样式旧"。浏览器:3000一直是对的。教训:前端改完必须重打包桌面 App(签名 + sealed resources,直接替换包内文件会破签名,只能npm run dist:mac)。 - 确实还有真偏差。跑了一次设计稿对照审计(6 个维度 finder + 对抗性 refute 复核,18 个 agent),得 59 处;逐条在真实浏览器的 live DOM 上验证过再改。
- 截图那个客户端根本没拿到改动。它是
- 改动 · 控件"有类名没规则",渲染成原生控件(这就是用户说的"奇怪"):
.proj-wt-form完全没有布局规则 → 输入框和 Create Worktree 贴在一起、一出校验错误按钮就被挤到下一行;.proj-wt-error/.proj-wt-actions-error没规则 → 校验失败以 16px 近白色正文渲染,和内容分不开;worktree 的 ✕ 与 Prune 同样零规则;.proj-fanout-mode没有appearance:none→ WebKit 保留原生浅色下拉(全panel 唯一一个);--danger从未定义过,所有校验文案都在 fallback 到不属于调色板的红。 - 改动 · 层叠 bug(后面的同权重规则静默覆盖前面的):
.proj-wt-row-current的琥珀 wash + 边框完全失效 —— "你在这里"和其它行长得一模一样(.proj-wt-row的background/border简写在文件更靠后,等权重后写胜);.proj-wt-branch没有font-family/font-size,继承 16px UA 默认无衬线,是整页最大的字。 - 改动 · 布局:
.proj-syncband去掉flex-wrap(设计稿.sync本来就不换行)—— 换行会在 721–1000px 之间排出参差空行,900px 时 Fetch 独占第二行;改为只让"长句那一格"(proj-sync-cell-flex)让出宽度,于是一路到 720px 断点前都是单行。提交行从 flex 改4 列 grid,且 ↑ 标记每行都渲染(空占位)—— 原来只有未推送行有标记,导致没有两行的列位置相同,未推送那组读不出是一组。已推送的 subject 降到--text-dim,分界线才有意义。 - 改动 · 面板的"诚实性"(设计稿两条硬规矩): ①
?? 0把undefined折成 0,导致计数未知时也能显示绿色✓ in sync—— 设计稿唯一明令禁止的事;改为未知渲染↑ —/↓ —并标unverified,绿色要求真零 + 新鲜 fetch。② 陈旧的behind会把数字本身染成警告红;而"没 fetch 过"是默认状态,等于整条面板长期在喊"这仓库坏了"。疑虑改为只挂在unverified芯片和脚注上,永不碰数字(落后是信息,不是错误)。 - 改动 · 收尾四条(第二个 worktree): detached HEAD 补短 sha(
SyncState.head,从readSyncState已经读过的.git/HEAD文本里取,零额外 I/O、零额外 git 调用);ghost 按钮统一为一套几何(此前漂成三种尺寸,且都用满强度--text),四个 outline 按钮集中声明一处并删掉靠后的重复块 —— 等权重的第二个块正是当初漂移的成因;.proj-branch补 border + 等宽(它是全 panel 唯一没边框的芯片);View Diff 上方补Changes分区标题(此前贴在同步条下沿,读起来像同步条的一部分)。 - 改动 · 语义 token: 新增
--sunk/--warn/--ok/--dirty/--mono-font,替掉散落的 hex 字面量;全 panel 统一一套芯片语言;分区标题改成设计稿的"低字重 + 宽字距大写",计数拆成独立的浅色元素。 - 新增
.gitattributes——public/projects.ts diff。该文件故意含真实 NUL 字节(命名空间哨兵'\0active'/'\0other',为的是绝不可能和真实First.Second键碰撞),git 因此判定为二进制,任何 diff 都只打印Binary files differ、--stat报Bin 66815 -> 68701 bytes,整个文件无法 review。diff属性让 git 按文本 diff,且不设text,所以不做行尾归一化、磁盘字节不变。同一个坑在 shell 里也有:grep只打印Binary file … matches,看起来像没匹配到 —— 用grep -a。(另:macOS 是 BSD grep,\|不支持,要-aE。) - 验证:
tsc两个 project 全干净。单测 2167 全过(78 文件),新增 7 个回归测试:未知计数不给绿色、疑虑不落在数字上、零不用 accent、detached 有/无 sha(前端 2 + 后端 2)。集成 27/27。- 真实浏览器逐宽度验证(不是靠读 CSS 推):1280/1000/900/780/721/600/420px —— 同步条 721px 以上恒单行、以下堆叠成列;提交行每行列位完全对齐(实测 x = 198/230/314/380);任何宽度都不产生横向滚动。ghost 按钮实测
.proj-diff-toggle与.proj-wt-open几何完全一致(同为 h=31)。 - 自己也踩了一次同类层叠 bug 并修掉:第一版把"≤720px 丢掉 age 列"的规则写在了
.proj-commit-row基础规则之前 —— media query 不增加特异性,等权重下后写者胜,于是 3 轨声明失效、subject 掉进 56px 的 age 轨被压成一条。已挪到基础规则之后,420px 实测 subject 264px。
- 真实浏览器逐宽度验证(不是靠读 CSS 推):1280/1000/900/780/721/600/420px —— 同步条 721px 以上恒单行、以下堆叠成列;提交行每行列位完全对齐(实测 x = 198/230/314/380);任何宽度都不产生横向滚动。ghost 按钮实测
- 决策 / 偏离: 审计给的 59 条里,token 名映射差异(设计稿自带 token 名 vs 本项目真实主题 token)一律不算偏差;设计稿的文档外壳(
.wrap/.note/表格等)不是产品 UI,不动。.proj-commitlog-count按设计稿改成--text-dim(此前是 accent)—— 设计稿把 accent 留给同步条里的数字。 - 遗留 / 待办: 无。59 条已全部处理(高/中/低全收)。桌面 App 已用
npm run dist:mac重新打包并签名校验通过(Team IDC738Z66SRW,与原装一致),产物desktop/dist-app/*.dmg+*.zip;安装与重启 App 需用户自己做(旧 App 仍在跑旧资源)。 - 注意: 集成测试
test/integration/server.test.ts在并发负载下(我同时开了第二个 server + 无头浏览器)6 次里挂过 2 次,失败点是 WebSocket 关闭时序(约 862 行),与本次改动无关;单独重跑 5/5 通过。这与上一条日志里"真实 PTY 的 E2E 不能和整套共用机器"是同一个已知抖动。 - commit:
950d229(合并a25fe30)+dba8355(合并e506d0d)。
🌿 [x] w6 项目详情 Git 面板 — G1–G7 全部完成(2026-07-29,worktree project-detail-git-mockup)
- 动机: 详情页只有一个光秃秃的
●,回答不了"有没有 commit 没 push / 现在在哪个 worktree"。设计稿docs/mockups/project-detail-git.html,计划docs/plans/w6-project-git-panel.md。 - 贯穿全篇的一条硬规矩:
ahead/behind比的是本地缓存的@{u},不 fetch 永远不动。本仓库当时就是活教材 ——↑9是真的,而↓0假的:FETCH_HEAD停在 7-10,已 19 天。因此:ahead永远可信(只用本地 ref,绝不标记陈旧);behind超过FETCH_STALE_MS(1h)就打stale;只有↑0 ↓0且刚 fetch 过这一个状态允许显示绿色;upstream === undefined(新 worktree 分支的常态)必须显式说no upstream,绝不能因为"没数字"掉进绿色分支。 - G1 后端同步状态 (
src/types.ts,src/http/projects.ts): 新增SyncState(upstream/ahead/behind/lastFetchMs/detached)+ProjectDetail.sync、dirtyCount。全部为可选新增字段 —— Android/iOS 客户端解码同一批 JSON,不改名不删字段。readSync助手本已存在(列表页在用),buildProjectDetail从来没接;新增readUpstream、readLastFetchMs(读FETCH_HEADmtime,不 spawn)、readDirtyCount(一次 porcelain 同时出布尔和计数)。- 顺手修掉一个既有 bug:
readBranch直接读<repo>/.git/HEAD,而链接 worktree 的.git是文件不是目录,所以在任何 worktree 里都读不到分支。新增resolveGitDirs()同时解析 per-worktreegitDir(放 HEAD)和共享commonDir(放 FETCH_HEAD)。
- 顺手修掉一个既有 bug:
- G2 fetch 路由 (
src/http/git-ops.ts,src/server.ts):POST /projects/git/fetch+fetch()引擎。纪律照抄 push:remote 一律服务端推导(当前分支 upstream,否则唯一 remote;0 个 → 400,≥2 个无 upstream → 409),绝不接受 body 里的 remote/refspec,错误信息永远是白话不是 raw stderr(SEC-M10)。只动refs/remotes/*,不碰工作树/索引/分支,不是 pull。自己的限流桶,不与 push 抢额度。失败时不更新lastFetchMs,让 UI 继续显示 stale 而不是假装刷新过。- 计划里的一条写错了: 原计划说"顺手给 push 加
GIT_TERMINAL_PROMPT=0" —— push 早就有NO_PROMPT_ENV了,无需改。
- 计划里的一条写错了: 原计划说"顺手给 push 加
- G3 同步状态条 (
public/projects.tsmakeSyncBand,public/style.css): 取代光秃秃的●。upstream 名 + ↑n + ↓n + stale 标记 + dirty 计数 + Fetch 按钮(detached 时禁用)。nowMs可注入以便测试。in-flight 期间fetching标志挡住第二次点击。 - G4 推送分界线 (
src/http/git-log.ts,public/git-log.ts):CommitLogEntry.unpushed+GitLogResult.upstream,服务端从git rev-list @{u}..HEAD打标。刻意不用"前 N 行就是未推送"这个便宜做法 ——git log按日期排序,合入一个较老的分支会把未推送的 commit 插到已推送的下面,那样会把未推送的说成已推送(和 ↓0 撒谎是同一类错误)。短 hash(%h)与 rev-list 的全 SHA 用前缀匹配,避免缩写长度不一致。前端只在有 upstream 时画一次分界线。 - G5 worktree 面板 (
public/projects.ts): 标题从worktrees.length > 1 ? 'Worktrees' : 'Branch'改成恒为Worktrees (n);当前 worktree 那行复用 band 的芯片词汇显示↑n/● n/no upstream。其余行保持无状态 —— 不知道就什么都不显示,不猜。 - 验证:
tsc --noEmit干净、npm run build干净。新增 33 个测试全过(2128 → 2161)。分模块:projects.test.ts43、git-ops.test.ts29、projects-panel.test.ts90、git-log.test.ts(前端)17 +(后端)17。- 全量
npm test有 9 个失败,但全部是既有测试,没有一个是 w6 的。在 develop HEAD 上跑基线同样失败(5 个)。根因已定位:vitest 默认testTimeout是 5s,而这些 fixture 要串行 spawn 6–12 个git(init/config/commit/clone/push),全量并行时超预算 →Test timed out in 5000ms。我给自己新增的 describe 块显式加了{ timeout: 30_000 };既有测试没动(改共享测试配置超出本次范围)。
- 全量
- G6 成本控制 (
src/http/projects.ts,public/projects.ts): 否决了计划里的.gitmtime 缓存。用 HEAD/index/reflog 做指纹时,git push更新 remote-tracking ref 不会改动其中任何一个 —— 缓存的ahead会在推送成功后继续宣称"还有 9 个要推",正是这个功能存在的意义所要避免的那种自信的谎话。改用三条不可能变陈旧的措施:- 并发合流:同一仓库的并发探测共用一个 promise,settle 即删。N 个设备看同一个项目 = 一次探测,且不跨时间缓存任何东西。
- 数据未变则跳过重渲染:原来每 5s 重建整棵子树,顺带重新挂载 commit log(又是两次
gitspawn)去重画一模一样的行。 - 页面隐藏时暂停:后台标签页 / 手机熄屏时完全停止探测。
- G7 逐 worktree 状态 (
src/http/projects.ts,src/server.ts,public/projects.ts): 新增buildWorktreeState+GET /projects/worktree/state(比/projects/detail窄:一行只要 branch/sync/dirty,给 N 行都跑一遍列 worktree + 读 CLAUDE.md 是在为不渲染的数据花 spawn)。- 前置缺口已补:
ProjectSessionRef加了cwd。配套的countSessionsByWorktree用最深匹配 ——.claude/worktrees/<name>就在主 checkout 里面,前缀匹配会把每个 worktree 的 session 同时算到父仓库头上。 - 偏离计划:改成每次打开项目探一次而不是"展开某行才探"。状态在客户端缓存、且被"未变则跳过"挡住,成本一样,但信息不用点就有。
- 前置缺口已补:
- 验证:
tsc --noEmit干净、npm run build干净。新增 46 个测试全过(2128 → 2174)。- 顺手修掉了既有测试的抖动,
npm test现在端到端全绿(单元趟 78 文件 / 2147,e2e 趟 27,连续多次)。两个独立原因:- fixture 撑破了 vitest 的 5s 默认值。git 类的要串行 spawn 6–12 个
git,真实服务器类的要起 server + shell。给相关describe加显式{ timeout: 30_000 },真实 PTY 的等待抽成具名的PTY_WAIT_MS。11 个失败 → 约 1 个。 - 真实 PTY 的 E2E 文件不能和整套共用机器。
test/integration/server.test.ts单独跑 27/27 反复稳定通过,一进全量并行就抖 —— 它要在几秒内断言真实提示符输出,而 8 个 worker 正把机器榨干,这个预算根本不成立。继续调大数字只是让抖动换个地方出现,所以npm test改成两趟:test:unit(其余全部,并行)然后test:e2e(该文件独占)。vitest run仍然一次跑全部。
- 另外给 H1 的
finally里那个await srv.close()加了上限。它原本无上限,会把函数体里的真实错误吞掉、只报一个光秃秃的超时 —— 上面这个诊断是先把失败变得可读之后才做得出来的。
- fixture 撑破了 vitest 的 5s 默认值。git 类的要串行 spawn 6–12 个
- 更正:上一条日志把这 3 个失败写成"沙箱环境限制,与代码无关",那是错的。
PTY_AVAILABLE在本机是true,这些用例根本没跳过,是真的在跑并且卡在时间预算上。
- 顺手修掉了既有测试的抖动,
- 遗留 / 待办: 无(G1–G7 全部完成)。设计稿里画的每样东西都落地了。
- commit: 已合入 develop(
8fe1f52/cc811dd,合并e81c426)。后续的设计稿对照收尾见上一条。
🧹 [x] 补完上一条留下的三个遗留(2026-07-29,紧接死锁修复之后)
- ①
.gitignore把源码吞了:agent/src/dist/buildBinary.ts是打包配置(源码),却被通配的dist/规则排除,从未提交——全新 clone 既过不了agent/src/index.ts的类型检查,也导入不了已提交的agent/test/buildBinary.test.ts。修:先解除目录排除(!agent/src/dist/+!agent/src/dist/**)再提交文件 —— git 不会进入被排除的目录,所以只否定文件名是无效的(实测确认)。反向验证agent/dist/、dist/、public/build/、desktop/dist-app/仍被忽略。 - ② 隧道日志不带主机标识:
pair从 enroll 响应拿到 hostId/subdomain 后直接丢弃,长跑的run进程无从得知,所以每条日志都是{"subdomain":null,"hostId":null}—— 包括 8 天故障期那 6380 条告警,恰恰是最需要知道"是哪台机器"的时候。修:新增agent/src/config/hostRecord.ts(saveHostRecord/loadHostRecord/subdomainFromCertPem/resolveHostIdentity),enroll 时落host.json;解析优先级 config(env/argv) > 记录文件 > 叶证书 SPIFFE SAN。第三档是给记录文件出现之前入网的老主机的:它们的证书里本来就有子域名,因此无需重新 pair 就能把标识找回来。接入点:certs/nativeRenew.ts(续期告警)与cli/deps.ts(健康状态行)。 - ③ 手机轨没有恢复路径: 新增
POST /device/:id/recover,与主机版同构 —— 证书走 body、device-CA 路径验证、SPIFFE 解析、notBefore绝不宽限,外加完整 registry 一致性检查(active + 同账户 +:id与证书一致 + 同 key),只放宽notAfter。nginx 在 enroll vhost 加location ~ ^/device/[^/]+/recover$。 - 验证: 单测 agent 300/300、control-plane 296/296,两侧
tsc干净(TDD 先 RED 后 GREEN)。- 线上实测:
/device/xyz/recover已能抵达 CP(返回 CP 的{"error":"rejected"}JSON 而非 nginx 页面);回归 —— 伪造主机证书打/recover仍 401、过期证书打/renew仍被 nginx 拦为 400、/enroll免证书路径未受影响。 - 标识实测(本机 h7fd8):写入
host.json后日志出现subdomain: h7fd8+host_id: 7190ecd4-…;把host.json移走后重启,新一跳仍能从证书 SPIFFE SAN 恢复出subdomain: h7fd8(host_id: (none),证书里本就没有)——老装机回退路径实盘确认。隧道端到端 HTTP 200 不受影响。
- 线上实测:
- 遗留: iOS / Android 客户端尚未接线去调用
/device/:id/recover—— 服务端能力已具备,客户端触发还没有。设备证书过期时目前仍需重新 enroll。 - commit:
2a602d5(合并d92caed)。
🔥 [x] 隧道断了 8 天 —— 证书过期后自动续期死锁,已修 + 已上线(2026-07-29)
- 现象: 用户问"远端 relay 是否在运行"。服务端全绿(nginx/frps/cp-zte/panel-zte/xray/pg/redis 全 active,LE 通配证书有效至 2026-10-05,
*.terminal.yaojia.wang通配 DNS 正常),但 frps:7000已建立连接数 = 0,h7fd8.terminal.yaojia.wang打不通。Mac 上 frpc 在跑,每 20s 重试一次,全部connect to server error: EOF(frps 在 mTLS 阶段拒)。 - 根因(死锁): Mac 的 frp-client 叶证书 2026-07-21 13:55 UTC 过期(TTL 仅 ~24h)。
POST /renew是用它自己要续的那张证书做 mTLS 鉴权的 —— 证书一过期就再也换不了证书。触发链:续期窗口(T-8h)那天 Mac 在睡眠,醒来时 DNS 未就绪 → 24 次getaddrinfo ENOTFOUND enroll.terminal.yaojia.wang错过窗口 → 过期后改报client certificate has expired; renew before dialling,重复 6380 次、8 天、永不自愈(agent.log 涨到 3MB)。 - 三层都 fail-closed,只改一层没用:① agent
transport/dial.tsbuildTlsOptions抛CertExpiredError,连 socket 都不开;② nginxenroll.conf的location = /renew要求$ssl_client_verify = SUCCESS;③ CPapi/renew.tsassertPresentedCertTrusted再查一次有效期 → 401。 - 两个假设被实测推翻(重要,别再踩):
ssl_verify_client optional在客户端证书验证失败时直接返回裸 400(400 The SSL certificate error),请求根本到不了 location —— 所以任何if ($ssl_client_verify …)都救不了。optional_no_ca也不行:它只容忍链错误。见 nginx 的ngx_ssl_verify_error_optional()—— 只含DEPTH_ZERO_SELF_SIGNED_CERT/SELF_SIGNED_CERT_IN_CHAIN/UNABLE_TO_GET_ISSUER_CERT_LOCALLY/UNABLE_TO_VERIFY_LEAF_SIGNATURE,不含X509_V_ERR_CERT_HAS_EXPIRED。已在 VPS 上用真过期证书打 :8472 实测确认。结论:nginx 在任何模式下都不会转发过期客户端证书。
- 最终方案(用户选定:30 天宽限窗口 + 超窗回落配对码): 恢复路径放弃 mTLS,改为不带客户端证书的普通 HTTPS POST,过期证书放进 body。不丢鉴权 —— CSR 是用同一把私钥自签的,而签名网关本来就强制 CSR PoP(
ca/csr.tsverifyCsrPoP)+CSR key == 注册 key,持有私钥的证明与原来 TLS 握手给的完全等价。重放一张(公开的)证书而没有私钥,最多拿到一张自己用不了的证书。- agent:
dial.ts与 mTLS renew 通道保持严格 fail-closed(放宽已从 TLS 层完全移除)。改由 rotator 逐次决策:有效 → mTLS/renew;过期但在宽限内 → 普通/recover;超出宽限 → 终态onExhausted,一次告警后停止重试(不再发任何请求),日志直接给出web-terminal-agent pair <CODE>。新增recoverCert()、recoveryUrlFor()、DEFAULT_EXPIRED_RENEW_GRACE_MS(30d)、CertExpiredBeyondGraceError;AgentConfig加可选recoverUrl(envRECOVER_URL)。 - CP:
/renew恢复严格(grace 0,与终端器行为一致)。宽限只给新增的POST /recover—— 证书从 body 读、忽略x-client-cert头,然后跑与/renew完全相同的信任链:frp-client-CA 锚点做真 X.509 路径验证 → SPIFFE 解析 →notBefore(永不宽限)→ registryactive+ 账户一致。吊销照样生效。 - deploy: 不需要新 vhost / 新 SNI / 新 DNS —— 只往现有 enroll vhost 加一个
location = /recover(+limit_req10r/m)。文档deploy/nginx/enroll-recover-location.md(含 nginx 源码引证)。
- agent:
- 验证:
- 单测:agent 289/289、control-plane 290/290,两侧
tsc --noEmit干净。TDD 全程先 RED 后 GREEN。 - 承重测试:「山寨 CA 签的、SPIFFE SAN 与真证书逐字节相同的伪造证书 → 401」。这条最关键 —— 这条路径上 nginx 已不再验链,它一旦变红,
/recover就成了无鉴权发证机。已在线上实测 401。 - 线上端到端自愈实测:把那张真·过期 8 天的证书塞回 keystore → 重启 agent → 日志
frp-client cert rotated; restarting frpc onto the fresh leaf→ 证书自动换成新的 24h 叶(Jul30 08:06)→ frpclogin to server success/start proxy success→ 设备证书走完整链路:443 → SNI → :8470 mTLS → frps → frpc → Mac base app拿到 HTTP 200(返回本机真实 session 列表)→ 健康探针healthy: true。全程零人工介入 —— 正是生产卡死 8 天的那个场景。 - 回归:
/renew对过期证书仍 400(nginx 拦住,未放宽);4 区 SNI 探针(frp/通配/enroll/xray-Reality)全部未受影响。
- 单测:agent 289/289、control-plane 290/290,两侧
- 应急恢复(修复前先做的): 用 frp-client CA 给 h7fd8 补签了一张 7 天过桥证书(同一把私钥、同子域名、同 SPIFFE SAN,经
openssl ca登记进 index.txt 因此可吊销),先把隧道救活。自愈生效后它已被换成 CP 正常签发的 24h 叶。 - 遗留 / 待办:
agent/src/dist/buildBinary.ts是源码却被.gitignore的dist/规则吞掉,从未提交(既有问题,与本次无关):任何全新 clone/worktree 都缺它 →agent/src/index.ts类型检查失败、已提交的test/buildBinary.test.ts直接导入失败。建议加!agent/src/dist/例外。- agent 日志里
subdomain: (none)/host_id: (none):enroll 后未把 subdomain/hostId 落进配置,导致告警缺主机标识(既有问题)。 - 手机轨(device)目前没有
/recover对应路径,设备证书过期仍需重新 enroll;CP 侧宽限代码是通用的,补一条路由即可。 - 24h TTL + 单次定时续期 + 笔记本睡眠 的组合仍偏脆,宽限窗口是兜底而非根治。
- 文件:
agent/src/certs/rotation.ts·agent/src/certs/nativeRenew.ts·agent/src/config/agentConfig.ts·control-plane/src/api/renew.ts·deploy/nginx/enroll-recover-location.md(新) · 各自测试。 - commit:
f3f4d8b(第一版,方向对但 nginx 那条路走不通)+5509c81(实测后改为 body 传证书,最终版)。
🐛 [x] Mac 版中文全变 _ + 框线渲染错乱 —— PTY 缺 UTF-8 locale(2026-07-28)
- 现象(用户截图,Mac 版):Claude Code TUI 里所有中文变成
_;banner/输入框只剩零散横线,圆角边框消失。 - 根因(实测确认,不是字体也不是前端):
src/session/session.tsspawn PTY 时用env: {...process.env},而服务端由 launchd/Finder 启动,环境里根本没有LANG/LC_*(~/Library/LaunchAgents/com.web-terminal.base-app.plist的EnvironmentVariables只有 PATH/PORT/BIND_HOST)。tmux 逐 client 从LC_ALL/LC_CTYPE/LANG判断 UTF-8 能力,全空 → 进入非 UTF-8 模式,在服务端就把字节改写掉:宽字符→_、✓ ⏺等→_、═║╔╗降级成 DEC Special Graphics(ESC ( 0+qxlk)。所以前端 xterm 收到的已经是残骸,改字体/renderer 救不回来。- 用户
~/.zshrc里其实有LANG=sv_SE.UTF-8,但没用:tmux 是 PTY 的根进程,它做完 UTF-8 判定之后 shell 才 source profile。登录 shell(-l)救不了 tmux。 - node-pty 探针实证:无 LANG →
MARK ________ ==BOX== \e(0qxlk;有 LANG →MARK 中文测试 ==BOX== ═║╔╗ ✓。
- 用户
- 修复: 新增
src/session/locale.ts→withUtf8Locale(env),在createSession()的 spawn env 上先过一道。POSIX 优先级LC_ALL > LC_CTYPE > LANG;已是 UTF-8 就原样不动(用户的zh_CN.UTF-8/ja_JP.UTF-8保留),只在缺失或非 UTF-8(C/POSIX/ISO8859-1)时补en_US.UTF-8(并清掉会盖过它的非 UTF-8LC_ALL/LC_CTYPE)。 - 验证:
test/locale.test.ts9 例(TDD:先 RED 再 GREEN);tsc --noEmit干净。- 端到端:用
env -u LANG -u LC_ALL -u LC_CTYPE复现 launchd 环境跑 server → 新建 session →中文测试:你好,世界!/🎉 ✅/╭─╮ ═║╔╗ █▓▒░ ⏺ ⎿DOM 里逐字符核对全部正确(修复前同一条命令得到________)。 - 之后
npm run build+launchctl kickstart让真正的 launchd 单元(仍无 LANG)接管,再复验一次通过。 - 回归:
test/integration/server.test.ts(含 ⑥ CJK ring-buffer replay、H1 tmux 重启存活)+ git-ops + worktree + projects 共 107 例全绿。(全量 79 文件并跑时有 4 例超时 flake,单跑全过,与本改动无关。) - 存量 tmux session 无需重建:探针证明用带 LANG 的新 client 重新 attach 一个"非 UTF-8 模式下创建"的旧 session,新输出即恢复正常(只有已滚进 scrollback 的旧
_救不回)。
- 遗留:
/Applications/Web Terminal.app/Contents/Resources/dist/里打包的是 7/18 的旧代码,仍未修——桌面 Electron 版要cd desktop && npm run dist:mac重打包(或就地补dist/session/*.js,注意 adhoc 签名会失效)。仓库内dist/与 launchd base-app 已修好。 - 文件:
src/session/locale.ts(新)·src/session/session.ts(spawn env)·test/locale.test.ts(新)。
🔐 零接触隧道 Enrollment 产品化 + Control Panel(2026-07-18~23,当前活跃)
-
需求: 用户要"本机 app 一跑就自动连服务器、自动管证书;手机连接时自动搞定证书"——即产品级零接触(引导时一次动作,之后自动签发+续期,永不再管)。诚实边界:第一次签发无法零人类动作(否则谁碰端点谁拿证书),所以"引导一次,之后免管"。
-
产物: docs/PLAN_ZERO_TOUCH_ROLLOUT.md(执行计划,含预检/两轨/风险)。分支
feat/zero-touch-enrollment(13 提交)→ 已合并develop(merge7c1d433)+ control-panel(675de77)。未 push origin。记忆见mac-tunnel-autoconnect。 -
[x] 主机轨 Track A — 上线 + 实机验证:
web-terminal-agent pair <码> --install→ CP 原生 enroll(真 frp-client 证书) → 装 base-app+agent launchd 单元 → frpc 自动连 frps → 公网h7fd8.terminal.yaojia.wang实测 200。自动续期实测成功:证书 Jul19 13:41→Jul20 05:55 前移、frpc 无缝切、隧道不断。旧手动 mac1 已退役。桌面 GUI 骑 :3000(S110688b0)。- CP 代码:B1 登录/device:enroll bearer(
fff011b)· A2-prep 生产 native-CA 接线(磁盘 P-256 CA,af63014)· renew 接受 escaped-PEM(55d177e)。 - agent 代码:A5 native 自动续期(
9a5909f)+ 实机 shakedown 修复(9f7f5c0/1e398c7/c98f5e6):hostContentSecret 可选、caChain 数组+DER→PEM、launchd 绝对 node+PATH、agent 单元缺 env、renew 用系统根验 LE 服务器(原错钉私有 CA)、launchd 单元加日志 sink。
- CP 代码:B1 登录/device:enroll bearer(
-
[x] 手机轨 Track B — 服务端实机验证 + 两端 code-complete(真机 QA 待做):
- 服务端:curl 全程实测
login → enrollToken(PASETO) → device/enroll(P-256 CSR) → 设备证书(device-CA,SAN 绑定子域名) → 用它 mTLS 访问 h7fd8 → 200。 - iOS(
07bcbf0+ review):契约 6 点全对、已接 UI(主机菜单「自动获取证书」→ SE keygen→CSR→enroll→SecIdentity→静默续期),可直接真机 QA。 - Android(
5e427dc+0b35dc0):enroll 库建好但原没接 UI——已补(EnrollmentScreen+VM+Hilt,照搬 iOS)+ 修 renew(改 mTLS-only 无 bearer)+ 缓存刷新。gradle assembleDebug/单测/kover 绿。
- 服务端:curl 全程实测
-
VPS 部署(8.138.1.192): 新 CP = systemd
cp-zte.service(loopback :8080,从 worktree/root/wt-zte跑 tsx,env/etc/relay/control-plane-zte.env含 NATIVE_/OPERATOR_)· nginx enroll vhost(/enroll·/device/enroll·/auth/login免证书,/renew·/device/:id/renewmTLS + CA bundle)· frps-ctrl 用 frp-client-CA 重签(让 agent enroll caChain 验得了控制通道服务器证书)。 -
[x] Control Panel — 新建 + 上线: 新包
control-panel/(Fastify auth-broker + esbuild SPA,55 测试 + 对抗安全审查修了 1 HIGH〔rate-limit trustProxy〕+3 项)。在线https://panel.terminal.yaojia.wang(operator 口令登录 → 看主机/状态/证书到期、铸配对码〔QR+pair 命令〕、吊销主机);面板每调用现铸 60s manage token 打 CP loopback admin API。systemdpanel-zte.service+ nginxpanel.conf(:8091,X-Forwarded-For $remote_addr 配 trustProxy)。已用面板清掉 5 个失败 pair 的孤儿主机(留 h7fd8)。 -
[x] Android 本机运行验证: 装 emulator + android-35 arm64 镜像 → build APK(67MB)→ 装 + 启动 → 截图确认 UI 正常渲染(「配对主机」界面)、无崩溃。(StrongBox 硬件密钥模拟器验不了,回落软件密钥;真机 QA 仍待做。)
-
验证: CP 281 测试、agent 281 测试、android assembleDebug+单测+kover、panel 55 测试——全绿,每块 orchestrator 独立复验。主机隧道 + 手机服务端 + 面板均真实 HTTP 200 端到端实测。
-
决策/偏离: R1(生产 native-CA)从"配置"重判为"真代码任务"(env.ts 无 NATIVE_*、server.ts 未接线),已建 file-backed KmsResolver 用磁盘 CA;现网无 njs 故 renew 证书头归一化放 CP 侧;manage token off-band 铸(capability 私钥)。
-
遗留/待办: ① iOS/Android 真机 QA(StrongBox + 手点 UI); ② push origin / 未做; ③ 面板"实时在线状态"(查 frps 已注册 proxy)小增强; ④ native-enroll 容忍 + agent-unit env + renew 头归一化 缺专门单测; ⑤ CP 跑在 worktree,别删 /root/wt-zte。
-
commit / 分支:
feat/zero-touch-enrollment(13) → mergedevelop7c1d433+675de77;VPS bare repo 有 develop。
🗺️ ROADMAP 落地 — Wave 1-4 八个功能(2026-07-12,当前活跃;多 agent 并行计划 + 逐个 builder 实施)
-
产物: docs/ROADMAP.md(分层 backlog)+ docs/plans/(8 份可直接照写的实施计划,并行生成)。全在
develop。 -
编排: 主线洞察"产品把一切都采集了却没拿去行动",两个原语(PTY-inject、审批预览)解锁一片。流程 = 并行生成 8 份计划(1 个 workflow / 8 agent)→ 逐个功能派 builder 在主工作树实现(一次一个,避免踩 server.ts 等共享文件)→ orchestrator 独立复验(typecheck 两 config + full suite + build:web + 安全审)→ 提交 → 下一个。
-
[x] 八个全绿(独立复验,非仅采信 builder),每个单独 commit:
- W1 可点链接/文件路径
debf47d—public/link-paths.ts纯 matcher + xterm link provider;URL scheme 白名单 + noopener;附加openFileInEditor(--goto file:line,现有路由只能开目录)。 - W1 审批预览
e062065— 手机上 Approve 前先看到 Bash 命令/Edit diff。src/http/approval-preview.ts有界 sanitize(40 行/200 字/4KB),走 gate 同款晚加入者重发;渲染只走 textContent/renderDiffFile。 - W2 PTY-inject + idle 队列
3076843— 地基原语。POST /live-sessions/:id/queue(Origin+限流+SESSION_ID_RE),idle 时 drain 一条(去抖 timer + pop-one + settle 复检 三重防重复),注入复用 writeInput 字节原样进 PTY。 - W3 diff-vs-base
b119c31—?base=<rev>审整条分支。三层防选项注入:isPlausibleRev →rev-parse --verify --end-of-options→ 只有解析出的 sha 进git diff <sha>... --。 - W3 PR/CI chip
7551f8a—src/http/gh.ts单次gh pr view --json;缺 gh/未登录/无 PR 全降级不抛;PR title 走 textContent。 - W3 quick wins
1dd12b0— 项目卡 ahead/behind + 最近提交时间;成本预算告警(COST_BUDGET_USD单次 latch + push);/digest重连摘要;/projects/log最近提交。 - W4 worktree 删除/prune
552f35c— 破坏性,护栏:必须在git worktree list(realpath 匹配)、拒主 worktree、容器内、脏树要 force、locked 拒、错误归类、git worktree remove不用 rm -rf。 - W4 stage/commit/push
19f241d— 手机审完直接落地。MVP 只 stage/commit/push 当前分支(砍 discard/checkout);push 的 remote+branch 从 repo 读、绝不--force/+refspec;三路由 Origin+GIT_OPS_ENABLED+限流;路径 realpath 容器内 +--后作 argv;错误归类不泄露。
- W1 可点链接/文件路径
-
已知测试抖动(非回归): 两个真-PTY/tmux 集成测试(
ring buffer重放、H1 tmux)在沙箱满负载下撞默认 5s / 自身 20s 超时;单独跑或--test-timeout=30000全绿(full suite @30s = 2005/2006,唯一 red 是 H1 tmux 撞自身上限)。逻辑无回归。 -
未 push: 全部本地
develop,领先 origin/develop 一批。
🖥️ SPLIT-GRID 看板 — 桌面多 session 分屏(2026-07-11)
- 需求: web/Mac 大屏、开多个 tab 时,把
#term大窗切成 1×2 / 2×2 宫格,多个 live 可交互终端同屏,方便"vibe coding"时盯多个 Claude session。手机不做(<1024px 强制 single)。 - 分支:
feat/split-grid-view(自feat/tunnel-automation)。用户决策(AskUserQuestion):全部阶段(v1→v2→v3)用多 agent + loop 完成;审批用每格内联 ✓/✗;成员=前 N 个 tab(拖拽换序控制);布局=single + 1×2 + 2×2。 - 编排: orchestrator 亲写互锁的 5 文件(并行 builder 会互相踩),每阶段后并行对抗式 review workflow(4 lens → 逐条 verify)→ 修 confirmed → 复验绿 → commit → loop 下一阶段。
- [x] v1 DONE(2026-07-11) — orchestrator 独立复验全绿:
npm run typecheck(前后端两 config)干净、build:web干净、全套 1566 测试通过(+33 新增),覆盖率 grid-layout.ts 95%/tabs.ts 94%(≥80 门槛)。- 核心设计:
activeIndex语义不变=聚焦格(keybar/voice/approval 全部零改动);新增gridLayout+ 派生visibleIndices;每个 pane 包进.term-cell(header + 终端 + 可选内联审批 footer);applyLayout()独占 pane 显隐/grid class/cell 排序/占位符;#term变 CSS grid。服务端/WS 协议零改动。 - 文件: 新
public/grid-layout.ts(纯逻辑 + matchMedia 门 + 持久化 + toolbar 分段控件)、tabs.ts(applyLayout/renderCell/renderInlineApprove/setFocused/setGridLayout/refitVisible +activate()改为board-aware)、terminal-session.ts(show({focus})防 4 格抢焦点 +onFocus回调)、main.ts(挂 toggle + refitVisible)、style.css(cell/grid/焦点环/pending 脉冲/内联审批/占位符/toggle +.term-pane改 position:relative flex 子)。新测test/grid-layout.test.ts+tabs.test.ts追加 split-grid 块。 - 交叉验证抓修 3 真缺陷:HIGH —
activate()原先非 board-aware:满格(4/4)时点 "+" 会让新 tab 成为 activeIndex 但display:none,用户对着看不见的 session 打字(空最常见路径触发)。修:把 off-board→moveTab 上板逻辑折进activate(),setFocused/setGridLayout委托之。LOW — 通知抑制未算homeForced(⌂ 覆盖时 pane 实际不在屏);修:onScreen = !homeForced && isVisible。LOW — toggle 触控尺寸(coarse-pointer 平板);修:@media (pointer:coarse)加大。全部补了回归测试。
- 核心设计:
- [x] v2 DONE(2026-07-11) — orchestrator 独立复验全绿:typecheck 两 config 干净、build:web 干净、全套 1579 测试(+13),覆盖率 grid-layout 95%/tabs 93%。加了:1×3(row-3)/2×3(grid-6)布局、Ctrl+
/Ctrl+Shift+循环焦点(main.ts capture keydown → cycleFocus,单格模式不吞键)、每格最大化 ⛶(.maximized覆盖层)、拖 tab 到格(wireCellDropTarget 复用 dragIndex)。用单个grid标记类承载共享 cell 样式(不再枚举每个 lay-*)。- 交叉验证抓修 3 真缺陷:HIGH — 最大化原用
grid-column/row: 1/-1span 网格是错的:CSS Grid 把兄弟格挤进隐式行→"最大化"格变成细条(非全屏)+ 兄弟格 box 变化触发 ResizeObserver→给后台 live PTY 发错误 resize(伪 SIGWINCH)。评审在真 headless Chrome 里实测复现。修:.maximized改position:absolute; inset:0; z-index:4脱离网格流做覆盖层。独立 Playwright 复验:最大化格填满 #term(1000×700)、兄弟格尺寸 0px 变化,对照组证实旧规则确会 891×572 细条+兄弟塌成 52px。MED — 最大化时被盖住的 pending 格 amber 脉冲外溢;修:(!maximized || focused)门。LOW —.cell-max缺 coarse-pointer 触控尺寸。均补回归测试;并把 Ctrl+键匹配抽成matchFocusCycleKey` 纯函数单测。
- 交叉验证抓修 3 真缺陷:HIGH — 最大化原用
- [x] v3 DONE(2026-07-11) — 三个特性,orchestrator 独立复验全绿(typecheck 两 config + build:web 干净、全套 1615 测试、全局覆盖率 89% stmts/82% branch 过 80 门槛)。分三 commit:
- v3a 只读 monitor 格(
cd97114):每格 👁 切换 live↔read-only。public/cell-monitor.ts轮询GET /live-sessions/:id/preview写只读 xterm,不 attach WS、不发 resize,故不驱动共享 PTY 尺寸——解决"小格盯 session 会缩掉别的设备全屏"的跨设备 shrink。monitor 时 live pane 保持 hidden;toggle-off/关 tab/离开 grid 皆清理。 - v3b 可拖拽分隔条(
007e598):格间 gutter 拖拽调每布局 col/row 的 fr 比例(adjustSplit纯函数:相邻轨道互让、clamp 0.3、守恒),内联 grid-template + 持久化web-terminal:grid-splits;拖拽中 reposition 不重建 handle。 - v3c 布局预设(
007e598):public/grid-presets.tstoolbar 下拉,存/应用/删命名的 布局+split(web-terminal:grid-presets);外点/Esc 关、同名替换、XSS-safe(textContent)。 - 交叉验证抓修 4 类真缺陷(6 confirmed,2 dup):HIGH —
splitForLayout({'grid-4':null})抛 TypeError(null!==undefined过守卫后读null.cols),每次渲染都炸→整个 tab UI 砖掉;修:守卫加!==null && typeof==='object'。MED — monitor 在 attach 前(id=null)切换:按钮显 active 但格仍 live 且发 resize,且不自愈;修:onSessionId到达时 reconcile(re-render 使 startMonitor 生效)。MED —renderGutters拖拽中被并发 applyLayout 销毁重建→丢拖拽监听;修:draggingGutter标志跳过重建。LOW — grid-presets 硬编码 1024→改用GRID_MIN_WIDTH。均补回归测试(含 null-split 不抛、reconnect-reconcile、拖拽中重渲染 handle 存活)。
- v3a 只读 monitor 格(
- ✅ 全部阶段(v1+v2+v3)完成:5 commit(06814ba/5475b66/cd97114/007e598 + fix)。每阶段 = 实现→typecheck/test/build 绿→并行多 lens 对抗式 review workflow + 逐条 verify→修 confirmed→复验→commit。maximize 的 HIGH 几何缺陷经真 headless Chrome(Playwright)实测证实修复。服务端/WS 协议零改动;单格模式行为不变。
- 探索产物: 交互式原型 artifact(single/1×2/2×2 切换、点击移焦、内联审批)已给用户看过并据此拍板方向。
🔐 TUNNEL AUTOMATION — 零接触隧道注入(客户永不碰证书/密钥;2026-07-08)
- 计划: PLAN_TUNNEL_AUTOMATION.md(design-locked)。目标:host 一条命令 onboard、device 登录一次即在硬件里生成不可导出密钥→CSR→拿证书,无 .p12/AirDrop,私钥永不离设备。三轨:A 控制面/PKI、B host agent、C 原生客户端(iOS/Android/desktop)。基础设施(frps/device-CA/frp-client-CA/nginx:8470 mTLS)已在 VPS M1 上线;本工作流建的是自动化(签发端点、njs cert→Host 绑定、硬件 keygen、host onboard)。
- 分支:
feat/tunnel-automation(自 develop)。走 MVP fast-path §7(10 个 tracked task,见任务表 #1–#10),依赖图强制:A1 crypto → A2 契约 → A3 nginx 绑定为顺序地基,之后按职责(backend/host/iOS)扇出。 - 编排(loop 模式): 每个 task = 一个 Workflow(TDD builder →
typescript-reviewer+security-reviewer并行交叉验证 → 自动修 confirmed critical/high → orchestrator 独立复验 tests 绿 → 记本日志 → 解锁下游)。三个 workflow 并行:A1(backend/crypto)、A2a(backend/契约)、B2h+B3(host/agent),文件互斥不冲突。 - 地基三块全绿(均 orchestrator 独立复验,非仅采信 agent):[x] A1(x509-assembler+csr-ec;40 测试+openssl verify+tsc 干净)、[x] A2a(契约编辑;relay-auth 133 + relay-contracts 85)、[x] B2h+B3(frpcToml+frpcBinary;agent 全套 184)。详见下方条目。[x] A3(nginx cert→Host 绑定)DONE — orchestrator 已亲自审计 njs 解析器 + zone-anchor 加固(见下方条目)。
- Wave 2 全绿: [x] B1(frp-client P-256 签发 + host keygen)、[x] A4(device-enroll + 会话令牌)。统一复验:control-plane 全套 206/206、
tsc --noEmitexit 0(B1+A4 合并态干净,交叉噪声已消)。A4 交叉验证抓到并修复 1 个 CRITICAL:enroll 路由曾把客户端传的subdomain直接烧进 device 叶子 dNSName SAN 且无归属校验→account A 的 bearer 可铸victim.*证书绕过隔离;已修(归属校验ownerOfSubdomain==accountId+ 复用 host 的 charset/reserved 规则 + 用规范化后的标签),负例测试覆盖(B 请求 A 的 subdomain→403、未认领→403、非法→400、自有→201),orchestrator 已亲读 device-enroll.ts 审计确认。 - Wave 3 三路扇出(不同职责/包,文件全互斥):[x] C-iOS(iOS:SecureEnclave keygen + CSR + Keychain 重构)DONE — orchestrator 独立复验
swift build干净 +swift test27/27 + 跨语言:真 control-plane verifyCsrPoPEc 接受 Swift 生成的 CSR(ok=true);[x] B5(host:两服务安装 + BIND_HOST S-GATE +pair --install)DONE — 独立复验 agent 214/214 + tsc 干净;[x] A6(backend:/renew + rotate.ts 真 X.509)DONE — 独立复验 control-plane 224/224 + tsc 干净;已复用 A4 教训(/renew 身份取自当前 mTLS 证书 + 注册表记录、绝不信 body,anti-smuggling 测试覆盖)。[x] B4(host 原生 run-loop + frpc 监督 + health probe + keystore P-256)DONE — 独立复验 agent 247/247。 - ✅ MVP 全 10 任务代码完成 + 集成复验全绿(2026-07-08):716 测试跨全部包(relay-contracts 85 / relay-auth 133 / control-plane 224 / agent 247 / iOS ClientTLS 27),4 个 TS 包 tsc 全 exit 0,swift 27。MVP 分层保证证实(契约→auth→控制面 签发/注入/续期→host agent 安装/运行/keystore→iOS 硬件注入 全共存绿)。
- 交叉验证共抓修 5 个真缺陷(否则静默上线):A4 客户端 subdomain 跨租户绕过(CRITICAL)、B5 loopback 守卫绕过 ×2(HIGH)、A6 device 续期不延期(HIGH)、B4 health monitor 永不 healthy + frpc 管道不排空致挂起(HIGH)——每个安全关键修复 orchestrator 均亲自审计。
- [x] 加固 pass DONE:9 项 confirmed-review 修复(Ed25519 长度守卫、csr-ec 不可变 + timingSafeEqualBytes、getCertSub readTlv 父容器边界[附 crafted-DER 回归:修前泄 'evil' 修后 '']、renew 有界限流 + csr max + 当前证书链/过期校验、systemd 全路径控制符守卫、keystore prime256v1 校验、@peculiar/asn1-* 声明为直接依赖)。最终全绿 734 测试(relay-contracts 85 / relay-auth 133 / control-plane 235 / agent 254 / iOS ClientTLS 27),4 个 TS 包 tsc 全干净。
- ✅ MVP 代码全部完成(10/10 任务)+ 5 个真缺陷经交叉验证抓修。剩余纯属"需真基础设施"(见下 ACC RUNBOOK):不能在本环境伪造。
- 「继续」round — 集成推进(2026-07-09):[x] #11 frpc tar.gz 解压(traversal-safe USTAR + 真 frp v0.61.1 sha256 pins;对真 frp 归档解出的 frpc 与
tar -xO逐字节一致 14,259,442 字节;agent 254→267)。[x] #14 原生 boot 接线(boot/native-ca.ts 建 frp-client-CA+device-CA 两个 P-256 CA[DEV 自签/PROD KMS fail-fast];main.ts 组合 issuers 共享一个 DeviceStore + 注册 device-enroll + renew 路由 + 真 anchors + 生产 fail-closed 断言;Ed25519 relay /enroll 原样不动)。wired app e2e 证实(integration-native.test.ts 6 测试,fastify inject):device mint→/device/enroll(自有 alice)→201 真叶子链到 device-CA + dNSName → /device/:id/renew→201 同 subdomain;归属门 e2e:未认领→403、跨租户 B-on-A→403、缺 token→401;host 叶子→/renew→201 链到 frp-client-CA、不信任 CA→401。[x] #15 原生 host /enroll P-256 arm DONE:/enroll 按 CSR 密钥类型分叉(EC-P256→原生 frp-client 叶子 via 已接线 hostSigner;Ed25519→relay 原样);把安全关键的配对门gateAndConsumePairingCode从 redeem.ts 抽出供两臂共用(单次 CAS+锁定+过期+PoP/防替换 顺序不变,relay 逐字节证实不变);anti-smuggling 再守(dNSName SAN 只来自服务端分配的 subdomain,body 的 subdomain 惰性——负例证实);hostContentSecret:null。host onboard e2e 证实:配对码→/enroll(EC CSR)→201 叶子链到 frp-client-CA + dNSName → /renew→201 同 subdomain;单次重放/防替换负例绿。当前全绿 758 测试(cp 246 / agent 267 / relay-auth 133 / relay-contracts 85 / iOS 27),4 个 TS 包 tsc 全干净。 - 🎯 后端 onboard 全链路已接线 + wired-app e2e 证实:host enroll + device enroll + host/device renew + 归属隔离门 + 链校验 全部跑通(fastify inject)。代码侧至此完成;剩余小对齐(#16:agent pair.ts 解析原生响应形状、生产 NODE_ENV 守卫、machineId 去重需 relay-contracts 加列)+ 真基础设施(KMS/nginx 部署/真机 iOS/真 VPS frps)。
- ACC RUNBOOK(#10;需 VPS
8.138.1.192+ 真 iPhone + KMS,不可本地伪造):- KMS/CA 物料:为 frp-client-CA(P-256)+ device-CA(P-256)provision KMS 密钥,
buildCaSignerfail-fast 需真 KMS ref;boot 装配 frpclient-issue/device-issue/rotate 用真签发器。 - 接线(#14):control-plane main.ts 注册 device-enroll + renew(并传 hostCaAnchorsDer/deviceCaAnchorsDer,否则链/过期校验静默 no-op)+ device-grant/session。
- nginx(部署,安全关键):装 getCertSub.js 到 /etc/nginx/njs/ + main-context
load_module ...js_module.so;frp-mtls.conf 已有 cert→Host map;renew 若走 header 认证,nginx 必须由 $ssl_client_cert 设 x-client-cert 并 strip 客户端自带。 - frpc 真二进制(#11.9):B3 tar.gz 解压 + 换真 sha256 校验和(frp 发布 checksums),host
run才能真监督 frpc。 - host onboard:真 Mac/Linux 跑
pair <code> --install(BIND_HOST loopback S-GATE 已守)→ 两 systemd/launchd unit →https://<sub>.terminal.yaojia.wang可达。 - iOS 真机:Apple entitlement + 物理设备构建 App(接 ClientTLS enroll seam,#12)→ 登录 → SE keygen → enroll → mTLS 连。
- 已在本环境证实(无需 VPS):真 nginx+njs docker 跨租户 403 / 自有 200 / no-cert 400;Swift CSR 被真 control-plane verifyCsrPoPEc 接受;BIND_HOST S-GATE fail-closed;734 单元/集成测试。剩余为真机/真网/真 KMS 验收。
- KMS/CA 物料:为 frp-client-CA(P-256)+ device-CA(P-256)provision KMS 密钥,
- ⚠️ B5 评审揭出两个真集成缺口(记 task #13,阻塞完整 host-side ACC):① keystore.loadIdentity 仍写死 Ed25519、不认 B1 的
alg判别式 → 存的 P-256 身份加载错;② cli.tsrun对所有 host 无条件走 legacy runTunnel(relay rendezvous)→ 原生 host 未监督 frpc(即 B4/H4 原生 run-loop,我 10-task 分解从 B4 跳到了 B5)。这两块 + frpc tar.gz 解压(#11.9)是 host 真正打通隧道所需。A2b 会话令牌子系统(login→device:enroll token)已并入 A4——它是门控所有 device 注入的唯一 bootstrap 凭据,过于安全敏感,orchestrator 在建 A4(消费该 token 的端点)时亲自设计(计划允许 MVP 用 stub OAuth / 手贴 pairing code)。 - HARDENING backlog(task #11,非阻塞,confirmed review findings 延后): A1 —Ed25519 签名长度守卫、
@peculiar/asn1-*声明为直接依赖、csr-ecUNIFORM_FAILURE改每次新字面量、复用timingSafeEqualBytes、signer/sigAlg 运行时交叉校验;B3 —tar.gz 解压(frp 发 .tar.gz;当前校验+放置的是压缩包非内层 frpc 二进制,B5 pair --install 端到端前必须补)、把占位 sha256(64 个 0)换成 frp 发布校验和。
[x] A2a — 契约编辑: enroll right + SPIFFE /device/ arm + spiffeIdFor(kind)(FIX C-native-1, H-cp-2, H-native-6;2026-07-08)
- 交付: 向后兼容的冻结契约编辑(会话令牌子系统刻意不在本轮)。①
relay-contracts/src/capability/token.ts:CapabilityRight联合类型 +CapabilityRightSchemaz.enum 各加'enroll'(relay-auth/capability/issue.ts自动接受,已由 issue→verify 测试证实)。②relay-auth/src/types.ts:SPIFFE_ID_RE+SpiffeIdSchema用(?:host|device)同时接受两 arm,保留../*拒绝;host 形式仍原样通过。③relay-auth/src/agent/spiffe.ts:spiffeIdFor(accountId, id, domain?, kind:'host'|'device'='host')——现有 3-arg 调用者(control-plane/ca/issue.ts)不变;PARSE_RE捕获 kind;parseSpiffeId → {accountId,id,kind};index.ts导出SpiffeKind。④relay-auth/src/agent/verify-mtls.ts:新 parse 形状 + host-only kind 守卫(not_host_cert)——/device/证书永不能在 host 控制信道被接受(防 device-id 与 hostId 碰撞跨租户),收紧信任而非放宽。relay.trust-domain 前缀刻意保留(按 FIX H-2 延后;强制点是 dNSName SAN 非 SPIFFE URI)。 - 测试(新):
relay-auth/test/spiffe.test.ts(host 向后兼容;device arm;cross-track:CP 签发端将来盖的原始 device SAN 能被 relay-auth schema+parser 接受且等于共享 builder 输出→签发方/验证方不漂移;malformed/traversal/wildcard/unknown-kind 拒绝)、relay-auth/test/mtls.test.ts(第 29 行 round-trip 断言改{accountId,id,kind}锁步 + device 证书在 host 信道被拒)、relay-auth/test/capability.test.ts(issue→verifyrights:['enroll'])、relay-contracts/test/capability-pairing.test.ts(parse('enroll')ok / 未知仍抛)。 - 验证(orchestrator 独立实测,非仅采信 agent): TDD RED-first(6 relay-auth + 2 relay-contracts 先红)。
cd relay-auth && npx vitest run→ 133 passed;cd relay-contracts && npx vitest run→ 85 passed;git diff复核 3 处信任关键 hunk(regex 非捕获组无放宽 / verify-mtls 守卫为收紧 / token 加法式)。交叉验证:两评审均 approve,仅 2 个 LOW(m[2]! as SpiffeKind断言可换运行时守卫;trust-domain 清理延后)——不阻塞。 - 偏差/决策:
parseSpiffeId返回{accountId,id,kind}无 hostId 别名(锁步更新唯一调用者 verify-mtls);新增not_host_cert防御分支。阻塞: 无。下一步: A4 device-issue 用spiffeIdFor(accountId, deviceId, trustDomain, 'device')盖 device SAN。
[x] A1 — x509-assembler + csr-ec(KMS-signed 真 X.509 + P-256 PoP;FIX C-1;2026-07-08)
- 交付: 单一签发原语,附加式(不改 issue.ts/rotate.ts,那是 A6)。①
control-plane/src/ca/x509-assembler.ts(NEW):assembleCertificate(input)用@peculiar/asn1-x509schema 建 v3 TBSCertificate +AsnConvert.serialize(不手写整证 DER),把序列化后的 TBS 交CaSigner.sign(tbsDer)签(KMS 边界——原始 CA 私钥永不载入,INV9),再包成 Certificate(嵌入 exacttbsCertificateRaw)。两签名族:ed25519(OID 1.3.101.112,raw 64B 入 BIT STRING)与ecdsa-p256(OID 1.2.840.10045.4.3.2,导出normalizeEcdsaSignatureToDer把 raw P1363 r‖s 转 DER ECDSA-Sig-Value,正整数前导零处理;已 DER 则透传);tbs.signature 与外层 signatureAlgorithm 同一 OID。subject pubkey 收 CryptoKey 或 SPKI DER。②control-plane/src/ca/csr-ec.ts(NEW):verifyCsrPoPEc(csr)→{ok,embeddedPubSpki}解析 P-256 PKCS#10、强制 EC P-256、验自签 PoP,fail-closed + uniform(malformed/非 P256/坏签统一{ok:false, []});buildCsrEc()测试助手。③boot/ca-wiring.ts(EDIT):加inProcessP256CaSigner(TEST/DEV P-256 CA,返回 raw P1363)。 - 验证(orchestrator 实测):
npx vitest run test/x509-assembler.test.ts test/csr-ec.test.ts test/ca.test.ts test/interop.test.ts test/rotate.test.ts→ 5 files / 40 passed;npx tsc --noEmitexit 0;全套 132 passed 无回归。5 个验收门全过:(a) Ed25519 与 P-256 叶子各new x509.X509Certificate(der)重解析;(b) 各自对 CA 公钥验签 + 错 CA 负例;(c) SAN 含 dNSNamealice.terminal.yaojia.wang+ URIspiffe://…/host/alice双值 round-trip(A3 njs 要解析的正是这些字节);(d) 合法 CSR ok:true、篡改/垃圾/Ed25519/P-384 统一 ok:false;(e) 真 openssl 3.0.18x509 -text+verify -CAfile ca.pem leaf.pem→ OK。 - 交叉验证: ts + security 两评审,verdict warn/approve,0 confirmed critical/high(未触发自动修);9 个 medium/low 记入 task #11 HARDENING(依赖声明、Ed25519 长度守卫、UNIFORM_FAILURE 不可变、signer/sigAlg 交叉校验等,均非阻塞)。阻塞: 无。下一步: B1
frpclient-issue.ts/ A4device-issue.ts经assembleCertificate出叶子;设备/host P-256 CSR 经verifyCsrPoPEc验。
[x] B2h+B3 — 原生 frpc.toml 写入器 + 固定版 frpc 校验下载(host-agent;2026-07-08)
- 交付(严守 lane,未碰 service/install、cli、keys、CA、control-plane、relay-*)。①
agent/src/transport/frpcToml.ts(NEW):buildNativeFrpcToml(opts)出 frp v0.61 TOML(PLAN_NATIVE_TUNNEL §4:serverAddr 默认 8.138.1.192/可配、serverPort 443、transport.tls.enable/serverName=frp.terminal.yaojia.wang/disableCustomTLSFirstByte/certFile/keyFile/trustedCaFile、auth.token、[[proxies]]type=http subdomain/localIP=127.0.0.1/localPort);反 SSRF 硬不变量:localIP 非 loopback →FrpcTomlError(负例测试);边界校验 DNS-label subdomain、port 1-65535、非空路径/token、控制符拒绝、Windows 路径转义;弃用 v0.8[common]/tls_enable(断言不存在)。②agent/src/provision/frpcBinary.ts(NEW):detectFrpcPlatform(darwin/linux×arm64/amd64)、FRPC_RELEASES固定 {version,url,sha256}、provisionFrpc走 buildBinary 纪律:fetch→写临时到盘→SHA-256 校验先于 place/exec→符合 chmod 0755+原子 rename、不符 rm 临时+throw(什么都不放);fetch/fs/sha256 皆可注入,默认 fetch 强制 https(无 -k)。 - 验证(orchestrator 实测):
cd agent && npx vitest run test/frpcToml.test.ts test/frpcBinary.test.ts→ 18 passed;全套 184 passed;tsc exit 0。TDD RED-first 确认。 - 交叉验证 + orchestrator 已修: 评审 verdict warn,2 个 confirmed(未达自动修的 critical/high 门,orchestrator 亲手补):MEDIUM
defaultFetch的fetch()未包 try/catch→网络失败抛未类型化错(已包成FrpcProvisionError);LOW 固定临时名 TOCTOU(已改${TMP_NAME}.${pid}.${randomBytes}每次唯一)。修后 frpcBinary 7/7 仍绿。延后(task #11): tar.gz 解压(当前放置的是压缩包,B5 端到端前必补)、占位 sha256 换真校验和、可选签名校验。阻塞: 无。
[x] A3 — nginx cert→Host 绑定(FIX C-native-3;单一承重租户隔离控制;2026-07-08)
- 交付: 在
ssl_verify_client on(device-CA)之上,njs 解析叶子 dNSName SAN,map要求其最左标签 == 请求 SNI 最左标签、否则return 403,关闭 Model A skeleton-key。①deploy/nginx/njs/getCertSub.js(NEW):纯函数extractLeftmostDnsLabel(pem)——手写 base64→定长 DER 遍历到 SAN(OID 55 1D 11)→取第一个 dNSName(0x82,而非 spiffe URI)→最左标签;主机名字节限[0-9A-Za-z.-](挡:map-key 走私);任何 畸形/缺 SAN/截断 DER →''(fail-closed,空值永不满足 map)。只用 njs∩Node API,故同一源码 Node 单测 + njs 生产同跑。②deploy/nginx/frp-mtls.conf(EXTEND):js_import+js_set $cert_sub+zone-anchored map"~^(?<s>[^:]+):(?P=s)\.terminal\.yaojia\.wang$"+单个if ($cert_host_ok = 0) { return 403; }(规则在 map,不散 if)。③ docker 集成测试deploy/nginx/test/integration/{Dockerfile,run.sh,…}。 - 验证(orchestrator 亲自审计 + 独立实测): 亲读 getCertSub.js 全文确认解析器结构正确且真 fail-closed(PEM body 锚
CERTIFICATE-----尾防头字母泄入 base64、拒不定长/超长长度、结构化走 TBS→[3]→Extensions→SAN)。npx vitest run test/getcertsub.test.ts→ 22 passed(含真 A1-assembler 铸的 P-256 叶子);全套 control-plane 153 passed;tsc exit 0。真 nginx+njs docker 集成 5/5:alice-cert@alice=200、bob-cert@alice=403(跨租户负例)、alice-cert@bob=403、bob-cert@bob=200、no-cert=400。 - orchestrator 加固(评审 confirmed MEDIUM,亲手补): map 原为 label-only
(?P=s)\.,不锚全 zone——虽被 stream SNI 路由 +server_name正则双重约束(实际不可利用),仍改为锚定全 zone.terminal.yaojia.wang$使 map 自足(不依赖 server_name 正确性),加alice.evil.com→deny断言(label-only 会误放)。评审 verdict approve/warn,自动修移除了 security 评审遗留的 scratch 测试文件(曾破 tsc);2 个 LOW(readTlv 父容器边界、critical=true 覆盖)记 task #11(均无利用路径,cert 已过 ssl_verify_client)。阻塞: 无。
[x] B1 — frp-client P-256 host 叶子签发 + host P-256 keygen/CSR(FIX H-host-2/H-host-4;2026-07-08)
- 交付: ①
control-plane/src/ca/frpclient-issue.ts(NEW)createFrpClientLeafSigner— 仿 issue.ts 形状但P-256:CSR 经verifyCsrPoPEc(A1)验、host 注册表门控(bound/active/non-revoked + 嵌入 EC-SPKI==注册 pubkey,uniformLeafSignError,拒绝时 CAsign()不可达)、经assembleCertificate(A1)sigAlg:'ecdsa-p256'+ frp-client-CA 签;SAN = dNSName<sub>.terminal.yaojia.wang(A3 强制点)+ URIspiffeIdFor(accountId,sub,trustDomain,'host');CA:false、EKU clientAuth、KU digitalSignature、[now-60s,now+24h]。②agent/src/keys/identity.ts(EXTEND)加alg:'ed25519'|'p256'判别式 +generateP256Identity/p256IdentityFromPrivatePem(私钥永不出机);③agent/src/enroll/csr.ts(EXTEND)P-256 ecdsa-with-SHA256 PKCS#10,Ed25519 路径逐字节不变。 - 验证(orchestrator 独立复验,A4 并发期隔离跑): control-plane
frpclient-issue.test.ts10/10(叶子 re-parse + 对 frp-client-CA 验签、SAN 双值 + relay-auth parseSpiffeId 认 kind 'host'、EKU/CA:false、gate uniform 拒绝、真 agent enroll/csr 铸的 P-256 CSR 过 verifyCsrPoPEc 且 pubkey 逐字节流入叶子);agentidentity/csr/cli27/27(全套 194/194)。my-files tsc 干净。 - 交叉验证: 评审 approve/warn;自动修 no-op(唯一 "high" 是误判——实为并发 A4 的
DeviceLeafSignerDeps.devicestsc 缺失,fix agent 正确拒碰 A4 文件)。B1 自身 findings 全 LOW(记 #11):frp-client 签发器尚未接入 enroll 路径(redeem.ts:110 仍用通用签发器)、gate step-2 单测缺、cli.testas never。偏差:AgentIdentity 加必填alg判别式(改了一处 mock);P-256 pubkey 存 EC SPKI DER(匹配注册表比对)。阻塞: 无。frps refused-without-cert bring-up 未本地跑(无 frps 二进制;已在 VPS M1 证实)→ VPS 验收。
[x] A4 — device enrollment + 最小会话令牌子系统(FIX C-2, C-native-1;2026-07-08)
- 交付(control-plane,全 deny-by-default / uniform-reject / immutable / Zod-boundary)。①
auth/session.ts:mintDeviceEnrollToken/verifyDeviceEnrollToken/loginToAccountId(单租户 STUB seam)/requireEnrollRight;device:enroll = §4.3 capability token(rights:['enroll'],sub=accountId,aud='device-enroll',10m TTL 夹 [60s,60m]),用 relay-authsignPaseto铸(非issueCapabilityToken——后者硬夹 ≤60s connect clamp + 要求 host/DPoP,与"独立于 connect clamp 的无 host 分钟级 enroll token"[FIX C-native-1] 结构冲突;同一 Ed25519 v4.public 原语 + 同 §4.3 body,冻结验证器原样接受)。②registry/devices.ts:DeviceRecord+DeviceRegistry+createMemoryDeviceStore(镜像 hosts.ts,INV8 版本化状态原子交换)+ 每账户 CAP + 滑窗 rate-limit。③ca/device-issue.ts:P-256 device 叶子经 assembleCertificate,注册表门控(verifyCsrPoPEc + bound/active/non-revoked + pubkey-match,uniform),SAN dNSName +spiffeIdFor(...,'device'),EKU clientAuth/CA:false。④api/device-enroll.ts:buildDeviceEnrollRouter(可注册 fastify 插件),POST /device/enroll + /device/attest/challenge(stub);401/403/429/400 uniform。⑤store/ports.tsADD-ONLY DeviceStore 端口。 - 交叉验证抓到并修复 CRITICAL(两评审均 block): enroll 路由曾把客户端
subdomain直烧进叶子 dNSName SAN 且无归属校验 + 无 charset/reserved 限制 → account A bearer 可铸 account B 的victim.terminal.yaojia.wang证书,A3 nginx 会放行 → 击穿整个隔离模型。自动修已闭合:accountId只来自已验 token(token.sub);normalizeSubdomain+isValidSubdomain(复用 host onboarding 规则)+ 归属门控ownerOfSubdomain(sub)===accountId(否则 403;未认领与外账户均 403,无存在性 oracle);SAN 用规范化后标签(非原始客户端字段)。 - 验证(orchestrator 独立 + 亲审): 亲读
device-enroll.ts确认修复正确;grep确认负例测试覆盖(B 请求 A 的 subdomain→403 line190、未认领→403、非法→400、wrong-right bearer→403、自有→201)。统一npx vitest run→ 25 files / 206 passed;npx tsc --noEmitexit 0(含并发 B1 的 frpclient-issue.ts,无交叉噪声)。 - 偏差/延后(记 #11): enroll token 的
cnf.jkt是随机占位符 → DPoP 绑定推迟,当前是 bearer token(§5 bootstrap 滥用仅靠 短 TTL+cap+rate 缓解,符合 MVP 计划);cap/rate 与 registerDevice 非原子(TOCTOU MEDIUM);rate-limit 在 CSR PoP 之后(LOW);device.* 审计未加(共享 enum,推迟);DeviceRecord 类型 + 内存 store 暂放 registry/devices.ts(model/records.ts + store/memory.ts 本任务冻结)。插件未接入 main.ts(集成步骤)。阻塞: 无。
[x] C-iOS — SecureEnclave keygen + 手写 CSR + DeviceEnrollmentClient + Keychain enroll 重构(FIX C-native ClientTLS trap;2026-07-08)
- 交付(全在
ios/Packages/ClientTLSleaf 包——身份/mTLS 类型所在处,仅依赖 Security/Foundation;全程 SecKey/Security.framework,避开 ClientTLS 陷阱:不用 CryptoKit SecureEnclave.P256,不用 SecKeyCreateWithData-over-SE(-25300))。①SecureEnclaveKey.swift(NEW):SecKeyCreateRandomKey+kSecAttrTokenIDSecureEnclave+ ECSECPrimeRandom(256) +kSecAttrIsPermanent+SecAccessControl(.privateKeyUsage);私钥不可导出/永不出机;Simulator 有软件 fallback + SE-unavailable 错误。②CertificateSigningRequest.swift(NEW):手写 canonical DER PKCS#10(id-ecPublicKey+prime256v1 SPKI),SecKeyCreateSignature(.ecdsaSignatureMessageX962SHA256)自签,sig OID ecdsa-with-SHA256。③DeviceEnrollmentClient.swift(NEW):POST /device/enroll(bearer + A4 body + base64 CSR)+ attest/challenge + /device/:id/renew seam +EnrollmentResult.isRenewalDue轮换 seam。④KeychainClientIdentityStore.swift(MOD):加enroll()/renew()/remove()—— 留 SE key、SecItemAdd(kSecClassCertificate)存回叶子、loadIdentity经SecItemCopyMatching(kSecClassIdentity),使 MutualTLSChallengeResponder/ClientIdentity/urlCredential 原样复用;loadIdentity优先 SE 身份、回退 legacy .p12(双信任迁移窗);保留 PKCS12Importer。 - 验证(orchestrator 独立实测):
swift build干净;swift test27/27(CSR 自签经SecKeyVerifySignature验 + SPKI/alg OID 逐字节 + PKCS#10 三元素结构 + enroll 请求映射 + save→loadIdentity roundtrip + 既有 mTLS responder 测试仍过)。跨语言金标准:Swift 生成的真 CSR 喂给真 control-planeverifyCsrPoPEc→ ok=true(91 字节 P-256 SPKI)。SourceKit 曾误报"cannot find type"——已证伪(build 干净);ZZEmitCSRCrossCheck.swift是一次性交叉验证临时文件,已删(仅剩 .build 索引残留)。 - 交叉验证: swift + security 两评审 verdict warn,0 critical/high;security 评审正向确认属性 1–5 成立(私钥不可导出/bearer 仅用于 enroll 请求/PoP 是真 SE 签名/轮换干净替换/SE-优先回退)。findings 全 medium/low 记 #12(2 处
as!强转、corrupt-record、App 层 enroll UI 注入 + URLSessionHTTPTransport 适配)。真机 SE keygen + SecIdentity roundtrip 需物理设备 + Keychain entitlement → 设备验收。阻塞: 无。
[x] B5 — 两服务安装 + BIND_HOST loopback S-GATE + pair --install(FIX C-host-1/M-host-2service/L-host-zone;2026-07-08)
- 交付(agent 包,复用模块全 import 不改)。① BIND_HOST S-GATE(FIX C-host-1,CRITICAL):
normalizeBindHost()+BindHostError;缺省→127.0.0.1,非 loopback(0.0.0.0/LAN IP/::)抛;在buildInstallOptions(读 env)与installService(任何 write 前)双重强制,拒绝的安装零输出。② 两个 unit(FIX M-host-2service):参数化 launchd/systemd(label/ExecStart/programArguments/description),installService出 base-app(node dist/server.js+ base-app env)与 agent(<bin> run监督 frpc、无 base-app env),uninstallService双拆。③ zone terminal(FIX L-host-zone):assertNativeZone(),base-app originhttps://<sub>.terminal.<domain>(复用 originConfig,不削弱)。④ clipair <CODE> --install:generateP256Identity(FIX H-host-2)→saveIdentity→enrollNative(P-256 CSR + POST /enroll)→provisionFrpc(B3)→writeFrpcConfig(buildNativeFrpcToml)→installService(双 unit)→打印 URL;legacypair(无 --install)不变。 - 交叉验证抓到并修复 HIGH×2(loopback 守卫绕过,同一 bug class):
isLoopbackBindHost(install.ts)与姊妹 anti-SSRF 守卫isLoopbackWsUrl(agentConfig.ts)都用startsWith('127.')→127.0.0.1.attacker.example.com/127.evil.net被当 loopback(Node 会 DNS 解析再 bind → 正是 S-GATE 要防的绕过)。自动修抽出共享agent/src/net/loopbackLiteral.ts(node:net isIPv4要求整串是 dotted-quad + 首段 127,或 localhost/::1/[::1],余皆拒),两处守卫同修。 - 验证(orchestrator 独立 + 亲审): 亲读
loopbackLiteral.ts确认严格 fail-closed;grep确认绕过回归测试在(normalizeBindHost('127.0.0.1.attacker.example.com')/127.evil.net抛 + 专门 loopbackLiteral.test.ts)。agent 全套npx vitest run→ 30 files / 214 passed;tsc --noEmitexit 0;S-GATE 负例确认 0 写 0 运行。 - 偏差/延后: 原生
runfrpc 监督 = B4/H4(未建,记 #13);keystore.loadIdentity P-256 分支(记 #13);frpcBinary 占位 sha256(#11.9);systemdEnvironment=注入守卫一致性(MEDIUM)+ BASE_APP_ENV_KEYS 漏 SCROLLBACK_BYTES/MAX_PAYLOAD_BYTES(LOW)+ cli 若干 minor 记入 backlog。阻塞: 无(B5 本体完成;端到端 host 隧道待 #13)。
[x] A6-min — POST /renew(host)+ /device/:id/renew + rotate.ts 真 X.509(FIX H-host-3;2026-07-08)
- 交付: ①
ca/rotate.ts(UPGRADE)弃 DEV JSON 占位,createLeafRenewer委托 P-256 签发器(frpclient-issue/device-issue → assembleCertificate,sigAlg ecdsa-p256,CA 签在 KMS 后)出真 X.509;renewHostLeaf(hostId, subjectPubkey, csr)+renewDeviceLeaf(deviceId, csr)→RenewedLeaf{cert,caChain,notAfter}(notAfter 从 cert 读回)。②api/renew.ts(NEW)fastify 插件:POST /renew(host)+ POST /device/:id/renew,仅由当前 mTLS 客户端证书认证(可注入 PresentedClientCert seam,默认从 x-client-cert 读 base64-DER);身份(accountId + subdomain/deviceId + subject pubkey)靠重解析当前证书的 SPIFFE SAN(relay-auth parseSpiffeId),绝不取自 body(body 仅 {csr})。anti-smuggling(A4 教训):重签 SAN 从注册表记录取同一 subdomain;把当前证书 pubkey 作 subject key 使委托门控强制 MVP 同密钥(不换钥)。每身份滑窗限流。uniform:无/坏证书 401、撤销/未知/账户或 deviceId 或密钥不符 403、换钥/坏 CSR 400、超限 429、成功 201。 - 交叉验证抓到并修复 HIGH:
renewDeviceLeaf曾每次复用原始 notAfter(读静态 DeviceRecord.notAfter,enroll 时设一次)→ 续期不延期、最终发已过期证书。自动修:续期先让注册表对 active 设备重算+持久化now()+ttl再签,notAfter 从发出的 cert 读回。 - 验证(orchestrator 独立 + 亲查修复): 亲读 rotate.ts 确认 device 续期先延期;统一 control-plane
npx vitest run→ 26 files / 224 passed;tsc --noEmitexit 0;rotate.test 升级为断言真 re-parse+CA 验签的 X.509(host+device 同 subdomain、换钥拒、撤销拒、no-smuggling),renew.test 13 路。 - 延后(记 #14,上线前必办): buildRenewRouter 未接 main.ts;部署安全:nginx 必须由 $ssl_client_cert 设 x-client-cert 且 strip 客户端自带的(否则头可伪造身份);当前证书链/过期校验 + "过期证书→拒" 测试;rate-limiter Map 有界化;csr 加 max 长度。阻塞: 无。
[x] B4/H4 — 原生 run-loop(frpc 监督 + loopback health)+ keystore P-256 loadIdentity(2026-07-08)
- 交付(agent 包)。①
keys/keystore.tsloadIdentity 按存储密钥 alg 分支(createPrivateKey(pem).asymmetricKeyType:ed25519→原路径逐字节不变;ec→p256IdentityFromPrivatePem)——存的 P-256 frp-client 身份能 round-trip(alg:p256 + 可用 ECDSA 签名钥)。②health/probe.ts(NEW):4 个可注入 seam 子检查(frpc alive、base-app loopback GET http://127.0.0.1:PORT、frpc 'start proxy success' 日志扫描、cert 未近过期 8h 窗)、renderHealthStatus只印非敏感标识(subdomain/host id/过期 ISO/布尔,绝无 key/cert/token/CSR,INV9)、startHealthMonitor。③transport/frpSupervise.ts(NEW——计划标 REUSE 但从未存在):spawn frpc + backoff 重启(1s..cap30s,稳定 60s 后 reset),IO 全注入。④cli.tsrun 分支:P-256 身份 + 写好的 frpc.toml → superviseFrpc;否则 legacy runTunnel(保留)。 - 交叉验证抓到并修复 HIGH:
readFrpcLog读<stateDir>/frpc.log但无人写它 → 恒返 '' → HealthReport.healthy 永不为真(H4 监控形同虚设);且 frpc 子进程 stdio pipe 不排空 → 缓冲填满致 write(2) 阻塞挂起。自动修:createFileLoggingSpawn把 frpc stdout/stderr tee 进持久化(每次 spawn 截断)日志,同时排空管道(一并解决挂起 MEDIUM)。 - 验证(orchestrator 独立): agent 全套 247/247(29+ 新:probe 19、frpSupervise 5、keystore/cli),tsc 干净;keystore P-256 round-trip 断言(alg + 原 pubkey 验签);status 无敏感泄漏断言(无 PRIVATE KEY/CERTIFICATE/token/csr)。
- 偏差/延后: 真 frpc 二进制运行仍待 B3 tar.gz 解压(#11);frpc-stdout→log 已由 tee 打通(生产可用);keystore 仅按 asymmetricKeyType='ec' 分支未细究 namedCurve(LOW,记 #11);cli args.code! 无 runCli 内守卫(LOW)。阻塞: 无。
🤖 ANDROID CLIENT — 与 iOS 对齐的原生客户端(计划 + 地基已落;2026-07-08)
- 计划: ANDROID_CLIENT_PLAN.md(多 agent 探索 iOS→综合→4 视角评审→终版)。镜像 iOS 的 SPM 包为 Gradle 模块;36 任务 AW0–AW6。三个开放问题已定:R2(Termux terminal-view/emulator 是 Apache-2.0 + JitPack,非 GPL-阻塞,独立验证更正)、R1(接受 FCM 尽力而为后台送达)、R7(服务端用
google-auth-library)。 - 本机环境约束: JDK+Gradle 可用,但 Android SDK 为空 → 仅纯 Kotlin/JVM 模块能真构建验证;Android 框架层(
:app/:terminal-view/UI/FCM)只能写、待有 SDK 的机器构建。 - [x] AW0 + AW1(纯 Kotlin 地基)DONE — commit
4ea8f78: 一个实施 Workflow(A1 脚手架→A2 冻结:wire-protocol契约→A3 fakes → 并行 A4/A5/A6/A8/A9/A10 → 统一 Verify → 3 路评审)。验证(实测):./gradlew clean test= 218 测试 0 失败(wire-protocol 47 / session-core 60 / api-client 69 / client-tls 27 / test-support 15),clean 重建可复现。- 模块:
:wire-protocol(冻结契约 + HostEndpoint CSWSH origin 派生 + 手写 codec 逐字节对齐JSON.stringify)、:session-core(ReconnectMachine 退避/PingScheduler/GateTracker 双行 epoch 守卫/AwayDigest/TitleSanitizer/KeyByteMap 逐字节匹配public/keybar.ts)、:api-client(全 REST + Origin-仅-守卫 + prefs 未知键保留 + pairing 分级)、:client-tls(纯半:PKCS12/别名逻辑/证书摘要)、:test-support(fakes)。 - 评审 3 视角各 8/10;findings 已修 + 加 11 回归测试:sessionId JSON 注入转义(deep-link 攻击面)、PingScheduler CancellationException 传播、PairingProbe 超时关闭泄漏、HostEndpoint trim、HostClassifier 提升
:wire-protocol统一类型、Pkcs12 错密码判定 BouncyCastle 可移植、lone-surrogate 转义。
- 模块:
- 待办: AW2+(SessionEngine/wiring 冻结)、AW3(
:terminal-viewTermux 封装,需 SDK)、AW4 UI 屏、AW5 push/mTLS 框架半 + A33 服务端 FCM(/push/fcm-token路由现缺,api-client 已标注)。这些需在有 Android SDK 的环境构建验证。
🚧 RELAY-PHASE1 — 把原生 rendezvous-relay 部署到单台 VPS(8.138.1.192,阿里云;2026-07-06)
- 目标: DEPLOY_RELAY §4 的 Phase 1「1b 全量持久化 staging」。VPS 上 Docker 自建 Postgres+Redis;已备案域名走 443 + Let's Encrypt;agent 跑在操作者本机向外拨号。完整文件级方案见 PLAN_RELAY_PHASE1.md。
- 进度: Wave A/B/C/D/E 代码全部完成并通过 Verify —— 一个后台 Workflow(12 agents,0 error)跑完 A2、B1–B5、B6、C、D1、E。Verify 门槛:四个包 tsc 全干净、可构建包全 build 成功、314/314 测试通过(control-plane 103 / relay-run 74 / agent 137 + relay-web 118)。对抗式安全评审:所有硬不变量 PASS(INV2 opaque splice、Origin/CSWSH 精确匹配、mTLS INV14 registry 门控、token-mint 密码学、撤销 INV12、jti 单次、参数化 SQL)。
- 各任务: A2 P3 server 入口+Redis 总线
[x];B1 共享 store EnforceDeps[x];B2 registry-backed MtlsVerifier[x](异步阻抗由 B5 的bridgeAsyncMtlsper-DER 预取桥无锁解决);B3 store RouteResolver[x];B4 撤销订阅[x];B5main-phase1.ts组合入口 + staging/auth/mint签发[x];C agentdist/cli.js构建+runTunnel运行入口[x];D1 同源托管 relay-web[x];E systemd+脚本+RUNBOOK[x];B6 relay-web 登录/DPoP[~] PARTIAL。 - B7 收尾
[x]DONE: ① 功能阻塞已闭环 —browser-server.ts现从term.dpop.<b64u>子协议读 DPoP proof 喂UpgradeRequest.dpop(头优先、否则回退子协议),真实浏览器可通过 DPoP 门。② F1/auth/mint加 Redis 按 IP 限流(HMAC 盐哈希 key,burst5/refill0.2,早于口令比对,耗尽 429);F2activeSessionCount接真实同租户在线 WS 计数;F5 错误日志只记e.message(+.code)防 DSN 泄漏。relay-runnpx vitest run→ 10 files / 92 passed、tsc 干净。F3/F4(撤销后 agent 空转重连、slowloris)为 LOW → Phase 2 backlog。 - ⚠️ 端到端仅可在 VPS 验证:浏览器登录→token→DPoP→upgrade→agent 拼接,本地只到类型/单元层。E2–E5 远端执行见
deploy/RUNBOOK.md(需 VPS 访问权)。 - 远端(需 VPS 执行,见
deploy/RUNBOOK.md): DNS、Let's Encrypt 签证、私有 enrollment CA + capability keypair 生成、阿里云安全组放行 443/AGENT_PORT、systemd 起两个服务、端到端 enroll→dial→浏览器点进 shell。 - 未做/推迟到 Phase 2: 真 KMS、F6 replay、WebAuthn step-up(staging 用
NO_STEPUP_POLICY)、通配多租户、F3/F4。 - 未做/推迟到 Phase 2: 真 KMS(dev signer 暂用)、F6 replay(
loadReplayfail-closed)、WebAuthn step-up(staging 用NO_STEPUP_POLICY)、通配多租户。
[x] A1 — Postgres store 适配器(P3)+ 迁移运行器(2026-07-06)
- 交付:
createPgStores(query): Stores把 9 个 store 端口映射为参数化 SQL(零字符串拼接);runMigrations(query)按文件名顺序跑db/migrations/*.sql(幂等)。 - 文件(全在 Owns 内):
control-plane/src/store/pg.ts、src/db/migrate.ts、db/migrations/0002_routes.sql(新增 — 0001 无 routes 表)、test/store/pg.test.ts。 - 语义对齐 memory.ts: INV8 accounts/hosts
swapStatus用单条 writable CTE(UPDATE 指针 + INSERT 版本行)原子完成,无显式事务;重复插入捕获 pg23505→throw duplicate;casRedeem单赢家UPDATE...WHERE redeemed_at IS NULL RETURNING;RouteStore 存expires_at,expires_at<=now()视为不存在(fail-closed INV7)+ 惰性 DELETE。 - 验证(实测):
npx vitest run test/store/pg.test.ts→ 23/23 通过(自起postgres:16一次性容器 :5433,afterAll 自动docker rm -f);pg.ts覆盖 96.72% stmts / 100% funcs。 - 偏差: ①
0002_routes.sql新增;② SubdomainStore 无独立预留表,按hosts.subdomain UNIQUE归属裁决;③ migrate 按;切分逐条执行(参数化路径拒绝多语句)。阻塞: 无。
[x] A3 — 真能力验证器(relay-auth verifyCapabilityToken)+ 同步→异步 seam(2026-07-06)
- 交付: 抛异常的
refuseAllVerifierstub 换成委托 P5 异步verifyCapabilityToken的真验证器;await贯通整个 authorizer 路径。默认仍 fail-closed(异步化后 reject→401)。 - 文件:
src/api/authz.ts(CapabilityVerifier.verify→Promise)、src/api/provision.ts(5 个路由 call site 改await principal(req))、src/boot/verifier.ts(新;configureCapabilityVerifyKey把env.capabilitySignPubkey32 字节导入 WebCrypto verify key 后调 relay-authconfigureVerifyKey,桥接CAPABILITY_SIGN_PUBKEY_B64vsRELAY_AUTH_VERIFY_PUBKEY命名差)、src/main.ts、package.json(加relay-auth: file:../relay-auth)、test/verifier.test.ts(新)+ 更新 api 测试 stub 为异步。 - 验证(实测):
npx tsc --noEmit全项目干净;npx vitest run15 files / 101 tests pass。 - 偏差: 手建
control-plane/node_modules/relay-auth符号链接以配合新 file: 依赖(npm install会重建)。阻塞: 无。
✅ NATIVE-TUNNEL — 原生 mTLS 反向隧道:四轨代码全部落地(2026-07-07,分支 feat/relay-phase1,commits 5337281/e38e6d1/bb09495/d0c249c)
- 背景: 原生 iOS/Android/桌面客户端讲 base-app 协议(非 E2E relay),需经用户 VPS 反向隧道(frp)回到本机
127.0.0.1:3000。base app 无鉴权 → mTLS 客户端证书是唯一闸门。总计划见 PLAN_NATIVE_TUNNEL.md(Model A 单主人机队;LE 泛证书;frps 控制通道 mTLS+token;CRL 第一天上;BIND_HOST=127.0.0.1强制)。 - 执行: 两个后台 Workflow —— (1) build→review 四轨并行(8 agents,0 error,860k tok);(2) 修复两个 HIGH(2 agents,0 error,308k tok)。四轨文件互不相交(
ios/·desktop/·agent/·deploy/),零src/改动(计划承诺兑现)。 - 各轨(全部已验证):
- C-iOS
[x]: 新ios/Packages/ClientTLS(SecIdentity 包装、PKCS12 导入含类型化错误、keychain 存储AfterFirstUnlockThisDeviceOnly、纯MutualTLSChallengeResponder真值表、跨平台 X.509 DER 摘要)。两个 transport + SessionThumbnail 改惰性@Sendable ()->ClientIdentity?provider(WS 每连接、HTTP 每证书挑战解析 → 装证书后免重启生效)。ClientCertScreen经hostMenu的「设备证书」入口可达;PairingViewModel 对*.terminal.yaojia.wang无证书时挡探测、mTLS 拒绝重映射为 clientCertRejected。验证: ClientTLS 14/14、SessionCore 93/93、xcodegen+xcodebuild BUILD SUCCEEDED。 - C-Desktop
[x](D-1/D-2):remote-hosts.ts+ DesktopPrefs(不可变、向后兼容)+ 托盘选择器;will-navigate动态锁定选中远端 origin、null 失败关闭(无开放重定向);select-client-certificatehandlerevent.preventDefault()优先、按 issuer CN 选证、缺证弹框不静默。验证:npm run typecheck干净。 - S2 host-packaging
[x]: launchd 写<EnvironmentVariables>、systemd 写EnvironmentFile、originConfig zone 参数化(默认term保留);InstallOptions从 CLI install → createCliDeps → installService seam → writers 全程贯通,生成单元携带BIND_HOST(隧道机默认127.0.0.1)/ALLOWED_ORIGINS/PORT/SHELL_PATH/IDLE_TTL/USE_TMUX;systemd 拒绝控制字符。验证: agent typecheck+build 干净、vitest 166/166(154+12 新)。 - V2/V7 VPS 脚本(可提交件)
[x]:gen-device-ca.sh(EC P-256,--kind frp-client)、issue-device-cert.sh(P-256 leaf,EKU=clientAuth only,.p12-legacy+.pem,防 CSR 注入 serverAuth/CA:TRUE)、revoke-device.sh(CRL 强制)、frp/{frps,frpc}模板(disableCustomTLSFirstByte=true)、nginx/frp-mtls.conf(:8470 TLS 终止 +ssl_verify_client on+ssl_crl,WS 升级代理 →:7080)、nginx/stream-sni-additions.md(两条加性 SNI map,合并式共存)。验证: tmp 目录跑通 gen→issue→openssl verify→revoke→CRL(OpenSSL 3.0.18);无密钥/证书误入库。
- C-iOS
- 对抗式评审结论: Desktop/VPS 直接 PASS;iOS/S2 各命中 1 个 HIGH(ClientCertScreen 无入口 / 环境注入未接到 install CLI)—— 均属跨 lane 收尾接线,已由第二个 Workflow 补齐并复验。
- 实机 VPS 部署 V0→V6 完成,M1 达成
[x](2026-07-07,8.138.1.192): frps 0.61.1(systemdfrps.service,控制 :7000 + vhost :7080,控制通道 mTLS+token);P-256 device CA + frp-client CA(openssl ca db + CRL);nginx:8470(ssl_verify_client on+ device-CA +ssl_crl→ WS 代理 :7080);stream map 加性合并frp.terminal→7000/*.terminal→8470(保留*.term→8443/writer→8444/default→10443,.bak已存)。M1 全绿:无证书→400 拒;设备证书→200/live-sessionsJSON(全链路 :443→SNI→:8470 mTLS→frps→frpc→base app);frpc 无 frp-client 证书→控制层拒(error: EOF);吊销设备证书→400、另一证书仍 200(CRL,nginx -s reload后即时);4-zone SNI oracle 共存(xray/writer 未动)。base app 零改动。详见 memorynative-tunnel-m1-live。 - LE 泛证书已上线
[x](2026-07-07): VPS→LE 被墙(curl err 28),改在本机 Mac 跑acme.sh手动 DNS-01(用户不交Ali_Key/Ali_Secret,手动加两条_acme-challenge.terminalTXT,dig验证),本机签出 Let's Encrypt 泛证书*.terminal.yaojia.wang(ECC,exp 2026-10-05),scp到 VPS/etc/relay/frp-tls/,systemctl restart nginx(graceful reload 有 worker 竞态,restart 才稳)。无-k公网信任验证全绿:无证书→400、设备证书→200/live-sessionsJSON;共存 oracle 不变。续期注意:手动模式不自动续,2026-10-05 前需重跑本机手动流程(或补 Ali 凭证走 dns_ali 自动续)。 - M2 现在只剩设备侧: ① 把
feat/relay-phase1的 iOS ClientTLS 版本 build 装到 iPhone;② 设置→设备证书 导入.p12(测试证书/root/deploy-native/out-dev/test-device.p12,口令testpass123);③ 配对https://t1.terminal.yaojia.wang。t1 现指向 VPS 自身的临时 base app;真机 iPhone→笔记本 用例需把笔记本按deploy/frp/README.md上线为一台 host。 - 旁注(非本次改动): E2E 浏览器 relay
:8443在部署前已 DOWN(只有 :8444 writer 在),故terminal.term无证书;*.term→8443路由从未被动过。
✅ 修复:底部快捷键栏遮挡终端内容(iOS 安全区,2026-07-06,main)
- 现象(用户截图,iPhone): 底部快捷键栏(Esc/Esc²/⇧Tab…)压住终端最后一行(Claude Code "bypass permissions" 提示行只露上半)。
- 根因(
public/):index.html设了viewport-fit=cover(布局铺满物理屏、延伸进刘海/Home 指示条),但style.css全程没有任何env(safe-area-inset-*)补偿。于是#keybar(fixed; bottom:0; height:--keybar-h)整条压进 Home 指示条区,而#term只预留了裸--keybar-h,末行与键栏相撞。教科书级 cover-无-safe-area 缺陷。 - 修复(纯 CSS,零 JS):
:root新增--safe-t/--safe-b = env(safe-area-inset-top/bottom, 0px),穿过所有贴边固定元素:#tabbar顶部让出刘海(border-box +padding-top:--safe-t)、#terminset 改calc(--tabbar-h+--safe-t) … calc(--keybar-h+--safe-b)、#keybar高度calc(--keybar-h+--safe-b)+padding-bottom:--safe-b(芯片留在顶部 --keybar-h,Home 条上方)、#approvalbar与body.home-open #term底部、#searchbox/#settingspanel顶部同步。inset=0 时全部塌回原值 → 桌面/无安全区平台零回归。 - 验证(headless,390×844,注入
--safe-t:59px --safe-b:34px模拟 iPhone): 开一个真实会话后量测term_bottom==keybar_top==764(gap=0,无重叠)、tabbar_bottom==99(=40+59,让出刘海);截图确认芯片位于 Home 指示条上方、终端不再被遮。npm run build:web通过。 - 待办: 未提交;未在真机 iPhone 上复验(仅 headless 模拟安全区)。
✅ 修复:项目面板把父文件夹当项目 & 会话点亮所有祖先项目(2026-07-06,分支 feat/ios-client)
- 现象(用户截图): 只在
web-terminal跑了一个会话,但 "Active now" 同时显示web-terminal/Documents/yiukai三张卡,且父文件夹本身被列为项目。 - 根因(
src/http/projects.ts): ①belongsTo纯前缀匹配 → 会话按 cwd 归属到每一个祖先项目;② 历史合并(mergeHistory)把曾经跑过会话的 cwd(如~、~/Documents)原样列为项目。 - 修复(TDD,先红后绿): ① 新增
assignSessions—— 每个 live 会话只归属最深的包含项目(buildProjects改用;buildProjectDetail保留前缀匹配,单项目详情页语义不变);② 新增dropParentFolders—— 丢弃"非 git 且包含其他已列项目"的父文件夹条目(discovery 阶段过滤,进缓存)。 - 验证: 新增 3 测(最深归属/父文件夹剔除/独立非 git 历史项目保留);
test/projects.test.ts29/29 绿;全量npm test53 files / 1473 全绿;tsc --noEmit干净。前端零改动("Active now" 分组消费sessions[],服务器修正后自然收敛)。
✅ 全端应用图标 "Orbit" + 托盘模板图(完成 — 2026-07-06,main,commit 86d100a)
用户选定 "Orbit"(节点绕方块光标核环绕=agent 自主替你运行),品牌沿用桌面 --accent #E3A64A 琥珀金 + 暖近黑 #100F0D。矢量母版三份(App 圆角暖底 / iOS 满幅不透明 / 托盘纯黑透明加粗),rsvg-convert(brew install librsvg)栅格化铺各端:
- iOS/iPad:
Assets.xcassets/AppIcon.appiconset(单尺寸 1024 不透明,Xcode 自动生成全套;此前无图标)。构建验证AppIcon60x60@2x/76x76@2x~ipad入 Assets.car;模拟器主屏实拍确认。 - 桌面 App:
desktop/resources/icon.png(圆角 1024)→npm run dist:mac重打包出新 icns + 156MB DMG(dist-app/Web Terminal-0.1.0-arm64.dmg),顺带带上 projects 修复。装机需xattr -cr清 quarantine(ad-hoc 签名 Gatekeeper)。 - 桌面托盘:
trayTemplate.png/@2x换成正确的 macOS 模板图(黑形状+透明底,系统按亮/暗菜单栏自动染色)——根治"一坨白"。 - web/PWA:
public/icon.svg矢量。待办: 分支/main 未推送;图标对比页(5 创新+5 经典)在 job scratchpad 未入库。
✅ iOS 客户端全面 UX/UI 打磨(完成 — 2026-07-05,分支 feat/ios-client,commit 660a404;精致原生方向)
用户定方向「精致原生 + 可重构」(靛紫 #7C8CFF 强调、语义状态色、SF Mono 数字、系统材质)。编排 = 一个 ultracode Workflow(9 agents,4 阶段):3 路审计(易用性/视觉/无障碍,各 boot 两 sim 取证)→ 冻结 DesignSystem(协调点,防各 agent 各挑配色)→ 4 组文件互斥并行套用 → 验收。
- 审计: 3 视角共产出 ~40 findings,共识根因=「无设计系统」——全 App stock 系统蓝、状态仅靠颜色(违反色+形,idle 蓝撞 unread 蓝)、gate 决策按钮 <44pt、间距/圆角魔数无刻度、数字非 tabular、缩略图纯黑块、gate 中英混排、reduceMotion 未处理。
- 冻结 DesignSystem(
ios/App/WebTerm/DesignSystem/**4 文件): Tokens(自适应 accent 靛紫、语义 status 色、间距 2-24 刻度、圆角 sm8/md12/lg16、Motion gated by reduceMotion、Haptics)、Typography(SF ramp + mono tabular)、StatusStyle(status→色+SF Symbol+中文,单一真源,色+形)、Primitives(StatusBadge/TelemetryChip/Card/SectionHeader/DSButtonStyle 可复用)。根.tint(DS.Palette.accent)单点注入。 - 4 组套用(全 DONE,纯视觉零逻辑改动): G1 列表行重构+状态徽标+telemetry 芯片+缩略图占位;G2 终端 gate 卡(≥44pt approve/reject)+SwiftTerm accent 主题+KeyBar/reconnect/quick-reply/digest;G3 配对 hero+警告分层视觉化+Projects 卡片+Timeline/Diff;G4 导航 chrome+iPad 分栏占位(accent 图标)+隐私遮罩+动效+
ContinueLastBanner/ProjectsToolbarItem抽共享组件。gate 文案落中文。 - 验收 PASS_WITH_FINDINGS,设计评分 8.5/10,1 finding: TimelineSheet class→色仍裸色(全改动里唯一越界)→ orchestrator 补
DS.Palette.timelineTool/timelineUsertoken + TimelineSheet 全走 DS + 更新钉旧裸色的测试。 - 验证(亲验): iPhone 16 290 + iPad Pro 11 290 全绿(修 finding 后);包+集成绿;iPad 分栏占位截图确认靛紫强调色生效。未推送。待用户目视: 会话列表行/gate 卡/配对页真机观感(截图受通知授权弹窗遮挡,建议真机或点过授权后看)。iPad 侧栏 List(selection:) 高亮审计标记为「列表组 Owns 后续 pass」——当前 onOpen 已驱动切换,视觉选中高亮可再补。
✅ iOS 客户端 iPad 适配(完成 — 2026-07-05,分支 feat/ios-client;自适应布局,非分叉)
按 docs/PLAN_IOS_IPAD.md(5 人日)编排:W0 orchestrator 亲做 → 一个 ultracode Workflow(4 agents: T-iPad-2 核心串行 → T-iPad-3 ∥ T-iPad-4 → T-iPad-5 验收)。
- [x] T-iPad-1 device family(orchestrator 亲做,commit
77502ec):TARGETED_DEVICE_FAMILY全 target1→1,2;iPad~ipad四方向(iPhone 不动);ios.yml 加 iPad Pro 11 测试腿。实测:产物UIDeviceFamily [1,2]、iPad 原生满屏(不再 iPhone 黑边)。 - [x] T-iPad-2 AdaptiveRootView + NavigationSplitView + LayoutPolicy(核心):
LayoutPolicy.mode(hSizeClass)唯一 size-class 决策点(regular→split/compact→stack,仿 PrivacyShadePolicy);AdaptiveRootView把横切布线(隐私遮罩 ZStack 顶层两分支共享/scenePhase/deepLink/sheets)上提唯一一处;StackRootView=原 RootView body 原样搬迁(iPhone 字节级零回归);SplitRootView=左 SessionListScreen(复用不改)+ 右 TerminalContainerView(复用不改,带.id);AppCoordinator 加 SidebarItem 路由桥(非平行生命周期)。 - [x] T-iPad-3 KeyBar 自适应 + 指针右键菜单:
KeyBarVisibility纯谓词(硬件键盘在场默认隐);TerminalContextMenu(iPad idiom,复制选区/开新会话/结束会话,一一路由既有通道);kill 经 APIClient 带 Origin 单测。 - [x] T-iPad-4 Projects 大屏化:
ProjectsGridLayout.columnCount(iPhone 恒 1/iPad 按宽 1-3);iPad 多列网格复用同款 row/分组/prefs 逻辑零改;Timeline 已有 detent 两端合理。决策: Projects 在 iPad 以 form sheet 呈现,其内 hSizeClass 恒 compact → 改用userInterfaceIdiom判据(与 size class 正交)。 - [x] T-iPad-5 验收 PASS_WITH_FINDINGS → orchestrator 修复 4/4: iPhone 零回归硬门守住(277 App+261 包+10 集成);iPad 分栏工作;无 CRITICAL/HIGH/安全回归。修复:①MED kill 菜单项无法渲染 →
onKillSession贯穿 TerminalContainerView→两根视图 +AppCoordinator.killCurrentSession()(复用 kill 通道);②MED split 未消费 route →AdaptiveRootView对.loading/.pairing强制走 StackRootView(iPad 首启配对不被困在空态);③LOW 「继续上次」横幅补进 split sidebar(截图确认);④LOW iPad happy-path XCUITest 接受推迟(split 选中逻辑已由 SidebarSelectionTests 9 测真 engine 覆盖,stack happy-path 未变,XCUITest 单跑 7-11min 且脆,成本不划算)。验证(亲验): iPhone 16 277 + iPad Pro 11 278(+kill 桥测试)全绿;iPad 分栏 sidebar+detail 截图确认。 - 合计: iPad 适配零改
ios/Packages/**/src//public/;纯逻辑包设备无关直接复用。待用户: 真机 iPad(分栏手势/硬件键盘/指针右键/Stage Manager)DEFERRED;多窗口(拖会话开新窗并排两终端)另立下一期(本期单场景)。
✅ iOS 客户端 P1 实施(完成 — 2026-07-05,分支 feat/ios-client,P0+P1 共 14 commits,未推送/未合并)
- [x] P1-A(W6+W7 首发: T-iOS-37 ∥ T-iOS-20 ∥ T-iOS-38,3 builders ∥ + verify 6/6 PASS;
/push/apns-token线形状由 orchestrator 预冻结,两侧并行编码零失配——verify 逐项对照 method/path/body key/hex 规则/状态码/守卫/限额全 match):- T-iOS-37 server lastOutputAt:
LiveSessionInfo.lastOutputAt?: number可选增量 +manager.list()一行映射 + 4 测;web 端 grep 零消费者,tsc+全量双证兼容。 - T-iOS-20 server APNs:
src/push/apns.ts(576 行)——env 三件套 all-or-disabled(缺失→路由不挂载,启动不 crash,密钥材料零日志)、token store 照抄 subscription-store 约定(幂等/上限/0600)、手写 ES256 JWT(node:crypto ieee-p1363,零新依赖)、NEEDS-INPUT(priority 10+WEBTERM_GATE+capability token)/DONE(priority 5)、payload 最小化结构性保证(构造函数只读 meta.id,cwd/命令内容不可能出现)、410/BadDeviceToken 逐出、combineNotifyServices与 web-push 并联互不阻塞;hasPushTarget扩展(仅 APNs token 也能 hold gate)。65 测含双 e2e(本地 h2c 假 APNs:held gate→双通道同发同 token→/hook/decision 放行)。新增APNS_HOSTenv(sandbox 切换+测试零网络)。npm test 53 files/1470 全绿,tsc 干净。 - T-iOS-38 APIClient P1 契约: APNs token builders(hex 校验镜像服务器、联网前拒绝、显式 ASCII 字符集防全角)、projects/detail(证实线上无 namespace 字段——纯 web 端分组概念;lossy 解码;percent-encode 单点+裸 + 规避)、prefs(未知顶层键往返逐字节存活——防 iOS 覆写 web 偏好,关键陷阱已测;JSONValue 独立 integer case 防 42→42.0)、公开四层 HostNetworkTier(W3 dedup 欠账,PairingVM 后续 pass 可切换)。76/76 绿,覆盖 95.26%。
- 遗留(LOW/cosmetic): Swift doc comment 里 server 行号引用漂移;PairingVM 分类去重待后续 pass。
- T-iOS-37 server lastOutputAt:
- [x] P1-B UI 九连链(22→24→25→27→26→23→29→28→21 全 DONE,10 agents;verify 判 BLOCKED→orchestrator 修复后放行):
- T-iOS-22 DeepLinkRouter: 全字段白名单(scheme/action/path 空/query 键精确/重复键拒)、UUID 复用冻结 Validation(v4);冷启动单槽 stash;
route(from:)供 push-tap 复用;非法链接计数不回显内容(防日志注入)。21 测。 - T-iOS-24 Timeline sheet: 呈现顺序逐行镜像 web render()(slice 50→reverse);
[]=disabled→空态非错误;复用 AwayDigestView 既有 onExpand(T-iOS-14 零改动);label Text(verbatim:)。14 测。 - T-iOS-25 Quick-reply: 内置 6 chips 逐项镜像 quick-reply.ts(Esc/Enter 经 KeyByteMap 零手写转义);显示条件=持牌 gate(流上唯一 waiting 信号)&&!readOnly;UserDefaults lossy 载入。16 测。
- T-iOS-27 Diff 查看器: App 层 DiffFetcher(RO 无 Origin;并注记归 T-iOS-38 后续收编);hunks 着色/staged 切换/truncated 横幅/lazy list;diff 字节 verbatim 渲染。
- T-iOS-26 Projects: 分组逐条镜像 projects.ts 且组 key 与 web 逐字节一致(collapsed 经 /prefs 跨端共享,key 漂移=两端互丢折叠);prefs clobber 防线——GET 失败→toggle 仅本地绝不 PUT(空底盘 PUT=清库)、PUT 采纳服务器 echo;
claude\rbootstrap 镜像 tabs.ts:679。 - T-iOS-23 切换器: UnreadLedger(单调水位,严格 >)+TitleSanitizer(C0/C1/DEL+bidi/零宽全表,字素簇截断 256)入 SessionCore(+15 包测);LiveSessionInfo.lastOutputAt 容错解码(授权增量);列表边界二次净化。
- T-iOS-29 杂项: new-in-cwd(cwd 不可信,非绝对路径按未知;bootstrap 绝不复注入——开新 shell≠起 claude,专项断言);exit 横幅"开新会话";加建修复潜在 bug: RootView 给容器补
.id(controller.id)(呈现中换 controller 不换 SwiftTerm 视图→终端假死)。 - T-iOS-28 缩略图: 离屏 SwiftTerm 渲染管线(LRU 32/并发闸 2/预览 256KiB 上限/网格钳制),缓存键 (sessionId,lastOutputAt),CADisplayLink 注销不泄漏。
- T-iOS-21 PushRegistrar+锁屏: WEBTERM_GATE category(Allow=.authenticationRequired、Deny 免认证 fail-safe、均无 .foreground,形状测试钉死);device token 对每 host 注册+记账重试;NotificationActionHandler(sessionId 复用 DeepLinkRouter 白名单、token 校验后仅入参绝不落盘、beginBackgroundTask 包 POST、403→本地通知兜底)。锁屏真机行为 DEFERRED(需付费账号)。
- verify(独立 agent): 包 261(SessionCore 92/APIClient 77/…)+App 247+集成 10 全绿;7 项语义抽查全 PASS(含恶意标题/putPrefs 唯一调用点走保留路径/token 零持久化 grep);Owns 审计干净;但抓到 CRITICAL——生产模式启动即崩: @MainActor UNNotificationCenterAdapter 的 completion 闭包继承主 actor 隔离,UN center 在后台队列回调→Swift 6 executor 断言 trap(测试全绿盖不住:push 接线在测试宿主下被跳过——测试盲区实证)。orchestrator 修复: 三处闭包加
@Sendable字面量标注(不继承隔离);重建+实机启动验证:进程存活、崩溃报告零新增、通知授权弹窗弹出(=走过原 trap 点)、弹窗期间隐私遮罩正确覆盖(scenePhase .inactive)。 - 遗留(LOW,归 P1-C 复核): deep link 未断言 userinfo/port(
webterminal://user:pass@open仍路由;id 均 v4 校验+host 只经 store 解析,影响可忽略);Diff fetcher 收编 APIClient 待后续 pass。
- T-iOS-22 DeepLinkRouter: 全字段白名单(scheme/action/path 空/query 键精确/重复键拒)、UUID 复用冻结 Validation(v4);冷启动单槽 stash;
- [x] P1-C 验收+安全复核(T-iOS-30,2 reviewer ∥,report-only)—— 双双 PASS,零 findings:
- Part A 功能验收: 亲跑 261 包测+247 App 测+10 集成全绿;启动崩溃修复复核 PASS(3 次实机启动零崩溃,通知授权弹窗弹出=走过原 trap 点,崩溃报告计数停在修复前的 2);
simctl push实测注入 WEBTERM_GATE payload → 通知横幅真实渲染("Needs input / Session 3d34818d",目视确认最小化:仅状态词+8 位前缀,无 cwd/命令/token);F-iOS-15 服务端切片实测(双会话lastOutputAt差分正确);意外收获——对你的真实服务器(127.0.0.1:3000,pre-T-iOS-37 旧构建)实测旧版兼容降级:App 直接渲染真实会话列表(stuck 红点/未知目录标题回退/0 台设备在看 · 51×50与服务器精确一致/无 unread 点=正确降级)。真机项(锁屏 tap/Face ID、unread 点亮目视、OSC 标题目视)DEFERRED 附手工步骤。 - Part B 安全复核(对抗式): APNs payload 最小化 post-P1-B 仍成立(OSC 标题不进 alert,grep 证实);capability token 全链路 CLEAN——issue(server randomUUID+TTL)→payload(仅 gate)→handler 校验后仅入参 hookDecision→零落盘(grep Push/ 及全 App 无 UserDefaults/Keychain/file 写;唯一持久 token 是内存态 device token)→单次消费(server resolvePending 一次性)→两侧日志零 token;deep-link 50+ fuzz 全部安全(userinfo/port LOW 判定 inert——从不用于构造请求、id 仍 v4 校验,继续接受);锁屏 Allow
.authenticationRequired形状测试绿;prefs 唯一写入点仍守空底盘;P1-B 新攻击面(缩略图字节只进 SwiftTerm、diff verbatim、标题净化覆盖全渲染点)扫盘无泄漏;残余风险台账完整。
- Part A 功能验收: 亲跑 261 包测+247 App 测+10 集成全绿;启动崩溃修复复核 PASS(3 次实机启动零崩溃,通知授权弹窗弹出=走过原 trap 点,崩溃报告计数停在修复前的 2);
- P1 收官统计: 累计 518 项自动化(261 包测 + 247 App 测 + 10 集成)+ 服务器 1470 测全绿;新增能力=远程 APNs 推送(锁屏 Allow/Deny+Face ID)、deep link、Projects 项目列表(分组/收藏/在仓库起 Claude)、多会话切换器(unread+OSC 标题净化)、Timeline/Diff/Quick-reply/缩略图;服务器仅 §0.3 声明的两处增量触点(lastOutputAt + APNs)。待用户: ①真机走查(T-iOS-30 报告手工清单);②Apple 付费账号(APNs 真机端到端+TestFlight);③推送分支跑首轮 GH Actions(ui-test 腿/ios17 runtime);④P2(语音 PTT/worktree/终端内搜索/主题/web ?join= 互通)是否开工——P2 任务需先按 P0 规格扩写 RED 清单再分派(§7 P2 注)。
✅ iOS 客户端 P0 实施(完成 — 2026-07-05,分支 feat/ios-client,7 commits,未推送/未合并)
按 PLAN_IOS_CLIENT §8 批次推进;编排 = 每波一个 ultracode Workflow(builder 按任务派发 + 独立 verify agent 复跑验收)。
- [x] W0 基础(T-iOS-1/2/3/4,5 agents,verify 6/6 PASS):
- T-iOS-1 脚手架:
ios/全结构(project.yml/XcodeGen、5 包空壳、WebTermApp 空窗、.github/workflows/ios.yml);Info.plist 经 plutil 核对 = §5.2 逐字(五段 CIDR,全程零 NSAllowsArbitraryLoads);bundle idcom.yaojia.webterm。偏差: "default MainActor isolation" 属 Swift 6.2+,本机 6.1 → Swift 6 语言模式 + strict concurrency + 显式 @MainActor 替代(已注 project.yml)。环境事实: 本机 Xcode 16.3 原本无 iOS 模拟器 runtime →xcodebuild -downloadPlatform iOS装 iOS 18.4(22E238)后模拟器构建绿。 - T-iOS-2 Day-1 双 spike: 平台事实定音——URLSessionWebSocketTask 可发自定义 Origin(①无 Origin→401 ②精确匹配→101+attached ④端口失配→401),不切 Starscream;③ 1.5MB 回放在默认 1MiB 上限失败(withKnownIssue 记录)、16MiB 成功,ESC/C0 对抗(转义≈3.6MB)成功。勘误(已写回 PLAN §1/§3.2/T-iOS-9): 超限 errno 实测 NSPOSIXErrorDomain 40=EMSGSIZE(计划原文误标 ENOBUFS=55),T-iOS-9 归类以 40 为主、55 兜底。ServerHarness 自举真服务器(tsx spawn+death-pipe watchdog,无孤儿进程)。真机 smoke 4 项 DEFERRED(无真机),SpikeTerminalScreen 已编入 App target 待 T-iOS-18。
- T-iOS-3 WireProtocol(冻结): §3.1 全类型 + Tunables §3.2.1 全表;59 tests 全绿、覆盖率 100%(llvm-cov 226/226);300 轮 roundtrip property + 500 轮 fuzz(decodeServer 永不 crash);跨实现实测: Swift encoder 输出经 tsx 喂给真
src/protocol.tsparseClientMessage → 13 帧+mode 顶层恢复全 PASS。自此冻结。 - T-iOS-4 TestSupport: FakeTransport(actor,scripted failures+按连接记帧)/FakeClock(class+锁,
waitForSleepers确定性栅栏,零真实等待)/FakeHTTPTransport(记录 headers 供 Origin-iff-G 断言);3 smoke 全绿。 - verify(独立 agent 复跑): 模拟器构建 exit 0;59+3 tests;覆盖率 100%;spike 6 tests+1 known issue;
git status零越界。
- T-iOS-1 脚手架:
- [x] W1 叶子包(T-iOS-5/6/7/8,4 builders;编排 [5→6 串行] ∥ 7 ∥ 8——5/6 同在 SessionCore 包,主树并行会互坏编译):
- T-iOS-5 ReconnectMachine+PingScheduler: 纯 reducer,§3.2 签名逐字;backoff 1s→…→30s 封顶镜像 terminal-session.ts(连上才归零,retry/foregrounded 不重置阶梯——防狂按打穿);Ping 25s/连续 2 miss→connectionLost/取消与断线显式区分。16 测,0 真实等待,文件级覆盖 100%/95%。
- T-iOS-6 GateState+AwayDigest: epoch 语义逐行镜像 terminal-session.ts:306-311(上升沿 +1、持续帧刷新 detail、下降沿不重置计数器);
canDecide(epoch:)防误批;affordance 映射逐字对齐 tabs.ts:345-347(绝不发 raw auto);digest 词表与TimelineEvent.knownClasses有相等性绊线测试;防御:乱序容忍、Date→ms clamp 不 trap。25 测,两文件 100%。 - T-iOS-7 HostRegistry: §3.3 契约;KeychainHostStore 经 SecItemShim 缝(swift test 打 fake;真 shim kSecUseDataProtectionKeychain+AfterFirstUnlockThisDeviceOnly,属性字典单点构造+逐字单测);InMemoryHostStore 入 Sources 供 App-VM 测试。30 测,own-Sources 覆盖 88.1%。
- T-iOS-8 APIClient+探针: 44 测先全绿后
[!] BLOCKED正确上报冻结契约自相矛盾——§3.4runPairingProbe → Result<Host,…>中 Host 是 §3.3 HostRegistry 类型,而 APIClient 依赖边仅 WireProtocol(§1 零耦合),签名无法编译(两轮 review 均漏)。裁定(orchestrator,三选一取 c): 探针职责=验证 endpoint → 返回Result<HostEndpoint,…>,Host{id,name} 由 T-iOS-12 VM 构造(id/name 本非探针所知);同时按法定路径加Tunables.pairingProbeTimeout(10s,§3.2.1 新增行+绊线断言)。裁定已写回 PLAN §3.4/§3.2.1/T-iOS-8,公开包装+冒烟测试由 orchestrator 落地。Origin iff-G 入测试名;探针②成功后必带 Origin kill 不留孤儿;403 kill→originRejected(HTTP 侧 G 守卫也是配对必验项)。45 测,own-Sources 覆盖 98.1%。 - W1 验证(orchestrator 亲验): 5 包 178 tests 全绿(59+3+41+30+45);覆盖率(own-Sources 口径)SessionCore 99.1%/HostRegistry 88.1%/APIClient 98.1%,全过 80% 门;
git status全部在 Owns 内零越界。发现归 T-iOS-16: §9 覆盖率命令的 ignore regex 不排静态链入的依赖包源码,CI 接线时须按包过滤(本次已按正确口径亲算)。
- [x] W2 连接核心(T-iOS-9→10 串行;首次派发 T-iOS-9 agent 死于 API 连接中断——现场零文件、直接 resume 重跑,非任务失败):
- T-iOS-9 URLSessionTermTransport: 冻结 TransportConnection 不动,ping 经 SessionCore 内部(非冻结)
PingableTermTransport/ConnectionPinger扩展点接入;pong 时限复用Tunables.pingInterval(无新常量);EMSGSIZE 归类 40 主/55 兜底→类型化.replayTooLarge;connect 失败原样重抛底层错误(保住 PairingError.classify);状态 delegate 驱动,close 三态可区分;测试服务器 = 裸 TCP 手写 RFC6455 framing(可逐字验 Origin/注入 binary/超帧)。11 测,文件覆盖 97.83%,iOS triple 编译过。 - T-iOS-10 SessionEngine actor: 单 Task 生命周期循环 + generation 计数防陈旧任务复活;首帧铁律 attach→lastKnownDims resize→排队帧按序 flush;终态(exit / replayTooLarge)绝不进 backoff;digest 断点原子消费恰好一次;gate 决策 canDecide 兜底、断线期决策丢弃不入队(重连后 gate 可能换代);防御:非 v4 UUID/相对 cwd 剥离(服务器会静默丢帧致挂死)。决策: 初始 resize 依赖首次 sizeChanged/notifyForegrounded(冻结 open 无 dims 参数,SwiftTerm 首次布局即触发,已注 doc);digest limit 传 Int.max(截断属 UI,T-iOS-14)。20 测,SessionEngine.swift 94.98%(480 行,<800 硬上限,内聚裁量已记)。
- W2 验证(独立 agent,5/5 PASS): SessionCore 72 测(41 W1 完好+31 新);四包回归全绿(累计 209 tests);own-Sources 覆盖 96.68%;冻结契约零改动;4 项语义抽查(Origin 单点 :169/16MiB 来自 Tunables/replayTooLarge 断重连路径逐行核/decodeServer 唯一入口 nil-drop)全数在源码落实。
- T-iOS-9 URLSessionTermTransport: 冻结 TransportConnection 不动,ping 经 SessionCore 内部(非冻结)
- [x] W3 UI+提前项(7 agents: [T-iOS-11→12→13→14] 串行链 ∥ T-iOS-16 ∥ T-iOS-17;协调点 = orchestrator 预建 WebTermTests 单测 bundle(project.yml+5 包依赖,模拟器实测过)并单独提交):
- T-iOS-11 Terminal+KeyBar: KeyByteMap(SessionCore 纯数据)逐字节复刻 keybar.ts 全 17 键(⏎=\r 非 \n 专项断言);TerminalViewModel 消费 engine.events,replayTooLarge→不可重试态+话术逐字断言;决策: 硬件 UIKeyCommand 仅注册 Esc/⇧Tab/Ctrl 和弦,箭头不注册(固定 CSI 会破 DECCKM 应用光标模式,vim/htop 错乱)——有排除测试;OSC52 剪贴板拒绝、链接仅 http(s)。
- T-iOS-12 Pairing: Phase 状态机;扫码→confirmingHost,确认前零网络(recordedRequests/connectAttempts 空断言);公网需显式勾选确认;§5.4 四层警告 13 组参数化;§3.4 裁定落地(VM 构造 Host 入 store);host 分级 VM 内重实现(APIClient 的 internal 粒度不足,重复已标注归 T-iOS-38 去重);真机 DataScanner 编译隔离,generic/platform=iOS 构建过。
- T-iOS-13 SessionList: 轮询 Tunables.listPollInterval,离开断言 0 sleeper+10 周期零请求(无泄漏);发现: LiveSessionInfo 无 pending 字段(已核 manager.list())→ ⚠ 徽标经 setPendingApproval overlay 由 T-iOS-15 从 gate 事件接入;kill 乐观移除+404=成功/403 回滚;staleness 严格 > 镜像 web;PR 链接 https-only(SEC-L5 镜像)。
- T-iOS-14 Gate/Digest UI: GateViewModel 独立 VM(events 与 engine 分离注入,T-iOS-15 fan-out 零改动);三选一映射唯一事实源=SessionCore Affordance.clientMessage(acceptEdits/default/reject,App 层零 allowAutoMode 逻辑);tap-epoch+affordance 双守卫;haptic 每 epoch 恰一次(HapticSignaling 注入);digest 自动淡出 FakeClock 驱动;文案逐字镜像 tabs.ts:326-350。
- T-iOS-16 集成 CI: 吸收 spike 重构 harness;10 测对真自举 Node 服务器(echo/stty resize 1..1000/16MiB+ESC/C0 对抗回放/JOIN mirror/401/401/403/kill→WS close 而非 exit 帧/自然 exit 广播);ios.yml=包矩阵+coverage-gate.sh(own-Sources 过滤,修正 §9 已知缺陷)+App 测试+集成 job;覆盖率门红→绿演示实录;CI 平台未跑(如实注明)。
- T-iOS-17 ntfy 文档: ios/README.md;只读验证(引 setup-hooks.mjs file:line),未触碰用户真实 hook 配置(orchestrator 安全约束);payload 最小化逐行核;真机端到端 DEFERRED。
- W3 验证(独立 agent,全项 PASS): 包套件 59+3+77+30+45;App 55 测 TEST SUCCEEDED;集成 10 测+1 known issue(故意的 1MiB 复现);语义抽查 5/5(KeyByteMap 逐字节 16 项、三选一字面量+零 allowAutoMode、零网络断言实测在 :60、轮询走 Tunables、CI 覆盖率 own-Sources 过滤);Owns 审计零越界。累计 224 单测 + 10 集成。
- [x] W4 汇合(T-iOS-15,1 builder + verify 8/8 PASS):
- T-iOS-15 App 接线:
Wiring/9 文件——AppEnvironment 生产 DI(真 Keychain/真探针/URLSessionHTTPTransport 零逻辑透传,Origin 规则全留 APIClient)、EventFanOut(单消费者 engine.events → TerminalVM+GateVM+SessionActivityBridge 三分支保序广播)、bridge(.adopted→存 lastSessionId/.gate→列表 ⚠ overlay/.exited→清两者,防「继续上次」指向死会话)、TerminalSessionController(.background→close 干净 detach;suspend 后 .active→重建栈+open,generation 换 SwiftUI identity 触发首次 sizeChanged 夺回全屏)、PrivacyShade 严格scenePhase != .active(三相位测试)、冷启动路由纯函数;WebTermApp 薄 @main;删除 SpikeTerminalScreen(grep 零残留)。 - 两项自动化走查代理: ①LiveServerSmokeTests——签名宿主内真生产 DI 图端到端(探针→host 入库→liveSessions→attach→echo 回显→close→kill 清理);②KeychainHostStoreLiveTests——真 Keychain 往返+
SecItemCopyMatching断言kSecAttrAccessible==AfterFirstUnlockThisDeviceOnly+非 synchronizable(T-iOS-7 递延项闭合)。模拟器 boot 截屏:配对页正常渲染。 - 环境硬墙(新发现,已记 harness): repo 在
~/Documents(TCC 保护)时,模拟器 App 语境 posix_spawn 的 node 读 repo 文件被 tccd 无限阻塞(sample 实测卡uv_cwd→__open_nocancel;平台二进制豁免)→ SimServerHarness 双模式:A 自举(CI/非 TCC 路径)/BWEBTERM_SERVER_URL外部注入;TCC 下 0.001s 快速失败+指引。本机跑法: 起服务器后xcrun simctl spawn booted launchctl setenv WEBTERM_SERVER_URL …再 xcodebuild test(命令行 TEST_RUNNER_ 前缀实测传不进宿主 App 测试进程);裸跑 smoke 红=有意显式失败非回归。 - 偏差闭合(orchestrator 代 T-iOS-11 owner 修): W4 曾记「alive-engine
.active未调 notifyForegrounded(dims:)——W3 无 dims 钩子」→ TerminalViewModel 增lastSentDims(仅记有效 resize,invalid 不覆盖,有测试)+ controller.active分支接notifyForegrounded(dims:)(无有效 dims 时跳过,SwiftTerm 首次布局兜底)。76 App 测全绿(75+1)。 - verify(独立 agent,8/8): 214 包测+76 App 测+10 集成;spike 删净;遮罩规则/生命周期双向 close 路径/组装安全(G 全走 APIClient、零 ad-hoc URLSession、零 ArbitraryLoads、Origin 零手拼)逐项在源码核实;Owns 零越界。
- T-iOS-15 App 接线:
- [x] W5 验收 + findings 修复(2 reviewer ∥ + 1 fix builder;均 PASS_WITH_FINDINGS→已闭合):
- T-iOS-18 F 走查: 全部机器可执行项本轮亲自重跑(集成 10/App 76/包 214 + 模拟器冷启动·后台·重启截屏);F-iOS-1..13 逐条 EXECUTED/EVIDENCED(指认测试名)/DEFERRED(真机)(QR 扫码/IME/震动/切换器遮罩目检/ntfy 端到端,附手工步骤清单)。
- T-iOS-19 安全核对: 对产物审——Origin 写入点全树恰两处且单点派生;G/RO 分界;拆构建产物验五段 CIDR+零 ArbitraryLoads+双 usage description;Keychain 属性;P0 不该在的面(deep link/麦克风)确认缺席;残余风险(isCaptured 跳过/ws 明文模型/epoch 仅客户端)记录完整。
- findings(0 CRITICAL/HIGH; 1 MED+3 LOW,全部闭合): ①MED §9 XCUITest happy path 缺失 → orchestrator 建 WebTermUITests target/scheme,fix agent 落地唯一用例:配对(手输)→列表→attach→KeyBar ^L→注入 held gate(POST /hook/permission)→点 Approve→断言服务器真放行 behavior==allow,服务器为 oracle(XCUITest 读不了 SwiftTerm 字形);幂等三分支(全新/已配对/空态)全被实走;两次真实跑通(439s/677s,EXIT=0)。②LOW CI 缺 iOS17 下限腿 → ios.yml 增 ios17-floor job(runtime 缺失大声跳过/存在必判,CI-only 本机未验)。③LOW URLSession.shared 磁盘缓存可落盘 preview 终端字节 → 生产 HTTPTransport 默认 .ephemeral。④LOW XcodeGen target 级默认覆盖 project 级 TARGETED_DEVICE_FAMILY(实际打包 [1,2]) → 移 target 级,产物已验 [1]。
- XCUITest 实证发现(已记 ios.yml/用例注释):
TEST_RUNNER_env 只在 xcodebuild 进程环境里才透传 runner(命令行 KEY=VALUE 是 build setting 到不了);CODE_SIGNING_ALLOWED=NO打断真 Keychain(-34018)→ 顺手修掉既有 app-tests 腿同 flag(否则 KeychainHostStoreLiveTests 在 GH runner 必红);XCUITest typeText 够不着 SwiftTerm(无 AX 键盘焦点)→ 输入断言走 KeyBar ^L+清屏应答;SwiftTerm 光标闪烁烧 XCUITest 60s idle-wait/事件(单跑 7-11min,已压到 3 次终端交互)。 - 遗留观察(report-only,非阻塞): 配对探针偶发残留 clientCount=0 会话(killProbeSession 竞态,run 8 一例)——归 T-iOS-8 owner 复核;真 GH Actions 首跑待推送后确认(ui-test 腿/ios17 runtime 镜像现状)。
- P0 收官统计: 306 项自动化(214 包测 + 76 App 测 + 10 集成 + 1 XCUITest[计 1,参数化更多]) 全绿;覆盖率 own-Sources SessionCore 96.7%/HostRegistry 88.1%/APIClient 98.1%/WireProtocol 100%;19/19 任务完成(全部偏差有日志);服务器 src/ public/ 零改动。待用户: ①真机走查清单(T-iOS-18 报告内);②推送分支跑首轮 GH Actions;③P1 开工前拍板:Apple 付费账号(APNs/TestFlight)与 bundle id/App 名(现 com.yaojia.webterm/WebTerm)。P1 首批 = T-iOS-20(APNs server) ∥ T-iOS-37(lastOutputAt) ∥ T-iOS-38(APIClient P1 契约)。偏差(orchestrator 决策,W1 起生效): 并行 builder 不用 git worktree(Workflow worktree 合并回主树增加失败面),改为主树直跑——文件互斥 + 同包任务串行化,禁 agent 碰 git;每波末独立 verify + orchestrator commit 兜底。
✅ iOS 客户端实施计划 — docs/PLAN_IOS_CLIENT.md(规划完成 — 2026-07-04,main,未提交;纯文档,零代码改动)
多 agent 编排 = 一个 ultracode Workflow(66 agents,7 阶段):5 读者并行(wire contract 逐 file:line / 功能矩阵 / relay 现状 / 模板规范 / iOS 平台联网调研)→ 3 架构师独立竞标(full-native / WKWebView-hybrid / phased)→ 3 评委多视角打分(UX/工程/安全,phased 26.5 胜出,native 23.5、hybrid 16.5,21 条 mustSteal 嫁接)→ 综合简报 → 写手出稿(843 行)→ 4 评审交叉验证(对照 src/ 源码逐条核实 / iOS API 联网核查 / 安全 / 计划质量)→ 每条 finding 对抗式 verify(CRITICAL/HIGH 双票)→ fixer 应用。
- 方案: Phased-Native「口袋驾驶舱」—— SwiftUI + SwiftTerm(MIT/SPM),4 个纯 SwiftPM 包(WireProtocol/SessionCore/HostRegistry/APIClient)+ 薄 App 胶水,
ios/顶层新包;前台会话单条活 WS + 其余 HTTP 轮询;iOS 17+/Swift 6/XcodeGen;P0 服务器零触点(通知复用既有 ntfy 桥 scripts/setup-hooks.mjs:227-238),P1 仅两处声明触点(APNs sender +LiveSessionInfo.lastOutputAt,后者为 TS 任务 T-iOS-37)。relay/E2E 明确推迟(已审计 TS 加密不得随手 Swift 重写)。 - 任务机: 38 个任务(T-iOS-1…38),P0=19 任务 W0–W5 ≈13 人日、P1 W6–W8 ≈17.5、P2 W9–W10 ≈8,总 ≈38.5 人日;每任务 Owns/Depends/测试先行 Steps/Accept/估算/分派模型。关键平台事实:原生客户端 WS 握手必须显式带白名单内
Origin头(src/http/origin.ts:27 对 undefined 默认拒);URLSessionWebSocketTask.maximumMessageSize需调大到 16 MiB(回放单帧 JSON \u 转义最坏 6×2MB)。 - Review 结果: 35 findings → 32 confirmed 全部修复、3 rejected(含「ATS 不收 CIDR」被 verify 驳回 —— iOS 17+ 确实支持 CIDR exception domains)。修复亮点:plan-gate 三选一映射纠正为
acceptEdits/default/reject(对照 public/tabs.ts:345-347);「:443 破坏 Origin 匹配」为臆造 gotcha,反向纠正(new URL() 双侧归一化);ntfy 桥从「新建」改「复用既有」;CRITICAL 共享类型无主 → 全部收编进 WireProtocol(T-iOS-3);W7 Owns 重叠拆出 T-iOS-38;锁屏 Allow 加.authenticationRequired(Face ID 确认才批 shell 命令);QR 扫码改「先确认再发包」防恶意配对。 - 验证(orchestrator 亲验): 文档 943 行落盘、11 节结构完整、fixer 27 组修复日志逐条对账;
git status仅新增该 .md,src/ 零改动。未提交。下一步:用户批准后按 §8 分派批次开工(W0 首日 spike 实测 Origin header)。
✅ 安全审计 relay-e2e + relay-auth — 上线前深度审计 + 修复(完成 — 2026-07-02,分支 security/relay-auth-audit-fixes,未提交)
上线门:自动生成的 E2E/鉴权核心须过安全专家审计(前次复审判 approve-with-changes)。多 agent 编排 = 两个 ultracode Workflow:①审计(99 agents)—— 11 维 finder 并行(两包)→ 每个 finding 3 lens 对抗式 verify(exploitability/crypto-spec/refuter 多数票)→ 第二轮「重复 review」深挖 → 每包 completeness critic → dedup+定级;②修复循环(10 agents)—— foundation(types)→ 3 组文件互斥并行修复(module fixers)→ verify 循环至 tsc 干净 + 全绿 → 5 路对抗式 re-audit 确认每个 exploit 已闭合。
- 审计结论: relay-e2e = PASS 零改动(所有候选在 verify 阶段被驳回:确定性 nonce 安全 —— 方向分离 + 真实 agent 侧
createReplaySealer各自持独立单调 seq;strict-successor recv guard 无法导致 replay-accept)。relay-auth = approve-with-changes,5 条确认(2 HIGH / 1 MED / 2 LOW),全部在 human-auth/enforcement 层,全部包内可修。驳回但存档(不再复议):E2E replay nonce reuse、mTLS EKU、SPIFFE 域未 pin、reattach host-scope、TOTP 时序。完整报告见docs/REVIEW_RELAY_SECURITY.md。 - 修复(均含回归测试): F1 HIGH step-up 因子降级 ——
needsStepUp现把新鲜度绑定到精确方法(stepUpMethod),钓来的 TOTP 不再满足 passkey;F2 HIGH step-up 在新建会话路径被完全跳过(生产 callerterm-relay/data-plane/upgrade.ts:105硬编码principal:null→ 生产环境 step-up 形同虚设)—— 改为主机驱动 + fail-closed(policy.required时 principal 缺失/过期/错方法一律 403);F3 MED WebAuthn 断言未绑定已存凭据 —— 现校验 credentialId 且转发 pubkey 给 verifier;F4 LOW DPoP verify 未捕获抛出 → INV15 门未审计地崩(审计逃逸+DoS)——verifyDpopProof全体 try/catch→false +coreAuthorize边界兜成干净审计 401 + issue 校验 cnfJkt 格式;F5 LOW 验证过的 signCount 被丢弃 → 克隆检测失效 ——finishAuthentication现返回{principal,newSignCount}供持久化。 - 验证(orchestrator 亲验): relay-auth
tsc --noEmit干净 + 122 测试全绿(基线 104,+18 回归);relay-e2e 未动、tsc 干净、76 全绿。re-audit 5/5 闭合(置信 0.95–0.97,均有回归测试)。 - 遗留/behavioral note(非阻塞): F2 改为 fail-closed 后,任何
stepUpPolicyFor(host).required===true的主机,P1(term-relay)必须在 upgrade ctx 传入已认证 principal,否则正确地被拒;当前 v0.9 默认NO_STEPUP_POLICY.required===false,现网非 step-up 主机不受影响。P1 集成时须补此接线(见 relay-stepup-needs-principal-wiring)。 - 第二轮独立 review(Fable 5,换模型增强多样性)+ F6 修复: 第二个
Workflow(fix-regression 猎取 + 新角度 sweep + 对抗式 re-attack 被驳回项)判:5 条修复 0 regression、被驳回/D-i-D 项 re-attack 0 存活(确认真不可利用),但新增 1 条 HIGH——F6:relay-e2ereplay-key.ts的可恢复K_content仅由(hostContentSecret, sessionId)决定(稳定),而真实消费者agent/src/e2e/replaySeal.ts的let seq=0n在每次重建(重启/重连)时归零 → 两代 sealer 在相同 (key, nonce) 下封不同明文 = 灾难性 AEAD 复用。第一轮曾误驳此项(verifier 看到"独立单调计数器"就类比 live 路径判安全,漏了 K_content 跨重启稳定);第二轮定向 re-attack + orchestrator 亲读replaySeal.ts证实为真。修法(用户选 epoch-in-key,4 包):ReplayKeyParams加epoch;deriveContentKey把 epoch 以sessionId‖0x1f‖epoch混入 HKDF salt;createReplaySealer每代randomUUID()新 epoch 并暴露;ReplaySource带 epoch、浏览器按之重推。验证(亲验):4 包 tsc 干净 + 全绿(relay-contracts 81 / relay-e2e 78[+2 F6 回归] / agent 133 / relay-web 99);re-audit 判闭合(回归证:两代同 seq=0 nonce 但不同 key)。残留(fail-closed 非复用):ring-buffer 传输(P1/P2)须持久化并按代下发 epoch —— 已记 TODO,当前 replay 路径尚未端到端接线(manage-page.loadReplay仍是 throw stub),故 F6 此刻是潜伏漏洞。完整报告docs/REVIEW_RELAY_SECURITY.md(含 F6 + RESOLUTION)。 - 动态 e2e 安全测试台(新增
e2e/包): 跨包 harnessbuildRelayWorld()把真实 P5(auth)+P4(crypto)+P2(agent replay) 通过内存 seam + 不可信 relay「RelaySpy」攻击者视角接起来(仅 I/O 边界 fake,所有安全检查走生产代码路径)。全流程 issue cap→upgrade/authz→handshake→sealed session 过 relay→reattach→revoke。21 测试全绿、tsc 干净(亲验)。把 F1/F2/F3/F4/F6 从「静态+单测」升级为「动态攻击者实测」,另加 MITM/reflection/replay/reorder/INV2(live+replay)/cross-tenant/single-use。F5(signCount)为返回形状改动、由 relay-auth 单测覆盖;RelaySpy 代替 term-relay mux(真多进程/浏览器 e2e 需尚未建的 run tooling)。用符号链接免npm install(e2e/node_modules/*→ 各包源码)。覆盖表见docs/REVIEW_RELAY_SECURITY.md。未提交,留待用户决定。
✅ 桌面客户端 v0.1 — Electron 一体化壳(代码完成 — 2026-07-01,分支 feat/desktop-electron,未提交)
Mac/Windows 桌面 App,内嵌现有 Node 服务器 + node-pty(all-in-one):主进程 startEmbeddedServer 复用 src/server.ts 的 startServer(cfg)/loadConfig(服务器零改动 —— 它本就导出可编程启动且 import 无副作用),窗口 loadURL('http://127.0.0.1:<port>/') 复用前端零改动(前端严格同源,location.host 自动指向内嵌服务器;Origin 白名单默认含 localhost)。核心价值 = 原生通知(轮询 /live-sessions → computeNotifications → 系统通知)+ 托盘常驻 + 深链 terminalapp:// + 开机自启。
- 编排: ultracode
Workflow—— 脚手架(orchestrator 冻结desktop/src/types.ts契约 + package/tsconfig/esbuild/electron-builder)→ Build(4 builder 并行、文件互斥、对齐冻结签名)→ Review(correctness/security/typescript 三 lens 并行、schema 结构化 findings)。纯逻辑尽量抽出可单测;Electron glue(main/window/tray/menu/preload/embedded-server/logger)作为不可单测 wiring 排除出覆盖率(沿用既有 vitest.config 先例)。无用 zod、无用 electron-store(遵循项目手写校验 + 手写 JSON 持久化的最小依赖惯例)。 - 交付(
desktop/src/17 文件 +test/desktop/9 测试文件): 纯模块port(pickFreePort)/shell(平台默认 shell,Win→powershell)/server-config(env 覆盖映射)/deep-link(解析+防注入)/notify-policy(通知策略)/notifications(批量 diff)/live-poll(边界校验 /live-sessions)/prefs(手写校验)/settings-store(JSON 持久化);glueembedded-server/window(加固:contextIsolation+sandbox+拒绝外域导航)/menu/tray/preload(最小 bridge)/main/logger。 - Review 结果: 无 CRITICAL/HIGH 安全问题(Electron 加固与不可信输入处理被评为扎实)。修复的真实 bug(orchestrator 直接改+补测): ① 冷启动深链丢失(缓冲 open-url/argv 到 ready 后派发) ②
createTray无守卫会崩启动(try/catch,失败无托盘继续) ③ 首轮轮询通知轰炸(首 tick 只建基线) ④ poller 捕获过期 prefs(每 tick 读 settings.get) ⑤ notify 'tool tool' 病句→'tool approval' ⑥ live-poll Windows 反斜杠路径 + 未过滤 exited ⑦NotificationsResult.next→ReadonlyMap。 - 验证: 桌面
tsc --noEmit0 错;全量 1401 tests 绿(基线 1307 +94 桌面,零回归);覆盖率全局 91.65/85.39/92.44/93.48(阈值 80×4 通过)、desktop/src97.56/95.04/100/97.81;esbuild 打包main.cjs/preload.cjs产出、electron/node-pty 保持 external、dist 走运行时动态 import。 - 本环境无法验证(留真机/CI,DESKTOP_PLAN P1 D5–D7):
.exe(Windows,需真机/无 wine)。审批门通知源(/live-sessions不带 pendingApproval)留 P2/D8 接 hook 侧信道 —— 策略已实现且测试,当前以 status→'waiting' 为代理。方案文档docs/DESKTOP_PLAN.md。 - ✅ Mac 打包 + 启动验证(2026-07-01,本机 arm64,已实测):
electron-builder出.dmg(arm64,深度 ad-hoc 签名)。关键修复:首版只打了 node-pty、漏了服务器其它 npm 依赖(express/ws/web-push),导致仅原地能跑(沿目录树借了仓库根node_modules),移到/Applications即崩Cannot find package 'express'。改electron-builder.yml:把dist/+public/+node_modules(过滤 electron/electron-builder/esbuild/typescript 等构建工具;node-pty 经npmRebuild为 Electron 43 ABI 重编译)全部铺到磁盘 Resources(Model 2,与embedded-server.ts的process.resourcesPath解析匹配),desktop/package.json 补 express/ws/web-push 依赖。仓库外副本直接执行验证通过:服务器起、GET /+/build/main.js → 200、/live-sessions → []、node-pty 加载 OK、无Cannot find报错。main.ts默认不注册开机自启(消除 native "Operation not permitted" 噪音)。Gatekeeper:未签名 ad-hoc 在 Sequoia(darwin24)双击被拦 →xattr -cr清 quarantine/provenance + 深度签名后open/双击可正常启动(实测 PID 存活、200)。产物desktop/dist-app/Web Terminal-0.1.0-arm64.dmg(~206M,含完整 node_modules)。仅 arm64;Intel 需--x64/--universal;Windows 需真机;对外分发需 Apple Developer 签名+公证。
✅ VC 语音命令映射 — 语音直接 approve/reject 权限门(完成 — 2026-07-01,分支 v0.6-projects,未提交)
上下文门控:仅当活动 tab 有 held 权限门(pendingApproval===true 且 gate==='tool')时,语音「确认/批准/yes…」→ 复用现有 {type:'approve'} WS 通道、「拒绝/取消/no…」→ {type:'reject'};其余一律口述不变。后端零改动。安全设计:整句精确匹配(否定/口水词攻不破)+ 前导否定守卫 + reject 优先 + 置信度门(仅 approve)+ 可撤销确认窗口(1.5s,提交时复检门身份/连接,防 TOCTOU) + stale-gate epoch。新增 public/voice-commands.ts(纯匹配器 100% 覆盖)+ public/voice-confirm.ts(确认窗口 100%),接线 tabs/voice/terminal-session/main.ts。1307 tests 绿,双 review(code+security)判 CRITICAL 已全修复并补测。规划 docs/PLAN_VOICE_COMMANDS.md,详见 Detailed Log 首条。待办:confidence fail-open 取舍留给用户;tabs.ts 968 行待拆分。
✅ v0.6 Projects 命名空间自动分组 + 跨设备 prefs(完成 — 2026-07-01,分支 v0.6-projects,未提交)
Projects 页去杂乱:按点分隔命名空间自动折叠成 section(Billo.Platform ·8 / Billo.Infrastructure ·2 / Other)+ 置顶 "Active now" + 无前缀时退化为扁平网格。收藏/折叠态改服务端持久化(GET/PUT /prefs → ~/.web-terminal-prefs.json,离线回退 localStorage 镜像 + 迁移旧 proj-favs)跨设备同步。1227 tests 绿,真浏览器验证折叠跨 reload 从服务端载回。详见 Detailed Log 首条。
✅ v0.6 Project Manager 项目工作台(完成 — 2026-06-30,分支 v0.6-projects)
-
全部 6 任务完成并验证(P1–P6,多 agent 编排)。398 测试全绿(自 341 +57)· 双 typecheck 干净 ·
build:webOK · 覆盖率 90.98/81.95/89.28/93.16(阈值 80×4 通过)· 真浏览器 smoke 通过。 -
编排方式: ultracode
Workflow动态编排——Build(P2 后端 ∥ P5 前端,module-builder)→ Wire(P4 端点 ∥ P6 接线)→ Review(module-reviewer验收 +typescript-reviewer+security-reviewer,report-only 并行)→ 对每个 HIGH/CRITICAL 对抗式 Verify(module-reviewer)→ Fix(module-builder仅改已确认项)。P6 首跑因 transientserver_error挂掉(未落任何改动)→ stop +resumeFromRunId恢复:P2/P4/P5 命中缓存秒回,P6 重跑成功。 -
交付:
- P2
src/http/projects.ts(15 测试,93.37% 覆盖):parseGitHead(纯) +buildProjects(cfg, liveSessions);BFS 扫 .git(深度上限、跳过 node_modules/dotdir/symlink、命中即停下钻)→ 读.git/HEAD分支 +git status --porcelaindirty(execFile无 shell、2s 超时、并发 8)→ 合并~/.claude/projectscwd(复用history.listSessions)→ 按 cwd 前缀归并 live sessions(每次 fresh)→ TTL 缓存(仅缓存磁盘发现)+ in-flight 去重。 - P5
public/projects.ts(纯 helper 30+ 测试) + style 追加:mountProjects(镜像mountLauncher);1:N 卡片(分支 chip / dirty 点 / session 行 /+ start claude here)、搜索过滤、收藏置顶(localStorage);.home-seg段控样式也在此定义。 - P4
src/server.ts:GET /projects(只读、无 Origin 守卫、try/catch 兜底[])+ 5 集成测试(真起 server)。 - P6
public/tabs.ts:openProject(同名#n后缀、addEntry(null,label,cwd,'claude\r'))+countOpenWithTitlePrefix+ 首页 Sessions↔Projects 段控(updateHomeView);7 新测试。
- P2
-
Review 12 findings → 4 HIGH 全确认并修复: ① A3 关键:
openProject/newTabForResume缺回车\r(claude 不自动执行)→ 补\r;② 段控被定位面板盖住 → 面板inset:44px 0 0 0;③mergeHistory串行 I/O →mapWithConcurrency;④ discover 并发缓存击穿 → in-flight promise 去重。 -
Orchestrator 追加加固(MEDIUM/LOW,本人直接改 + 补测): 前端
fetchProjects加normalizeProject(API 响应边界校验,缺sessions不再 TypeError)+getFavs过滤非字符串(localStorage 防污);config.parseProjectRoots相对路径 fail-fast(+ config 补 v0.6 解析测试,config.ts 95.87%→98%)。style.css1143 行 > 800 上限属既有债(v0.6 前已超),esbuild 仍打单文件,留作后续拆分。 -
真浏览器 smoke(
PROJECT_ROOTS=~/Documents/git,83 真 repo): 首页段控 Sessions/Projects 切换 ✓;Projects 面板 83 卡片(web-terminal显示branch:v0.6-projects+ dirty 正确,含feat/...斜杠分支)+ 搜索框 ✓;点web-terminal卡 → 开 tab 标签web-terminal、首页隐藏、终端出现 claude 启动界面(A3 端到端)✓;console 仅既有/favicon.ico404 + apple-mobile 警告,无 v0.6 报错。 -
待办: 真机/手机验收(同历史 F4/F7 遗留);可选 server.ts 抽 routes;
style.css拆分。 -
当前阶段: v0.3 全部完成(H1–H4 + M3/M6/M7 已并入 main;O2 历史浏览 在分支
o2-history,待合并)。- ✅ Step1 前端快赢 · ✅ H2/H4 状态感知 · ✅ H3 远程批准 · ✅ H1 tmux 保活 · ✅ M3/M7/M6 · ✅ O2 历史会话浏览/resume · ✅ tech-debt 清理(@ts-ignore 去掉)· ✅ UI:终端不再打印 Connecting/Connected(靠标签点)。
- ✅ 现代化 UI 主题(design-token 调色板/靛蓝强调色/圆角芯片标签+键栏/毛玻璃弹层)· ✅ 键栏功能字幕(每键下方标作用)+ 新增 ^R/^L/^D(共 17 键)· ✅ ⌨ 快捷键速查弹层(
public/shortcuts.ts)。 - ✅ v0.4 多设备共享会话:放宽不变式 #5 —— 一个 session 可挂多个 WS 客户端(镜像;输出/退出/状态广播,谁都能打字)。
GET /live-sessions+ 打开应用自动把主机活动会话显示为 tab;🔗分享二维码 /?join=<id>加入指定会话。 - ✅ 镜像尺寸 = 最近活跃端优先(latest-writer-wins):共享 PTY 只能一个尺寸,之前取最小值导致宽屏桌面被 iPad 夹小(iPad 全屏、桌面留黑边)。改为"最近 fit/聚焦的设备决定尺寸",你正在用的设备永远全屏;attach/detach/
blur都不改尺寸,前端在 pane 显示 + 窗口聚焦时重发 dims(切设备即夺回全屏)。实测两端:200×50 → iPad 100×40 → 桌面 refit 200×50 → iPad blur 不变。后端纯客户端实测镜像输出 + 共享输入通过。 - ✅ 🗂 会话管理页(
/manage.html独立页,非弹窗):实时预览缩略图网格 —— 每张卡片用只读 xterm 渲染该 session 当前屏幕(像截图,缩放),一眼看出每个 session 在干嘛;RingBuffer.tail()+GET /live-sessions/:id/preview(不 attach、不影响观看数/回收),每 4s 刷新;可点缩略图/Open 进入、Kill / 批量 Kill。实测 3 个 session 缩略图(top / git log / ls)彩色可辨,Kill 3→2→0。 - ✅ v0.5 首页 = 会话选择器(launcher):打开应用不再自动建/恢复 tab,而是落到选择器 —— 主机所有运行中 session 的实时缩略图网格,用户自己点开(回放完整 scrollback)或
+ New session;关掉最后一个 tab 回到选择器。public/launcher.ts,复用.mg-*卡片样式。实测:首次加载 0 tab + 缩略图;Open→建 tab、选择器隐藏;关最后一个→选择器回来。 - ⬜ 仅剩:O1 token 认证(可选,默认关)+ F4/F7 真机验收(局域网/手机)。
- 225 测试全绿;工具栏 🔍搜索 ⚙设置 ▦仪表盘 🕘历史 ⌨快捷键 🔗分享 🗂管理 📱QR。
- 下一步: O1(可选)或真机验收。
- v0.2/v0.1 历史见下方条目。
-
Wave: W0–W5 基本完成。v0.1 功能完整、测试与真机浏览器验证通过。仅剩 2 项需用户真设备。
-
进度: 20/21 任务(T19 待真手机)· 187 自动化测试全绿 · 真 PTY 端到端验证 · 真浏览器 smoke 通过。
-
验收 F1–F9: ✅ F1/F2(浏览器 smoke:渲染+输入输出+全彩)· ✅ F3(resize 无错)· ⬜ F4 需局域网真机 · ✅ F5/F6(集成 ⑤⑥ 回放 CJK/ANSI)· ⬜ F7 需真手机(软键盘栏)· ✅ F8(浏览器 exit→重连提示)· ✅ F9(集成+curl 401)。
-
下一步(交用户): 真手机/另一台电脑开
http://<你IP>:3000实测 F4(局域网可用)与 F7(Esc/Shift+Tab/方向键等触摸键生效)。 -
阻塞: 无(F4/F7 非阻塞,属需物理设备的人工验收)。
-
次要 tech-debt: main.ts 1 处
@ts-ignore(CSS import);未来加*.cssd.ts。 -
最后更新: 2026-06-20(并入 review 全面修复,见下方条目)
维护性修复条目 (Review Fixes)
2026-06-20 · 按 REVIEW_REPORT 逐项修复(安全/架构/质量/测试)
- 状态:
[x]DONE(typecheck 干净 · 341 测试全绿(16 文件,自 228 起 +113)·build:web通过 · 覆盖率 90.3/80.7/87.6/92.4 ≥80 阈值已在vitest.config.ts强制)。 - 来源: 4 个 agent 并行 review 产出
docs/REVIEW_REPORT.md;用户决定修全部四档,/sessions保持现状仅加说明,clientDims/blur死状态彻底删除。计划见~/.claude/plans/piped-marinating-parrot.md。 - 🔴 关键:
- 多设备审批竞态(
server.tsws.close):仅当最后一个 client 离开时才resolvePending—— 关一个镜像不再取消另一台正在看的待批准。 - 会话无上限 DoS:新增
Config.maxSessions(默认 50,envMAX_SESSIONS)+manager.assertUnderSessionCap(),超限抛错复用 M4 的 exit(-1) 路径。 - 信号处理器泄漏:
onSignal/onUncaught提具名引用,close()里process.off全移除(去掉测试setMaxListeners(50)遮掩)。 - 前端
initialInput700ms 定时器:跟踪initialInputTimer、dispose()清理、disposed守卫、INITIAL_INPUT_DELAY_MS常量。 tabs.ts addEntry的null as unknown as TerminalSession类型洞:先建 session 再建 entry。
- 多设备审批竞态(
- 🟠 应修: 安全头中间件(无 helmet)+
DELETE /live-sessions[/:id]Origin/CSRF 守卫(403);history.ts改fs/promises(listSessions异步,/sessionsawait);彻底删除clientDims+blur(types/session/protocol/server/manager 注释/terminal-session.hide + 相关测试);WS 连接级限频(maxMsgsPerSec默认 2000,超限丢帧不断连);/sessions保持行为,加注释 +TECH_DOC §7记录已接受风险。 - 🟡 测试: 新增
test/{tmux,preview-grid,terminal-session,tabs}.test.ts(后两者 jsdom + mock WebSocket);扩history/config/manager/integration覆盖 killById/handleHookEvent/maxSessions/resolveUseTmux/live-sessions/preview-404/approve-reject/origin-guard/限频/信号泄漏回归。 - ⚪ 卫生:
parsePositiveInt→parseNonNegativeInt;ALLOWED_ORIGINS scheme 校验;server.ts日志注入 sanitize +isLoopback健壮化(127.0.0.0/8 + IPv4-mapped);运维常量(PERM_TIMEOUT_MS/REAP_INTERVAL_MS/PREVIEW_BYTES)入 Config;抽public/preview-grid.ts消除 launcher/manage 重复(类型复用LiveSessionInfo);ARCHITECTURE §8 记录运行时句柄可变例外(H6)+ #5/#7/#8 更新;CLAUDE.md「auto-show as tabs」→ v0.5 launcher;「min across clients」陈旧注释清扫。 - 验证:
npm run typecheckPASS ·npx vitest run341/341 ·npm run build:webOK ·npx vitest run --coverage退出 0(阈值 80×4)。 - 决策 / 偏离: 覆盖率
include收窄到src/**+ 四个有逻辑的前端模块,排除纯 DOM 接线/入口胶水(E2E 范畴),未下调任何阈值。未把审批状态机搬进 manager(报告 #10)—— close-handler 修法已正确最小,server.ts抽 routes 留作可选后续。 - 遗留 / 待办: 真机验收 F4/F7(同 v0.1 遗留);可选 server.ts 抽
http/routes.ts;胶水文件若要纳入覆盖率需补 E2E。 - commit: (本次提交)
任务总览 (Task Overview)
任务定义见 PLAN.md;按 Wave 分组。
Steps完成数 = PLAN 中该任务的勾选项。 多 agent 并行:同一 Wave 内任务互不依赖,可并行;跨 Wave 按依赖推进。
| 任务 | Wave | 模块 | 状态 | Steps (完成/总) | 完成日期 |
|---|---|---|---|---|---|
| T1 | W0 | 脚手架 + 装齐依赖 | [x] |
5/5 | 2026-06-16 |
| T2 | W0 | src/types.ts 共享契约(冻结源) |
[x] |
8/8 | 2026-06-16 |
| T3 | W0 | mock IPty 测试替身 | [x] |
2/2 | 2026-06-16 |
| T4 | W1 | config.ts |
[x] |
5/5 | 2026-06-16 |
| T5 | W1 | protocol.ts |
[x] |
4/4 | 2026-06-16 |
| T6 | W1 | ring-buffer.ts |
[x] |
4/4 | 2026-06-16 |
| T7 | W1 | http/origin.ts |
[x] |
3/3 | 2026-06-16 |
| T8 | W1 | public/index.html |
[x] |
1/1 | 2026-06-16 |
| T9 | W1 | public/style.css |
[x] |
1/1 | 2026-06-16 |
| T10 | W1 | public/keybar.ts |
[x] |
4/4 | 2026-06-16 |
| T11 | W1 | public/main.ts |
[x] |
6/6 | 2026-06-16 |
| T12 | W2 | session/session.ts |
[x] |
7/7 | 2026-06-16 |
| T13 | W3 | session/manager.ts |
[x] |
6/6 | 2026-06-16 |
| T14 | W4 | server.ts 接线 |
[x] |
6/6 | 2026-06-17 |
| T15 | W4 | 集成 / E2E 测试 | [x] |
2/2 | 2026-06-17 |
| T16 | W5 | 验收 F1–F3(终端/TUI/resize) | [x] |
3/3 | 2026-06-17 |
| T17 | W5 | 验收 F4+F9(LAN/Origin) | [~] |
1/2 | 2026-06-17 |
| T18 | W5 | 验收 F5+F6(保活/回放) | [x] |
2/2 | 2026-06-17 |
| T19 | W5 | 验收 F7(移动键栏) | [ ] |
0/1 | 需真机 |
| T20 | W5 | 验收 F8(退出处理) | [x] |
2/2 | 2026-06-17 |
| T21 | W5 | 安全核对 + README + LOG 收尾 | [x] |
3/3 | 2026-06-17 |
验收清单映射 (Acceptance — TECH_DOC §8)
功能验收独立于阶段勾选;F* 全绿才算 v0.1 完成。
| 编号 | 功能 | 状态 | 验证方式 / 备注 |
|---|---|---|---|
| F1 | 交互式终端(ls/补全/Ctrl+C/历史) | [x] |
浏览器 smoke:输入→输出往返 |
| F2 | 全彩 + 全屏程序(vim/top/claude) | [x] |
浏览器 smoke:彩色提示渲染(深 TUI 同理) |
| F3 | 终端自适应(resize 不错位) | [x] |
viewport 变更存活、无 error |
| F4 | 局域网访问(手机/他机) | [ ] |
需真机(同 WiFi 开 http://:3000);Origin 白名单已含网卡 IP |
| F5 | 会话保活(断开重连仍在跑) | [x] |
集成 ⑤;tmux 重启保活(H1) |
| F6 | 重连回放(环形缓冲) | [x] |
集成 ⑥:ANSI/中文不乱码(M2) |
| F7 | 移动快捷键栏 | [ ] |
需真手机(触摸键生效、不弹软键盘) |
| F8 | 进程退出处理 | [x] |
浏览器 smoke:exit→重连提示;detach 后退出 L1 |
| F9 | Origin 校验(401) | [x] |
集成 ①②;curl 坏 Origin→401;/term 限制(L3) |
详细日志 (Detailed Log)
倒序,最新在最上。每个子任务一条。复制下方模板填写。
2026-07-01 · VC 语音命令映射 — 上下文门控 confirm/reject(多 agent 编排 + 双 review)
-
状态:
[x]DONE(未提交,分支v0.6-projects) -
背景: 已有 v0.7 A2 push-to-talk 只做「语音→文字」口述;本次加语音命令层,让语音直接 approve/reject 权限门。规划见
docs/PLAN_VOICE_COMMANDS.md(多 agent:理解→前后端计划→4 路对抗 review→综合)。锁定决策:①上下文感知(仅当活动 tabpendingApproval===true才当命令,否则口述不变)②只做 confirm 类,复用现有approve/rejectWS 通道(后端零改动)③危险键不接线,two-step 组件仅用于 approve 确认窗口。 -
改动:
- 新增
public/voice-commands.ts(纯匹配器,100% 覆盖):matchCommand= 整句精确 + 前导否定守卫 + reject 优先 + 置信度门(仅 approve);中英确认/否定词表。 - 新增
public/voice-confirm.ts(确认窗口控制器,100% 覆盖):createApproveConfirm(1500)arm/cancel/isArmed,纯计时(注入 setTimeout,无 DOM)。 public/voice.ts:onTranscript多传confidence(仍只转写)。public/terminal-session.ts:加pendingEpoch(false→true 翻转自增,stale-gate nonce)。public/tabs.ts:handleVoiceTranscript分派 + 确认窗口 arm + WS 断开闪「未连接」+ 单独#voice-confirmoverlay(避 hideInterim 清屏)。public/main.ts:接onVoiceTrigger点亮 🎤。vitest.config.ts:两新模块入 coverage.include(orchestrator 协调编辑)。- 测试:
test/voice-commands.test.ts/test/voice-confirm.test.ts新建;test/tabs.test.ts加 VC 集成(含 6 条 review-fix 用例);test/voice.test.ts4 处旧断言随(text,confidence)新签名更新 + 补 confidence 转发断言。
- 新增
-
验证:
npm run typecheck干净;npm test→ 1307 passed / 43 files;build:webOK;覆盖率 voice-commands/voice-confirm 100×4、tabs.ts 94/84/89/97、TOTAL 91.3/84.6/91.7/93.2(80×4 通过)。git diff src/空(后端 NONE 成立)。 -
决策 / 偏离 PLAN:
- 实现 workflow 首跑失败(review 阶段某 agent 6 次 stall)——但 Build V1/V2 已落文件;改为 orchestrator 亲自跑验证 + 用普通 agent 并行做 code/security review(避开 workflow 卡死)。
- 两份 review 独立收敛,判 CRITICAL/BLOCK,均已修复并补测:①确认窗口 TOCTOU:
onCommit现复检pendingApproval && gate==='tool' && pendingEpoch===armedEpoch && connected,否则丢弃;②**cancel()从未接线**:接入新话语顶替(handler 顶部)、reject 分支、#voice-confirm点击;③WS 断开延迟提交错位:提交前复检连接。 - 偏离 PLAN §4:「松开麦克风取消」在 PTT 时序下不成立(松麦克风正是触发最终转录/arm 的动作),改为 tap + 新话语顶替 + reject 取消 三条真实取消路径。
startsWithNegation重写(review MEDIUM):原prefix+rest[0]实际只测 prefix0,改为显式测rest。
-
遗留 / 待办:
- confidence fail-open(review MEDIUM,保留):
confidence===undefined时置信度门短路(符合 §D「有置信度时才门控」),靠确认窗口兜底误听;若要 fail-safe(undefined→不批准)是一行改动,但会让不报告置信度的引擎(部分非 Chromium)语音批准失效——留给用户定。 public/tabs.ts已 968 行 > 800 上限(既有债 + 本次 ~99 行):本次仅加最小 diff,待后续抽voice-controller拆分(decision F)。
- confidence fail-open(review MEDIUM,保留):
-
commit: N/A(未提交)
-
状态:
[x]DONE。双 typecheck 干净 ·build:webOK · 全量vitest run1227 passed (41 files) · 真浏览器端到端验证通过。 -
需求(用户): Projects 页扁平网格太乱(50–100 repo)。多 agent 探索(4 路:自动分组/手动文件夹/密度分段/竞品调研)结论一致 → 主方向=按点分隔命名空间自动分组(repo 名如
Billo.Platform.Payment天然带层级),配 Active-now 置顶;手动文件夹只是加分项非主线。用户选:范围=自动分组+Active band,持久化=服务端(收藏/折叠态跨设备同步,修掉 localStorage 每设备各一份的缺陷)。 -
改动:
- 后端:
src/types.ts加UiPrefs {favourites[], collapsed{}}+Config.prefsStorePath;src/config.ts解析PREFS_STORE_PATH(默认~/.web-terminal-prefs.json,仿 push-store);新增src/http/prefs-store.ts(sanitizePrefs/loadPrefsStore,镜像 subscription-store:全字段有界强制、缺失/损坏→空默认、不可变替换);src/server.ts加GET /prefs(只读无 Origin 守卫,同 /projects)+PUT /prefs(状态变更→requireAllowedOriginCSRF 守卫 + body sanitize)。 - 前端: 新增
public/prefs.ts(loadPrefs/savePrefs:服务端为准、离线回退 localStorage 镜像、迁移旧proj-favs);public/projects.ts加纯函数groupProjects()(depth-2 命名空间键 +MIN_GROUP_SIZE=2阈值→单成员落 "Other" + 无命名空间收益时退化为单 flat 组 + 命名空间组按最近活跃排序 + 复制活跃项进置顶 "Active now" 组)、displayLabel()(组内卡片只显示前缀后的尾段,如Payment)、makeGroupHeader()(caret+label+count+"N active" 徽标,可折叠/键盘可达,Active-now 不可折叠);makeProjectCard加可选displayName形参;renderGrid改为按组渲染(每组一个.proj-grid-cards子网格 + 折叠态来自内存 prefs + 搜索时强制展开命中组 + 保留 scroll);mountProjects状态改用内存favs/collapsed,init()首次loadPrefs后再渲染,5s tick 只拉 projects 不重载 prefs(避免覆盖本地编辑);折叠/收藏改走persist()。style.css:.proj-grid改为纵向 section 栈 + 新.proj-grid-cards承接原 minmax 网格 +.proj-group*组头(sticky、hover、focus-visible、● N active绿色徽标)。 - 未做(按用户选的最小范围): 密度/紧凑列表切换、group-by 控件、frecency、模糊搜索、手动文件夹——均为探索报告里的后续可选层。
- 后端:
-
验证:
npx tsc --noEmit干净;npm run build:webOK(bundle 665kb);vitest run1227 passed(新增test/prefs-store.test.ts12 项 sanitize/load/persist +test/projects-panel.test.ts加 21 项 groupProjects/displayLabel)。真浏览器(临时 server + 假点名 repo):Projects 页渲染出▼ Billo.Platform ·8(卡片尾段 B2B.Api/Payment/…)、▼ Billo.Infrastructure ·2(EventBus/MongoDb)、▼ Other ·12;点组头折叠 → live DOMcollapsed+0 卡、PUT /prefs落{"Billo.Platform":true};整页 reload 后仍折叠(从服务端 /prefs 载回)=跨设备持久化打通。控制台 503/404 为既有(无 VAPID 的 push + 缺图标),非本次改动;/prefs、/projects、/config/ui 均 200。 -
决策 / 偏离 PLAN: 无既有 PLAN 覆盖此增量;探索用 4 个并行 general-purpose agent(用户明确"用多 agent 探索")。
getFavs/saveFavs仍导出(旧测试引用)但 mountProjects 不再调用,由public/prefs.ts取代。 -
遗留 / 待办: ① 两个同名 depth-2 repo(如两个
Billo.Customer)会成一个以自身命名的组、卡片显示全名(displayLabel 无尾段)——功能正确、观感略冗余,可接受。② 5s tick 不重载 prefs → 另一设备的改动要下次进页才见(单用户可接受)。③ 后续如上手动文件夹/密度切换,持久化接口已就绪(prefs-store 可扩字段)。 -
commit: (待提交)
2026-06-30 · v0.7 Walk-away Workbench — 多 agent 并行实现 (Band A+B)
- 状态:
[x]BUILT + 全量门禁绿(详细验收/打磨待续)。双 typecheck 干净 ·build:webOK · 全量 vitest + 覆盖率 91.37/84.06/92.18/93.37(≥80×4)。 - 实现: 按
docs/PLAN_WALKAWAY_WORKBENCH.md的 27 任务波次(R0→W0→W1×14→W2→W3→W4)以module-builder并行实现;23 任务 done, 0 blocked。功能: A1 Web Push + 锁屏审批(新依赖 web-push)、A2 语音口述、A3 快捷回复 chips + 提示词调色板、A4 活动时间线、A5 卡住/静默告警;B1 只读 git diff、B2 statusLine 遥测仪表、B3 从 UI 建 worktree、B4 plan/权限模式中继。新增src/push/*、src/http/{diff,statusline}.ts、public/{diff,timeline,...}.ts、statusLine 脚本等;扩 hook/manager/server-wire/termsession/tabs/projects-ui/sw。 - Orchestrator 修复 (W0 协调缺口): T-types 漏给
Config接口加 21 个 v0.7 字段(只在 config.ts 返回对象里);手工把 21 字段补进src/types.ts的 Config + 调config.ts(签名回:Config、base 去 satisfies)使其编译。移除过严的 W0 gate(全量 tsc 在契约领先实现期本就不能过——该检查归 W4/收尾)后resumeFromRunId续跑 W1–W4。 - W4 评审: 安全 6 项全过(每个状态变更路由带 Origin/CSRF;VAPID 私钥不外泄/不记日志;git
execFile无注入;WS Origin 防御无回归)。遗留(非阻塞, 无 CRITICAL/HIGH): ① protocol.tsapprove.mode校验应入parseClientMessage(现 server.tsparseApproveMode兜底, 功能与安全正确)② CSPconnect-src去掉ws:/wss:通配(既有, 低危)③ worktreebaseref 加格式校验 ④ protocol/preview-grid 补 2 个针对性测试。完整 27 条任务日志见 workflow result 文件。 - 验证:
npx tsc -p tsconfig.json/.web.json --noEmit干净;npx vitest run --coverage全绿 ≥80×4;npm run build:webOK;web-push已 npm install。 - commit: (本次提交)
2026-06-30 · v0.6 工具栏图标换成线性图标 + hover tooltip
- 状态:
[x]DONE。web typecheck 干净 ·build:webOK · 真浏览器验证通过。 - 需求(用户): 工具栏那些 emoji 图标(🔍⚙▦🕘⌨🔗📱)不好看,换成符合主题色的图标;hover 时显示图标含义。
- 改动(纯前端):
public/icons.ts加 7 个 lucide 线性图标(MIT,stroke=currentColor,脚本从 unpkg 抓取生成):ICON_SEARCH/SETTINGS/DASHBOARD/HISTORY/KEYBOARD/SHARE/DEVICE。7 个工具栏模块(search/settings/dashboard/history/shortcuts/share/qr)把textContent=emoji换成innerHTML=ICON_*+dataset.tip(tooltip 文案)+aria-label。style.css.toolbtn:svg18px、默认--text-dim、hover 变 Amber;新增[data-tip]::afterCSS hover tooltip(右对齐避免右边缘裁切)。 - 验证:
npx tsc -p tsconfig.web.json干净;build:webOK;bundle 内 0 toolbar emoji;真浏览器:7 按钮均为线性图标(--text-dim色)、hover 变琥珀 + tooltip(实测 "All sessions" 等显示)。 - 遗留: share/qr 的 aria-label 仍是旧描述(无害,屏幕阅读器友好);可选统一。
- commit: (本次提交)
2026-06-30 · v0.6 "New session" 改为网格卡片(去掉别扭的大按钮)
- 状态:
[x]DONE。web typecheck 干净 ·build:webOK · 真浏览器验证通过。 - 需求(用户): 头部那个大的实心琥珀 "+ New session" 按钮很别扭(与精致的分段 pill 不协调)。
- 改动(纯前端
public/launcher.ts+ style): 删头部.launcher-new按钮 +.launcher-actions;改为 grid 内的.mg-new-card虚线"+ New session"瓦片(与 session 卡同尺寸,始终排在网格第一格,每次 refresh 重新 prepend),hover 变琥珀。删冗余.mg-empty空态(瓦片即 CTA)。头部只剩标题 + 段控,更干净。 - 验证:
npx tsc -p tsconfig.web.json干净;build:webOK;preview-grid测试 14 全绿;真浏览器:Sessions 网格首格为虚线 New 瓦片 + 其后 session 卡(Open/Kill),无旧大按钮。 - commit: (本次提交)
2026-06-30 · v0.6 删除 manage 独立页 — Sessions 选择器即会话管理
- 状态:
[x]DONE。453 测试全绿 · web typecheck 干净 ·build:webOK(仅 main,无 manage 产物)· 真浏览器验证通过。 - 需求(用户): manage 页面不要了——在 Sessions 选择器这里直接管理 session(添加 + 删除)。
- 改动:
- 删
public/manage.html+public/manage.ts(及 build 产物);package.json的build:web/dev:web去掉public/manage.ts入口。 - 删
public/main.ts工具栏 🗂 按钮(原跳/manage.html);删public/launcher.ts头部 "🗂 Manage" 链接。 - 加
launcher.ts每张 session 卡 Kill ✕(makePreviewCard的extraActions,复用 manage 的killOne:DELETE /live-sessions/:id→ refresh)。.mg-kill样式已存在,直接复用。 - 后端 DELETE/preview 路由保留(仍由 launcher kill + 缩略图 + projects 复用);注释里的 "manage page" 字样属历史,无害。
- 删
- 结果: Sessions 视图 = 完整会话管理——+New session(加)· Open ↗(进)· Kill ✕(删),配实时缩略图。bulk「Kill all/detached」未迁移(如需可加)。
- 验证:
npx tsc -p tsconfig.web.json干净;npx vitest run453/453;build:web仅出 main(无 manage.*);真浏览器:Sessions 卡含 Open+Kill、无 Manage 入口、点 Kill 2→1、GET /manage.html→404。 - 遗留: manage-only 死 CSS(
#manage-root/.mg-header/.mg-bar/.mg-title/.mg-sub/.mg-btn)留作可选清理;README 若提 manage 页待更新。 - commit: (本次提交)
2026-06-30 · v0.6 项目网格卡片 — 直接关掉 session
- 状态:
[x]DONE。51 projects-panel 测试全绿(+1)· web typecheck 干净 ·build:webOK · 真浏览器验证通过。 - 需求(用户): Projects 页面(网格卡片)下应该能直接关掉 session(此前只有详情页有 Kill,卡片 session 行只能点进入)。
- 改动(纯前端
public/projects.ts+ style):makeSessionRow加可选onKill→ 行尾 ✕ 按钮(.proj-session-kill,stopPropagation 不触发进入);makeProjectCard加可选onKillSession透传;mountProjects加killAndRefresh(DELETE /live-sessions/:id后 refresh)传入网格卡片。CSS.proj-session-kill(hover 变红)。 - 测试(+1): 卡片 session 行——无 onKillSession 时无 ✕;有时点 ✕ 调 onKill(id) 且不触发 onEnterSession。
- 验证:
npx tsc -p tsconfig.web.json干净;npx vitest run projects-panel51/51;真浏览器:web-terminal 卡 2 个 session 行 → 点 ✕ → 杀掉并刷新为 1 行。 - commit: (本次提交)
2026-06-30 · v0.6 项目详情页 — 展示 CLAUDE.md + /init 生成/更新按钮
- 状态:
[x]DONE。458 测试全绿(+5)· 双 typecheck 干净 ·build:webOK · 真端到端(curl + 真浏览器)验证通过。 - 需求(用户): 详情页展示 CLAUDE.md,加一个按钮自动更新 / 生成 CLAUDE.md。决策(AskUserQuestion): 展示 + 一个交互式 /init 按钮(非静默后台覆盖,便于 review)。
- 改动:
- 后端:
ProjectDetail加hasClaudeMd/claudeMd?;projects.tsreadClaudeMd(读<repo>/CLAUDE.md,截断 64KB,best-effort)并入buildProjectDetail的 Promise.all。 - 前端:
renderProjectDetail加 CLAUDE.md 段——标题行带按钮(有则↻ Update、无则✨ Generate),内容滚动框(.proj-claudemd,monospace,max-h 320);无则空态提示。按钮 →onOpenProject(path, name, 'claude "/init"\\r')开交互式 Claude 跑/init(复用 launcher,零新后端,byte-shuttle 不变)。style 加.proj-section-row/.proj-claudemd-btn/.proj-claudemd。 - 测试(+5): buildProjectDetail 读/无 CLAUDE.md;renderProjectDetail 有内容+Update、无内容+Generate+空态、按钮调
onOpenProject(...,'claude "/init"\\r')。
- 后端:
- 验证:
npx tsc(双)干净;npx vitest run458/458;build:webOK;curl/projects/detail返回hasClaudeMd:true+ 13KB 内容;真浏览器:web-terminal 详情显示 CLAUDE.md 滚动框 + ↻ Update 按钮(段序 Branch/Active sessions/CLAUDE.md/Start)。 - 注:
/init经claude "/init"作为初始 prompt;若该形式不直接触发斜杠命令,会话是交互式的、用户可手动输/init(一行可调)。 - commit: (本次提交)
2026-06-30 · v0.6 项目详情页 — branch/worktree + 详细 active session
- 状态:
[x]DONE。452 测试全绿(+21)· 双 typecheck 干净 ·build:webOK · 覆盖率 90.16/81.32(worktrees.ts100% /projects.ts93.8%)· 真端到端(curl + 真浏览器)验证通过。 - 需求(用户): 每个 project 的详情页——详细显示具体 active 的 session,以及具体的 branch 或 worktree。
- 设计: 点项目卡的名字进入应用内详情视图(
renderProjectDetail,在 Projects 面板内切换,带"← All projects"返回)。分区:① 头部(名/路径/branch chip/dirty)② Branch / Worktrees(git worktree list每条:branch 或detached @ head+ main/current/locked/prunable 标签 + 路径;单 worktree 即显示分支)③ Active sessions(N)(每条:状态点 + 标题 +状态·👁设备数·started 多久前+ Open + Kill ✕)④ Start 启动器行(复用 Claude/Codex/Code)。再进 Projects 总是回到网格(不残留详情)。 - 后端: 新增
src/http/worktrees.ts(parseWorktrees(纯)+listWorktrees,git worktree list --porcelain,execFile 无 shell,2s 超时,best-effort→[]);projects.ts加buildProjectDetail(cfg, path, liveSessions)(校验绝对存在目录→否则 null;复用 readBranch/readDirty/hasGitEntry/matchSessions;不缓存,详情按需要新鲜)。src/server.tsGET /projects/detail?path=(只读、无 Origin 守卫,同 /projects;path 缺→400、非法/不存在→404)。types 加WorktreeInfo/ProjectDetail。 - 前端(
public/projects.ts+ style):fetchProjectDetail/killSession;makeProjectCard加可选onOpenDetail(名字变可点链接.proj-name-link);mountProjects加 grid↔detail 视图切换 + 自动刷新路由(详情每 5s 刷新);Kill 走DELETE /live-sessions/:id(同源带 Origin)。详情视图样式.proj-detail*/.proj-wt-*/.proj-dsession*。session meta 在 status=unknown 时省略状态点(不显示· ·)。 - 测试(+21):
test/worktrees.test.ts(parse:单/多/detached/locked/prunable/无尾空行/空);projects.test.tsbuildProjectDetail(相对/不存在/文件→null、非 git 目录、假 repo 读 branch、session 按 cwd 前缀归并、真 git init worktree(realpath 规避 /var symlink));projects-panel.test.tsrenderProjectDetail(null→not found+Back、头部、worktree 行+current tag、detached、session Open/Kill 接线+忽略 exited、空态+启动器、非 git)+ 名字链接。 - 验证:
npx tsc(双)干净;npx vitest run --coverage452/452(≥80×4);真浏览器:开 web-terminal 的 Claude session→进详情→显示 branchv0.6-projects(MAIN/CURRENT)+ Active sessions(1)(web-terminal,👁1,Open/✕)+ Start 启动器 + 返回。curl/projects/detail返回 worktree(main/current/head)+ 缺/相对 path→400/404。 - 安全:
/projects/detail只读(git branch/status/worktree-list,execFile 无 shell + 超时),path 校验绝对存在目录;与 /projects 同威胁模型(不新增可变更面)。 - commit: (本次提交)
2026-06-30 · v0.6 项目卡启动器 — Claude · Codex · VS Code logo 按钮
- 状态:
[x]DONE。覆盖率 90.6/81.48/88.71/92.71(≥80×4)· 双 typecheck 干净 ·build:webOK · 真浏览器 + 真 VS Code 端到端验证通过。 - 需求(用户): 把卡片上大的"+ start claude here"换成 Claude logo;新增 VS Code logo(点击在主机打开 VSC)与 Codex logo 按钮;logo 编排要"符合应用性"。
- 设计: 每张项目卡的动作区改为启动器行——3 个品牌 logo 图标按钮(logo 在上、小字标签在下、touch 友好、各带 tooltip),有运行中 session 时 session 行列在启动器上方。Claude=珊瑚色爆发、Codex=OpenAI blossom(近白)、Code=VS Code 蓝丝带;logo 用
fill=currentColor,颜色由 CSS 品牌色驱动。 - 行为: Claude→新终端 tab 跑
claude;Codex→新终端 tab 跑codex(复用openProject(path,name,cmd),onOpenProject加cmd?参数);VS Code→POST /open-in-editor让主机跑code <path>(文件在主机,故在主机开;不分查看设备)。主机已确认装有 claude/codex/code。 - 活跃高亮(用户追加): 项目若有活跃 Claude session(运行中且 hook 状态 ≠
unknown——纯 shell/codex 保持 unknown 不算),Claude logo 按钮珊瑚色高亮(coral ring + 淡底 + 浅色标签);>1 个时角标显示数量。activeClaudeCount(project)判定。 - 改动:
- 新增
public/icons.ts(simple-icons 的 Claude/OpenAI/VSCode 单路径 SVG,脚本生成避免手抄出错)。 public/projects.ts:makeLauncherRow/makeLauncher+openProjectInEditor(fetch/open-in-editor);ProjectsHooks.onOpenProject加cmd?;导出makeProjectCard便于测试;删旧.proj-new用法。public/tabs.ts透传 cmd。public/style.css新增.proj-launchers/.proj-launch*(品牌色),删旧.proj-new规则。- 后端:
src/http/editor.tsopenInEditor(cfg,path)(校验绝对路径+存在+目录→execFile(无 shell)启动,best-effort,error 事件兜底+unref);src/server.tsPOST /open-in-editor(Origin 守卫,与 DELETE 同档);Config.editorCmd(EDITOR_CMD,默认code)入 types/config。 - 测试: 新增
test/editor.test.ts(校验 400/404 +editorCmd='true'成功 204);config.test.ts加 editorCmd;projects-panel.test.ts加 makeProjectCard 启动器行(3 按钮/logo/Claude·Codex cmd)。
- 新增
- 验证:
npx tsc(双)干净;npx vitest run --coverage全绿(≥80×4);build:webOK。真端到端:curl/open-in-editor无 Origin→403、缺/相对/不存在 path→400/400/404、合法 dir→204 且 VS Code(Electron pid)实际打开;真浏览器 Projects 视图每卡 3 logo 按钮(珊瑚/白/蓝,标签 Claude/Codex/Code,tooltip 正确)。 - 安全:
/open-in-editor状态变更→Origin 守卫(CSRF);execFileargv 传参无 shell 注入面;path 校验为绝对存在目录;不超出"应用本就给主机 shell"的既有威胁模型。命令来自 config 非用户。 - commit: (本次提交)
2026-06-30 · v0.6 UI 打磨 — 首页段控重设计 + 选择器隐藏键栏
- 状态:
[x]DONE。415 测试全绿 · 双 typecheck 干净 ·build:webOK · 真浏览器验证通过。 - 反馈(用户): ① Sessions/Projects 段控在 UI 上"有点别扭"(原为两个独立描边方块浮在顶部);② 首页(Sessions/Projects 视图)底部那排终端快捷键栏没必要显示。
- 修复(纯前端,
public/style.css+public/tabs.ts): ①.home-seg重设计为 iOS 风分段控件——一个圆角"轨道"(surface-2底 + 圆角 999px + 内边距)内含两个 pill 按钮,激活项填 Amber;width:fit-content + margin:auto居中;去掉 first/last/only-child 圆角拼接;面板inset44→68px 适配新高度。②TabApp.updateHomeView在显示首页时给document.body加home-open类,CSSbody.home-open #keybar{display:none}隐藏键栏;打开 tab(终端激活)即恢复。 - 追加(用户反馈"段控独占一行、空白太多"): 桌面端(
@media min-width:769px)把段控position:absolute浮到右上角,与过滤框/启动器表头同一行;面板inset归 0(从顶部铺满),.launcher-head留 240px 右内边距避开段控;移动端保持居中堆叠(防止与全宽过滤框重叠)。另:首页隐藏键栏时body.home-open #term{bottom:0}收回键栏占位。纯 CSS(style.css直出,无需打包)。真浏览器实测:Projects 段控与过滤框同行(seg.top 56 / filter.top 62)、卡片在其下;Sessions 视图段控不与 New/Manage 重叠。 - commit: (本次提交)
2026-06-30 · v0.6 UX 修复 — 会话内无法返回 Projects 首页
- 状态:
[x]DONE。415 测试全绿(+3)· 双 typecheck 干净 ·build:webOK · 真浏览器验证通过。 - 问题(用户反馈): 打开某个项目进入终端 tab 后,首页的 Sessions/Projects 段控被隐藏,只能关掉所有 tab 才能回到选择器再开下一个项目——没有"返回 Projects"的入口。
- 修复(
public/tabs.ts+public/style.css): 标签栏新增 ⌂ Home 按钮;点击把首页选择器(Sessions/Projects 段控 + 对应面板)叠加显示在当前 tab 之上(homeForced标志:updateHomeView改为tabs.length===0 || homeForced时显示并隐藏所有终端 pane)。再点 ⌂ 或点任一 tab(activate重置homeForced)即回到终端;从叠加层开新会话/项目也会自动收起。关最后一个 tab 时复位homeForced。⌂ 叠加时高亮(.tab-home.active,Amber)。 - 测试(
test/tabs.test.ts+3): ⌂ 叠加首页再切回;叠加层切到 Projects 并开第二个项目后自动收起;0 tab 时 ⌂ 为 no-op。 - 验证:
npx tsc -p tsconfig.json/.web.json干净;npx vitest run415/415;真浏览器:开 shell tab→段控隐藏→点 ⌂→段控+面板叠加显示、⌂ 高亮→切 Projects 面板可见→再点 ⌂→回到终端。 - commit: (本次提交)
2026-06-30 · v0.6 Project Manager(P1–P6,多 agent Workflow 编排)
- 状态:
[x]DONE(分支v0.6-projects)。398 测试全绿 · 双 typecheck 干净 ·build:webOK · 覆盖率 90.98/81.95/89.28/93.16(≥80×4)· 真浏览器 smoke 通过。 - 目标: 首页新增 Projects 视图——发现主机上的 git 仓库,点卡片 → 开 tab(标签=repo 名、cwd=仓库、自动跑
claude);卡片显示分支/dirty/该项目运行中的 sessions(1:N)。设计见docs/FEATURE_PROJECT_MANAGER.md。 - 编排(ultracode
Workflow动态): Build(P2 后端 ∥ P5 前端)→ Wire(P4 端点 ∥ P6 接线)→ Review(module-reviewer验收 +typescript-reviewer+security-reviewer并行 report-only)→ HIGH/CRITICAL 对抗式 Verify → Fix。P6 首跑 transientserver_error挂掉(0 改动落地)→TaskStop+Workflow{resumeFromRunId}恢复;P2/P4/P5 缓存命中,P6 重跑成功。 地基 P1(types)/P3(config)上一会话已落,本会话先 commit 为dc5d073作干净基线。 - 改动:
src/http/projects.ts(新,P2):parseGitHead(纯)、buildProjects(cfg, liveSessions);内部scanRepos(BFS 深度上限/跳 node_modules·dotdir·symlink/命中即停)、readBranch、readDirty(execFilegit status,2s 超时,并发 8)、mergeHistory(复用history.listSessions)、缓存discoverProjects(TTL + in-flight 去重)、fresh session 按 cwd 前缀归并、_clearProjectCache(测试)。test/projects.test.ts(15)。public/projects.ts(新,P5):mountProjects+ 纯 helperfilterProjects/sortProjects/getFavs/saveFavs/toggleFav/normalizeProject。test/projects-panel.test.ts(36)。public/style.css追加 Projects 面板 +.home-seg段控样式。src/server.ts(P4):GET /projects(只读、无 Origin 守卫、try/catch→[])。test/integration/projects-endpoint.test.ts(5,真起 server)。public/tabs.ts(P6):openProject(同名#n、addEntry(...,'claude\r'))、countOpenWithTitlePrefix、updateHomeView+.home-seg段控。test/tabs.test.ts+7。
- Review→Fix: 12 findings,4 HIGH 全确认修复——① A3 关键缺回车
\r(openProject/newTabForResume)② 段控被定位面板遮挡(面板inset:44px 0 0 0)③mergeHistory串行→mapWithConcurrency④ discover 并发缓存击穿→in-flight 去重。Orchestrator 追加加固(MEDIUM/LOW + 测试):normalizeProject(API 边界校验)、getFavs滤非字符串、parseProjectRoots相对路径 fail-fast、config v0.6 解析补测(config.ts→98%)。 - 验证:
npx tsc -p tsconfig.json/.web.json --noEmit干净;npx vitest run398/398;npm run build:webOK;npx vitest run --coverage退出 0(阈值 80×4)。真浏览器(PROJECT_ROOTS=~/Documents/git,83 repo):段控切换 ✓、面板 83 卡片(分支/dirty/斜杠分支正确)+ 搜索 ✓、点卡 → tabweb-terminal+ 终端出现 claude 启动界面(A3 端到端)✓、console 无 v0.6 报错(仅既有 favicon 404)。 - 决策 / 偏离: 缓存只缓存磁盘发现、session 每次 fresh 归并(避免陈旧);git worktree 看板/文件树编辑器/ticket 拉取 = v0.6 明确不做(FEATURE §9)。
public/projects.ts/tabs.ts的 mount* DOM 接线沿用既有约定不纳入覆盖率(纯 helper 已测)。style.css1143>800 = 既有债,留作拆分。 - 遗留 / 待办: 真机 F4/F7(同历史);可选
server.ts抽http/routes.ts;style.css拆分。 - commit: (本次提交)
2026-06-16 · T1 脚手架 + 一次性装齐依赖
- 状态:
[x]DONE - 改动:
package.json(scripts + 全部依赖 +postinstall)、tsconfig.json(后端 NodeNext/strict)、tsconfig.web.json(前端 DOM/Bundler,当前 noEmit 仅类型检查)、vitest.config.ts(node 环境 +passWithNoTests)、.gitignore、package-lock.json、目录骨架src{,/http,/session} public test{,/helpers,/integration}(各带.gitkeep)。 - 验证:
npm install成功(运行时 72 包 + 开发包,0 漏洞)。解析版本(均比预期新的主版本):express 5.2、 @xterm/xterm 6.0、addon-fit 0.11、ws 8.21、node-pty 1.1;dev:typescript 6.0、vitest 4.1、tsx 4.22、@types/node 25。node -e require('node-pty')→ OK;真实 PTY spawnecho hello-from-pty→ 16 bytes、exitCode 0(沙箱外验证)。npm test→ exit 0(passWithNoTests)。
- 决策 / 偏离 PLAN:
- node-pty spawn-helper 修复:1.1.0 的 prebuild 自带
spawn-helper但权限-rw-r--r--(缺 +x), 导致posix_spawnp failed。加postinstall: chmod +x .../prebuilds/darwin-*/spawn-helper(对 Linux 无害,|| true), 使每次 install / 每个 worktree 自动修复。T12 spawn PTY 依赖此修复。 - 依赖解析到较新主版本(express 5 / xterm 6 / typescript 6 / vitest 4):T7/T11/T14/T15 注意 API 差异 (express 5 路由、xterm 6 import 形态)。
- 加了 PLAN 未列的脚本
dev/typecheck与postinstall(便利 + 必需修复),属合理补充,未改 PLAN 意图。
- node-pty spawn-helper 修复:1.1.0 的 prebuild 自带
- 遗留 / 待办: 无(原前端打包待决项已解决,见下补记)。
- commit:
b126cf0
2026-06-18 · v0.3 Step 5 — 中价值 M3/M7/M6(主题/仪表盘/同目录新标签)
- 状态:
[x]DONE。真浏览器验证通过。212 测试全绿。 - M3 主题/字体(commit
f79f14b):settings.ts(⚙ 面板:dark/light/solarized + 字号 A−/A+),localStorage 持久化;terminal-session.applyTheme;TabApp.applySettings 应用到所有+新标签。验证:light 主题渲染+持久化。 - M7 仪表盘(commit
f79f14b):dashboard.ts(▦ 浮层列出所有标签:连接点+文件夹+Claude 状态;点行切换;打开时每秒刷新);TabApp.snapshot()/focusTab()。验证:列出 2 标签、点击切换。 - M6 同目录新标签(commit
87e1173):title-util.cwdFromOsc7(解析 OSC 7 cwd)+4 单测;terminal-session 注册 OSC7 handler→cwd getter,新会话 attach 带 cwd;protocol/types attach.cwd(绝对路径);session/manager/server 透传到 spawn cwd;tabs+在当前 tab 的 cwd 开新标签(无则回 home)。验证:server attach.cwd 生效;cd /private/tmp→+→新标签 pwd=/private/tmp(此 zsh 发 OSC7)。 - 工具栏: 🔍搜索 · ⚙设置 · ▦仪表盘 · 📱QR。
- commit:
f79f14b(M3+M7)、87e1173(M6)
2026-06-17 · v0.3 Step 4 — H1 tmux 保活(服务重启不丢会话)
- 状态:
[x]DONE。真 tmux 集成测试验证通过(commit9099f73)。至此 H1–H4 全部高价值功能完成。 - 机制: useTmux 时把 shell 跑进
tmux new-session -A -s web_<id>;node-pty 进程只是 tmux 客户端。服务重启后,客户端带同 sessionId 重连 → manager 发现web_<id>tmux 会话仍在 → 用同 id 重建客户端(-A附着)→ 接回原 shell。 - 改动: 新增
src/session/tmux.ts(同步 CLI 封装 available/has/kill,best-effort);configuseTmux(envUSE_TMUX1/0/auto→检测 PATH);session createSession 加可选 id(重连用)+ tmux 分支 spawn +Session.tmuxName+ kill 走tmux kill-session;manager handleAttach 加 Case 3.5(重启后重附)+ shutdown 对 tmux 会话只杀客户端 pty 不杀 tmux(保活)。 - 测试: 共享集成 cfg 设
USE_TMUX:0(防 tmux 泄漏);单测 CFGuseTmux:false;集成H1(真 tmux,gate=tmux+PTY):设 shell 变量 → 重启 server → 同 id 重连 → 变量仍在(MARK-tmuxlives)。208 测试全绿,tsc 通过。 - 注: 当前后台 dev server(:3000)是 H3 之前启动的、不含 H1;要试 tmux 保活需
USE_TMUX=1 npm start。 - commit:
9099f73
2026-06-17 · v0.3 Step 3 — H3 远程批准/拒绝(不打字)
- 状态:
[x]DONE。端到端真浏览器验证通过(commit9a6150c)。 - 机制:
PermissionRequesthook 用 curl POST 到/hook/permission并阻塞等响应;服务端长挂该 HTTP 响应,推status{waiting,pending:true}给前端 → 前端显示 [✓Approve][✗Reject] 横幅 → 点按发 wsapprove/reject→ 服务端用{hookSpecificOutput.decision.behavior:allow|deny}回应被挂的请求 → curl stdout 把决定交给 Claude。全程不打字。 - 兜底: 5 分钟超时 / 断开即释放 → 回空
{},Claude 回退到自己的交互提示;web-terminal 外 curl 无 URL → no-op。 - 改动: types(ClientMessage approve/reject、status.pending)、protocol(白名单+解析)、manager(pending 透传)、server(
/hook/permission长挂 + approve/reject 路由 + resolvePending)、setup-hooks(PermissionRequest 用 held 命令)、FE(terminal-session approve()/reject()+pending、tabs 批准横幅)。 - 验证: 207 测试全绿(协议 approve/reject 单测 + 集成 ⑧ held→approve→allow);浏览器实测横幅"Claude wants to use Bash"→Approve→held 请求得到 allow→横幅消失(截图 /tmp/v03-approve.png)。
- commit:
9a6150c
2026-06-17 · v0.3 Step 2 — H2/H4 Claude 状态感知(headline)
- 状态:
[x]DONE(分支v0.3-cockpit)。端到端真浏览器验证通过。 - 机制: Claude Code hooks → 服务端
/hook侧信道 → WSstatus帧 → 标签角标实时更新。架构演进:服务端获得 hook 侧信道(终端流仍是 byte-shuttle)。 - H2 服务端(commit
a411c89):- types:
ClaudeStatus+ ServerMessagestatus;Session.claudeStatus;manager.handleHookEvent。 - session:spawn env 注入
WEBTERM_SESSION(哪个标签)+WEBTERM_HOOK_URL(本机 /hook)。 src/http/hook.ts纯映射(PreToolUse→working、PermissionRequest/permission_prompt→waiting、Stop/idle_prompt→idle);6 单测。- server:
POST /hook(仅 loopback,express.json 64kb)→ 推 status。顺带修真 bug:closeIdleConnections()让 hook keep-alive 空闲连接不再卡住 close()/SIGTERM。 - 集成 ⑦(真 PTY):attach → POST /hook → ws 收到 status(注意测试要在 fetch 前挂监听,推送是同步的)。
- types:
- H4 前端(commit
c81cebd):- terminal-session 处理 status → claudeStatus + onClaudeStatus。
- tabs:每标签 Claude 角标(⚙工作/⏳等批准/✓空闲);非活动 + waiting 标签加琥珀高亮 + 浏览器通知(首次切换请求权限)。
scripts/setup-hooks.mjs+npm run setup-hooks:幂等装/卸 ~/.claude/settings.json 的 command hook(内联 curl,web-terminal 外是 no-op),自动备份。
- 验证: 205 测试全绿;浏览器实测 PermissionRequest→⏳、PreToolUse→⚙、Stop→✓;非活动 waiting 标签琥珀+⏳(截图 /tmp/v03-status.png)。0 console error。
- commit:
a411c89(H2 服务端)、c81cebd(H4 前端+脚本)
2026-06-17 · v0.3 Step 1 — 前端快赢(键栏/搜索/链接/QR/PWA)
- 状态:
[x]DONE(分支v0.3-cockpit;纯前端,无服务端改动) - 计划:
~/.claude/plans/shimmering-wondering-island.md(v0.3 cockpit,已批准)。 - H4-keybar: 键栏加 Claude 快捷键 Esc·Esc/^O/^T/^B + 每键 tooltip(KEY_MAP 14 键;commit
eaeacf3)。 - M1 搜索:
@xterm/addon-search每终端 + 🔍 工具栏按钮 + 搜索框(Enter 下一个/Shift+Enter 上一个/Esc 关)。 - M2 可点击链接:
@xterm/addon-web-links。 - 结构: 标签栏拆成
#tabs(可滚)+#toolbar(工具按钮区),为后续 QR/设置/仪表盘复用。 - M5 QR: 📱 按钮 → 客户端渲染 location.origin 的二维码(经 LAN IP 打开即可分享)+ localhost 提示(commit
fbc218c)。 - M4 PWA: manifest + icon.svg + sw.js(network-first,不拦 /term//hook)+ 注册;可安装。
- 验证(真浏览器): 14 键栏按钮、搜索高亮命中、🔍📱 工具按钮、SW controlled、QR canvas 渲染、0 console error。199 测试全绿、双 tsc、build 通过。截图 /tmp/v03-quickwins.png、/tmp/v03-qr.png。
- commit:
eaeacf3、fbc218c
2026-06-17 · v0.2 修复 — 切换标签要点多次
- 状态:
[x]FIXED - 现象: 点标签切换常常点一两下不生效,要多点几次。
- 根因: 整个标签
draggable=true,点击时鼠标微动一点点就被当成拖拽(而非点击)→ 切换失效;且每次 activity/title/status 事件都replaceChildren()全量重建标签栏,点击瞬间元素可能被销毁。 - 修复(
tabs.ts重构渲染):① 切换改pointerdown(按下即切,先于拖拽判定,触摸也生效);② 标签 DOM 建一次,状态点/未读/标题就地更新(refreshTab),仅增删/排序/改名才全量重建;③ 关闭按钮/改名框 pointerdown stopPropagation 避免误激活。 - 验证(真浏览器): 连续单击 tab0→2→1→0 active 每次都跟随;CDP 真实点击也切换;改名/拖拽不受影响。198 测试全绿。
- commit:
e058751
2026-06-17 · v0.2 — 标签按当前文件夹自动命名
- 状态:
[x]DONE(纯前端) - 需求: 标签名用当前文件夹,易区分。
- 改动: 新增
public/title-util.ts的folderFromTitle()——解析 shell OSC 标题user@host:~/path取最后一段目录名(home 保留~);纯函数、无依赖、7 单测。terminal-session.ts把onTitleChange经此函数转为文件夹名。标题优先级:手动改名 > 文件夹名 > Term N。 - 验证(真浏览器): 198 测试全绿、tsc+build ✓。实测
cd实时更新:~→ 项目目录web-terminal→docs→~。 - commit:
4091283
2026-06-17 · v0.2 — 标签状态点 + 活动指示 + 拖拽排序
- 状态:
[x]DONE(纯前端) - 改动:
terminal-session.ts:跟踪SessionStatus(connecting/connected/reconnecting/exited)+onStatus回调。Bug 修复:onTitle之前只在 opts 里、构造函数从没绑定onTitleChange—— 自动标题原本是死代码,现已接上(实测两标签显示host:~自动标题)。tabs.ts:每标签状态点(颜色=连接态:绿/琥珀/红);未读活动(后台标签来新输出→hasActivity→蓝环,切到该标签即清);HTML5 拖拽排序(保留活动标签、持久化)。style.css:.tab-dot配色、.unread蓝环、.dragging/.drag-over反馈。
- 验证(orchestrator,真浏览器): 191 测试全绿、双 typecheck、build:web ✓。browse 实测:连接后绿点;Term1 后台收到延迟输出→未读蓝环(
unread:true);拖拽AAA|BBB→BBB|AAA且reload 后顺序+自定义名都保留。截图 /tmp/webterm-dots.png。 - commit:
b5f21e5
2026-06-17 · v0.2 — 多标签 + 标题 + Claude Code 快捷键栏
- 状态:
[x]DONE(纯前端;后端 manager 早已多会话,无需改后端/协议) - 需求: 用户要 ①多标签 ②标签加标题方便区分 ③适配平板/手机 ④移动端易触发 Claude Code 快捷键(按钮触发快捷键)。
- UX 调研(general-purpose 联网,参考 iTerm2/Warp/VS Code/Windows Terminal/Ghostty/Chrome):双击就地改名是现代主流(Warp/VS Code/Arc);自动标题用 OSC 0/2;中键关闭、× 悬停显示、省略号+tooltip、Cmd/Ctrl+数字切换是 Tier-1。验证了"OSC 自动标题 + 双击改名"方案正确。
- 改动(
public/):- 新增
terminal-session.ts:从旧 main.ts 抽出"单会话 = 1 Terminal + 1 WS + 重连",加 show/hide/fit/focus/dispose + onTitle/onActivity 回调。 - 新增
tabs.ts:TabApp 多标签管理 + 标签栏 UI。每标签独立 WS/会话(沿用 1WS↔1会话协议);自动标题(onTitleChange)+ 双击就地改名(手动>自动>Term N);中键关闭、× 按钮、省略号+tooltip;localStorage 持久化{id,title}[](兼容 v0.1 单会话 key 迁移)。关闭标签=detach(PTY 不死)。 keybar.ts:加slash键;导出KEYBAR_BUTTONS按 Claude 频率排序(Esc 标 primary);+4 单测。main.ts瘦身为引导;index.html加#tabbar;style.css重写:键栏全设备常显(桌面也可点按钮触发快捷键)、@media(pointer:coarse)触摸加大、Esc 琥珀色高亮、标签栏/改名框/活动下划线。
- 新增
- 决策:键盘快捷键(Cmd+T 等)不做——浏览器宿主会抢,改走点击/触摸 UI(对 web 终端正确取舍)。
- 验证(orchestrator,关沙箱真浏览器): 两 typecheck ✓;
npm test191 全绿(+4 keybar);build:web✓。browse 实测:双击改名 Term1→"claude"、两标签会话独立(claude | Term 2)、键栏桌面可见 10 键 Esc primary、窄屏可滚动可用。截图 /tmp/webterm-ux-desktop.png、-phone.png。 - commit:
4f66016
2026-06-17 · W5 — 验收 + 收尾(T16–T21)
- 状态: T16 ✅ · T17 部分(F9✅/F4 待真机)· T18 ✅ · T19 待真机 · T20 ✅ · T21 ✅
- T21(orchestrator):写
README.md(install/run/config/security);安全自查——无硬编码 secret(grep 空)、Origin+路径+maxPayload 校验已接线(server.ts 确认)、无 eval/exec、input 原样透传给 PTY(byte-shuttle 设计意图);npm run build→dist/server.js可产出。 - F1/F2/F3/F8 浏览器 smoke(关沙箱起 server + gstack browse 无头 Chromium):
· 加载 http://127.0.0.1:38080 → .xterm 挂载、0 console error、WS 连上、彩色状态行 + 真 zsh 彩色提示(F1/F2)。
· 聚焦
.xterm-helper-textarea输入echo hello-webterm→ 回显 + 输出hello-webterm(输入→WS→PTY→shell→输出→xterm 全链路)。截图 /tmp/webterm-smoke2.png。 · 输入exit→ 终端显示 "Process exited code=0 Press Enter to reconnect…"(F8)。 · 改 viewport(F3)→ 终端存活、无 console error。 · curl 复核:GET / 200、/build/main.js 200、坏 Origin upgrade → 401。 - 决策: F4(局域网多设备)、F7(手机软键盘栏)需物理设备,无法在本机无头验证 → 交用户真机。其余 7/9 F 项已自动化验证。
- 遗留: T19(F7)待真手机;F4 待局域网真机;main.ts 的 css
@ts-ignore小债。 - commit:
81585ad(README/安全)+ 本条 LOG
2026-06-17 · W2/W3/W4 — 会话核心 + 集成(T12/T13/T14/T15)
- 状态:
[x]ALL DONE · 串行(各有依赖),单 builder/任务 - T12
session/session.ts(opus,19 测试):createSession/attachWs/detachWs/writeInput/resize/kill;onData→buffer+lastOutputAt+转发;onExit→exitedAt/exitCode+发exit+注入onExit;spawn 失败抛(M4);sendIfOpen 守卫 readyState(M5);detach 不杀 PTY;exit 后 write/resize 忽略(L4);detach 后退出保留会话(L1)。测试vi.mock('node-pty')→mock IPty,沙箱内可跑。 - T13
session/manager.ts(sonnet,21 测试):handleAttach 四路径(新建/存活/已退出 L1/查不到);reapIdlenow-max(detachedAt,lastOutputAt)>idleTtl(M3);shutdown 全 kill;注入 onExit 在线退出删表、detach 后退出保留待 L1(L2)。 - T14
server.ts(sonnet,~200 行):Express 静态 public/+build/;WebSocketServer({noServer,maxPayload})(L5);upgrade 门(非 wsPath→destroy、坏 Origin→401、过→handleUpgrade,L3);connection 等 attach,try/catch→spawn 失败 exit(-1)+关连接(M4);message 路由;close→detach(不杀);SIGINT/TERM→shutdown;reapIdle 定时器;safeSend 守卫(M5)。导出 startServer 供 T15。 - T15 集成(sonnet,6 用例):真 ws 客户端打 startServer。①坏Origin 401 ②错路径 destroy ③超 maxPayload 1009 ④spawn失败 exit(-1) —— 沙箱内全绿;⑤attach→output ⑥重连回放(F5/F6,含
__MARK_测试__CJK)—— orchestrator 加PTY_AVAILABLE门控(沙箱自动 skip,真机跑),关沙箱验证 6/6 全绿。 - 验证(orchestrator 独立复核): 每步
tsc --noEmit✓ + 全量npm test递增至 187 全绿;集成 sandbox-off 6/6(真 PTY)。 - 决策 / 偏离: T12 onData 时间戳用 Date.now()(事件时刻,符合 lastOutputAt 语义);T15 由 orchestrator 加 PTY 可用性门控(原
it.skip→itPty),使真机会跑、沙箱自动跳过。 - commit:
20212dd(T12)·a2897b2(T13)·0dac54b(T14)·cdec19a(T15)
2026-06-16 · W1 前端批次(T8/T9/T10/T11)
- 状态:
[x]ALL DONE - 派活方式: 因 T11 import T10 的 mountKeybar 且验收是整目录 tsc.web,分两步:先并行 T8+T9+T10(haiku),后 T11(sonnet)。general-purpose 内联角色,共享树 + 文件不交叉。
- 改动:
- T8
public/index.html:#term/#keybar、viewport、<script type=module src=./build/main.js>+ link ./build/main.css 与 ./style.css(按 §1 约定)。 - T9
public/style.css:全屏终端、键栏 fixed 底部 flex、>768px 隐藏、深色;不碰 xterm 内部样式。 - T10
public/keybar.ts(+ test):KEY_MAP 纯数据(Esc/Shift+Tab/方向/Enter=\r/Ctrl+C/Tab)+ mountKeybar(onSend) touchstart→onSend+preventDefault。13 测试。 - T11
public/main.ts:esbuild 入口(import xterm css);xterm+FitAddon;WS scheme 随协议(M6);attach 首帧带 localStorage sessionId;onData→input、output→write、exit→ANSI 提示按回车重连;ResizeObserver 防抖 resize;指数退避重连≤30s;mountKeybar 接 ws.send;无 console.log(状态写终端)。
- T8
- 验证(orchestrator 独立复核): backend
tsc --noEmit✓;frontendtsc -p tsconfig.web.json✓;npm test6 文件 / 141 测试全绿(新增 keybar 13);npm run build:webesbuild 打包成功(main.js 430kb + main.css 6kb);build/ 已 gitignore 未入库。 - 决策 / 偏离: T11 用 1 处
@ts-ignore压 CSS import(Owns 仅 main.ts,无法另建 d.ts;记为 tech-debt);exit 后按回车 clear sessionId 再新建(契合 L1:已退出会话由服务端回放+补发 exit,前端不 reattach)。 - 遗留: 上述 css d.ts 小债;前端真实浏览器行为留 W5(T16–T20)实测。
- commit:
326d347
2026-06-16 · W1 后端批次(T3/T4/T5/T6/T7)— 5 个并行 builder
- 状态:
[x]ALL DONE - 派活方式: 5 个
general-purposesubagent 并行(自定义 module-builder agent 需重启会话才加载,故本会话内联角色), 共享工作树 + 文件所有权严格不交叉 + scopedvitest run <name>(官方文件分区法,替代 worktree;合并即 commit)。模型按 §4。 - 改动(各 agent 仅在 Owns 内):
- T3
test/helpers/mock-pty.ts(+ smoke test):IPty 替身,emitData/emitExit手动触发、记录 write/resize/kill、onData/onExit 返回 IDisposable。 - T4
src/config.ts(+ test):loadConfig(EnvLike);M1 origins 由os.networkInterfaces()推导(含 localhost,剔除 0.0.0.0/internal,http+https),wsPath 默认 /term,非法值 fail-fast,Object.freeze。 - T5
src/protocol.ts(+ test):SESSION_ID_RE(UUID v4)、parseClientMessage(永不抛,全分支校验,M7 拒abc123)、serialize。 - T6
src/session/ring-buffer.ts(+ test):M2 按真实字节(Buffer)计量、按 chunk 边界淘汰、不切碎 UTF-8/ANSI、snapshot 补\x1b[0m。 - T7
src/http/origin.ts(+ test):用 WHATWGURL比 protocol+host+port,undefined/解析失败拒绝。
- T3
- 验证(orchestrator 独立复核,非仅信 agent):
npx tsc -p tsconfig.json --noEmitPASS;npm test(全量) 5 文件 / 128 测试全绿(T4 32 + T5 60 + T6 15 + T7 12 + T3 9)。 - 决策 / 偏离: 无源码偏离。流程上:本批用 general-purpose 内联角色(见上)。T6 对"单块 > 容量"按 M2"绝不切碎"保留整块(测试固化),属忠实落地。
- 遗留 / 待办: W1 前端 T8–T11 待派;之后 T12 session(mock+ring-buffer 已就绪)。
- commit:
0fa0b69
2026-06-16 · T2 冻结共享类型契约 src/types.ts
- 状态:
[x]DONE - 改动: 新建
src/types.ts(纯类型,无实现);删src/.gitkeep。含 Config、ClientMessage/ServerMessage/ ParseResult、Dims、IPty(子集)+ IDisposable/PtyEvent、RingBuffer、WebSocketLike + WS_OPEN、SessionMeta/Session (lastOutputAt/exitedAt/exitCode)、SessionManager、MountKeybar;各模块工厂函数签名以注释列为 import 锚点。 同步更新 ARCHITECTURE §3.1/§3.4(加 wsPath、EnvLike、WebSocketLike 注记,防漂移)。 - 验证:
npx tsc -p tsconfig.json --noEmitPASS;grep 确认无ws/NodeJS/DOM 外部类型依赖(仅注释提及)。 - 决策 / 偏离 ARCHITECTURE §3(为"前后端共享 → 必须无外部类型依赖"):
loadConfig(env: EnvLike)替代NodeJS.ProcessEnv(EnvLike = Readonly<Record<string,string|undefined>>,process.env 可赋值)。Session.attachedWs: WebSocketLike替代ws的WebSocket(真实 ws 结构兼容);导出WS_OPEN=1供 M5 守卫。- 新增
Config.wsPath(默认 '/term'):不变量 8 要求 WS 路径入 config,§3.1 原 Config 漏列,已补;T4 需从 envWS_PATH读、T14 用cfg.wsPath。 三处均已回写 ARCHITECTURE,types.ts 为权威冻结源。
- 遗留 / 待办: 无。W1 全部任务现可只读 import
src/types.ts。注意:IPty/WebSocketLike 是结构子集, 实现方(T12/T14)可把真实 node-pty IPty / ws.WebSocket 直接赋值(结构兼容)。 - commit: (见下次提交)
2026-06-16 · T1 补记 — 前端打包定为 esbuild
- 状态:
[x]DONE - 改动: 装
esbuild(dev);package.json接build:web/dev:web(打包public/main.ts→public/build/main.js);.gitignore改忽略public/build/;tsconfig.web.json注释更新为"仅类型检查";前端构建约定写入PLAN §1并更新 T8/T11/T14 与DISPATCH.md、ARCHITECTURE §5。 - 验证: esbuild 0.28.1;临时入口
--bundle --format=esm --outdir=public/build出main.js+.map,smoke 通过。 - 决策: 三选一中选 esbuild(极快、零配置、与 node-pty/tsx 生态一致);现在装好避免 W1 并发 install。
约定:入口 main.ts、产物 public/build/(已 gitignore)、index.html 用
<script type=module src=./build/main.js>、server 静态托管 public/。 - commit: (见下次提交)
2026-06-15 · T0 项目记录起点(计划就绪)
- 状态:
[x]DONE - 改动: 创建
docs/PROGRESS_LOG.md、docs/PLAN.md(细粒度 T1–T21、为多 agent 并行设计);CLAUDE.md加入 plan/log 工作流约束。 - 验证: 文档自检;LOG 任务总览的 Steps 总数已与 PLAN 各任务勾选项对齐。
- 决策 / 偏离 PLAN: 采用 TypeScript(ARCHITECTURE §0);两份设计文档已多团队交叉验证并修订 12 项;计划按依赖分 W0–W5 波次,W1 含 9 个零交叉并行任务。
- 遗留 / 待办: 进入 W0 / T1(脚手架 + 装齐依赖);T1 完成后 T2 冻结
src/types.ts,随后 W1 可大规模并行。 - commit: N/A