The client reported COMPLETE with 617 green tests and a buildable APK, but had
never run on hardware, and the first interaction on a device crashed it. Seven
real defects, four of them crashes:
- every key press NPE'd (no TerminalViewClient was ever installed)
- the PTY never learned the real grid (no resize was ever sent)
- bare-LAN ws:// could not connect (a STUB network-security-config)
- swipe-scrolling inside an alternate-screen TUI NPE'd, i.e. during ordinary
use of Claude Code, which is one
- any password manager crashed the app (unguarded autofill)
- onDraw tore the terminal buffer and killed the process — the defect the docs
had explicitly classified as 'Accepted (not a defect)', and which was dormant
only because resize never happened
- text selection's Copy button was an NPE, so copy-out was cut rather than
shipped broken
Also: the queue frame and status.preview were silently dropped (remote approvals
were blind), the v0.6 git panel was unrendered, ThumbnailPipeline/QuickReply/
PointerContextMenu were tested dead code with no call sites, the WEBTERM_TOKEN
gate was inert, release builds emitted unsigned APKs, and A34/A35 had no code at
all despite being marked done.
Then the audit's own leftovers: silent certificate rotation (which surfaced a bug
that would have failed every renewal, plus a latent iOS defect Android now
avoids), /device/:id/recover, the W2 inject-queue routes, unread-watermark
persistence, host removal, and /config/ui finally honoured fail-closed.
617 -> 1150 JVM tests, plus 67 instrumented tests that actually execute on an
emulator against this repo's own server. The device crashes are reproduced as
JVM tests where possible, so they are guarded without hardware.
Still open and recorded, not papered over: S2 needs two physical handsets under
Doze; the rest of DEVICE_QA_CHECKLIST has no automated cover.
# Conflicts:
# docs/PROGRESS_LOG.md
212 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] Android 客户端"能装但不能用"修复 — 7 个真实缺陷已修,设备上 67 个 instrumented 测试全绿(2026-07-30,worktree android-blocker-fixes)
- 起因: 例行问"android 客户端完成状况如何"。
android/PROGRESS_ANDROID.md写着 "✅ ANDROID CLIENT COMPLETE — all 36 plan tasks landed",617 个 JVM 测试全绿,APK 也出得来。这个"完成"只在编译层面成立 —— 从来没在任何真机或模拟器上跑过,而一旦跑,第一个动作就崩。审计(45 agent,含逐条对抗验证)+ 修复(多波 agent)已把 blocker 清掉。 - 为什么 JVM 测试全绿也漏掉了: 一个结构性原因 —— 没有任何 JVM 测试会实例化 Android
View。:terminal-view那 17 个测试测的是纯逻辑,渲染/输入/触摸整条路径无人触碰。 - 三个让 App 在真机上不可用的缺陷(每一条都反编译
terminal-view-v0.118.0.aar逐偏移确认):- 按任何键必崩。
RemoteTerminalView只绑了 emulator,从没调用setTerminalViewClient—— KDoc 写着"A17 会装",A17 实际只交付了HardwareKeyRouter和 KeyBar(而 KeyBar 之所以能用,恰恰因为它绕过了 view)。stockTerminalView在onKeyDownoffset 82、onCreateInputConnectionoffset 0、onKeyUp、onKeyPreIme无判空解引用mClient。装个 client 还不够:返回 false 会继续走到 offset 150 的mTermSession.write(),而mTermSession永远是 null(TerminalSession是 final,而 fork 进程正是本 App 绝不能做的事,§6.1)。修法:client 自己消费按键,且KeyRouting刻意没有"交还 stock"这个分支 —— 崩溃变成不可表达,而不只是被绕开。键表仍由 TermuxKeyHandler决定,DECCKM 照旧发ESC O A。 - resize 从来不发。
RemoteTerminalSession.updateSize零调用点,stock 兜底也是死的(TerminalView.updateSize无 session 时 offset 18-25 直接 return),PTY 一直停在服务端的 80×24,所有全屏 TUI 都排版进错误的框。TerminalResizeDriver现在从真实字距驱动 —— 字距走公开 accessorTerminalRenderer.getFontWidth()/getFontLineSpacing()(agent 自己javap发现它们是 public),不用反射(R8 下会静默失效)也不重测 Paint(会与渲染器实际所用漂移,正是 §R5 风险)。 - 局域网连不上。
network_security_config.xml是自己标着 STUB 的全局禁明文,而HostEndpoint接受http并派生ws://。它注释里承诺的 CIDR 白名单平台根本不支持(NSC 格式没有任何 netmask/prefix 属性,<domain>只装一个主机名或一个 IP 字面量)。改为 base-config 放开 +*.terminal.yaojia.wang显式禁明文 + 信任锚钉死 system +OkHttpClientFactory在传输层拒绝向非私有地址发起明文 —— 平台表达不了的收窄,放到 HTTP 客户端里做。plan §6.9/§8 的错误描述已一并更正。
- 按任何键必崩。
- 对抗评审又挖出三个,其中一条评审自己判错了:
- 在 alternate-screen TUI 里滑动滚屏仍会崩。触摸路径完全绕过
TerminalViewClient:doScroll在 alternate buffer 激活时把滚动转成handleKeyCode,而那里解引用mTermSession。Claude Code 本身就是 alternate-screen TUI,所以这是主界面正常使用中的崩溃。TerminalScrollGesture抢占纵向拖动并复现doScroll的三个分支,连 fling runnable 和onGenericMotionEvent一并封死。 autofill()无判空解引用mTermSession,而该 view 自称可自动填充 → 任何密码管理器一填就崩。整个子树已排除出 autofill 结构。- 评审要求恢复 stock 文字选择(此前被 focus 改造弄成不可达)。这条它错了:ActionMode 自己的 Copy/Paste 处理器也解引用空 session(
TextSelectionCursorController$1.onActionItemClickedoffsets 89-100 / 126-136),恢复可达性只会把崩溃搬到用户点 Copy 那一刻 —— 比没有选择功能更糟。长按保持被消费,复制功能如实记为功能缺口(真正的修法是基于TerminalBuffer+ClipboardManager自建选择层)。
- 在 alternate-screen TUI 里滑动滚屏仍会崩。触摸路径完全绕过
- 被静默丢弃的服务端数据,现已解码: W2
queue帧在ServerMessageType里没有成员,decodeServer返回 null,SessionEngine当"无法解码"丢掉 —— "N queued"徽标在 Android 上从来不可能出现;W1status.preview完全没建模,导致远程审批是盲批(只看到工具名,看不到要执行的命令/diff)。两者均已解码。一条规矩用测试钉住:preview 坏掉绝不能连帧一起丢 ——pending才是让 gate 出现的信号。 - 当作功能发布的死代码:
ThumbnailPipeline(会话列表预览)、QuickReply(芯片+调色板)、PointerContextMenu三者实现完整、单测齐全、生产代码零调用点。均已接线。ThumbnailPipeline当年被明确跳过的交叉评审(AW3 条目自己承认)这次补做了。 - 凭据处理:
WEBTERM_TOKENcookie 现在在 REST 和 WS upgrade 上都携带,用 Tink AEAD + AndroidKeyStore 封装,fail-closed(封装失败就不持久化,绝不退回明文);allowBackup=false阻止这个 30 天 shell 凭据经 Google Drive / D2D 外泄。 - v0.6 git 面板数据层: 服务端
8fe1f52之后的字段 Android 全没解码。已补SyncState/dirtyCount/cwd/unpushed/upstream+GET /projects/worktree/state+POST /projects/git/fetch。两条易错语义编码成行为而非靠调用点自觉:SyncState.isFullySynced(now)是唯一被认可的"能否显示绿色"判断(任何未知即 false,无 upstream ≠ 0);GitLogResult.upstreamBoundaryIndex找最后一个未推送提交(取前 N 行会朝危险方向出错)。 - 验证:
./gradlew test :app:assembleDebug :app:testDebugUnitTest koverVerify—— blocker 修复提交时 757 个 JVM 测试全绿(基线 617);:wire-protocol/:session-core/:api-client三处 koverVerify 均过。commit3e5cfdc、3fe719f、35d32f4。 - 方法与其代价(如实记录): 分波多 agent(TDD builder → 对抗评审 → 修复)。API 端持续 529 过载,13 个 agent 里 8 个、11 个里 9 个被打掉;协议层和 REST 层两轮全灭后由 orchestrator 亲自补写。这也是为什么会话成本冲到 ~$360。对抗评审确实值 —— 滑动滚屏那个 CRITICAL 崩溃是 builder 漏掉、reviewer 抓到的。
- 仍未完成(不要把上面读成已完成):
- A34 / A35 完全没有代码 ——
:app连 androidTest 源集都没有,也没有 benchmark 模块。AW6 条目把它们标成[x]是因为它们被转成了清单条目,不是实现。 - 设备 QA 仍约 0% —— 仓库里唯一的真机证据是一张配对页的模拟器截图。上面每一条修复都只有 JVM 级验证。
- release 签名/版本号/minify;S2 FCM 真机投递;复制功能。
android/PROGRESS_ANDROID.md顶部已加"CORRECTION"段落说明上述全部。
- A34 / A35 完全没有代码 ——
- 后续同日完成(本条目最初写的"仍未完成"已不成立):
- 设备上真跑起来了:
:app/src/androidTest/**67 个 instrumented 测试 +macrobenchmark/,在emulator-5554上对本仓库自己的服务器全绿(67/0)。含四个崩溃回归、A34 E2E(attach→output、ring buffer 重放、用第二个故意配坏的服务器测 spawn 失败、POST /hook/decision解 gate、F9 坏 Origin 拒绝)、以及 plan §7 点名的 Compose 测试。模拟器要打通宿主机必须ALLOWED_ORIGINS=http://10.0.2.2:<port>—— 服务端的 allowedOrigins 从网卡 IP 派生,而 10.0.2.2 是模拟器对宿主机的别名、不是任何网卡地址。 - 设备上又抓出第四个崩溃:
onDraw→TerminalRow.getStyle的AIOOBE,而PROGRESS_ANDROID.md原本把它写成"Accepted (not a defect) — 撕裂读会自我修正"。两半都是错的:它是进程直杀;而且"matches upstream Termux"也不成立 —— 上游append由MainThreadHandler在主线程调用,压根没有并发读者。根因是TerminalEmulator.resize先发布新mColumns、后才重新分配行数组,length == index那个"可疑的一致性"是扩容的确定性签名。而且这个竞态以前是休眠的 —— resize 从不发生,列数就不变,没有宽度差可撕;是本次把 resize 修好才让它变可达。修法:emulator mutation 全部挪到渲染线程(与上游一致),单写者模型不变。已先复现成 JVM 测试,所以这条回归以后不需要设备就能守住。 - 审计遗留 6 项 + 复制功能全部关闭: 证书静默轮换(顺带修掉一个会让每次续期都失败的
renew()解码 bug,并指出 iOS 有个"只轮换一次"的潜在缺陷 Android 通过从叶证书反推renewAfter避开了)、/device/:id/recover(走非 mTLS,因为过期证书没法给自己的恢复做认证)、W2 inject-queue 三条路由、未读水位持久化、host 移除 UI、/config/ui被尊重且fail-closed、以及基于TerminalBuffer.getSelectedText自建的选择/复制层(stock ActionMode 的 Copy 处理器解引用空 session,所以不能恢复它)。 - 最终门禁:
./gradlew test :app:assembleDebug :app:assembleDebugAndroidTest :macrobenchmark:assembleBenchmark koverVerify→ BUILD SUCCESSFUL,1150 JVM 测试 / 0 失败(基线 617);instrumented 67/0。commit3e5cfdc→390dd11共 9 个。
- 设备上真跑起来了:
- 仍然开放: S2(FCM 真机投递,需要 ≥2 台物理设备在 Doze/厂商省电下验证,模拟器答不了);
DEVICE_QA_CHECKLIST.md里没有自动化覆盖的其余设备可观测项;一个 instrumented 测试在不传 host 参数且模拟器被反复安装卸载后会杀掉测试进程,按文档化的调用方式+干净安装则稳定通过 —— 上真机再判。 - 本次合并时发现的新 parity 缺口(记录,未做):
develop在本分支之后新增了 孤儿 tmux session 功能 ——OrphanSessionInfo类型 + 5 条路由(GET /orphan-sessions、/count-idle、/:id/preview、DELETE /orphan-sessions、/:id)。Android 客户端完全不知道它们。服务端刻意把它做成独立类型而非LiveSessionInfo上的 flag,所以现有解码器不受影响、不会解错 —— 这是一个新增缺口而不是回归。iOS 同样没有。
🗂 [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。 - 顺带修掉一个更严重的设计缺陷(用户确认后改): 桌面 App 之前把
USE_TMUX=0钉死在所有平台,不只 Windows。没有 tmux 时 shell 是内嵌 server 的直接子进程,于是SessionManager.shutdown走 kill 分支 —— 退出 App(托盘 Quit、注销、自动更新)会把所有 session 一起杀掉。这直接否掉了本 App 的立身之本:丢个任务给 Claude Code、走开、换台设备再连上;一个活不过关窗口的 session 做不到这件事。而独立 server 一直是对的(USE_TMUX未设 →auto→ 探测二进制是否存在)。现改为非 Windows 默认auto,Windows 仍强制关(tmux 是 *nix 的东西),显式 ambientUSE_TMUX依然优先,所以USE_TMUX=0仍是退出开关。- 测试断言的是端到端结果而不是那个字符串:
buildServerEnv喂进loadConfig后useTmux === true(装了 tmux)、显式退出时为false。原来的 bug 正是这两层对同一个值的含义理解不一致,只断言字符串是抓不到的。 - 注意:改动之前在 App 里起的 session 不会被追溯保护 —— 它们背后本来就没有 tmux session,仍会随 App 一起死。只有装了新包之后新建的才活得下来。
- 测试断言的是端到端结果而不是那个字符串:
- 遗留 / 待办: B(启动收养 + 让
reapIdle管到孤儿)未做,需要配"按需 attach"(Session.pty可空)才划算 —— 那要动 Session 形状和不变量 #2。 - 第二轮对抗式审查又抓到一个安全洞(必须记住):
tmux -t <name>的解析顺序是 精确名 → 名字前缀 → fnmatch。这直接绕过本模块的核心承诺("只碰web_+ UUID v4,用户自己建的 tmux session 永不被枚举/删除"):一个叫web_<uuid>_mine的 session 被parseSessionList拒绝(后缀不是 UUID)因此从不出现在列表里,但-t web_<uuid>会前缀匹配到它 —— 于是打向一个不存在 id 的 DELETE 会杀掉用户自己的会话,预览会打印它的屏幕。tmux 3.6a 独立 socket 实测:has-session -t web_<uuid>命中web_<uuid>_MINE,加=则can't find session;capture-pane -p -t web_<uuid>打印出了 DECOY 的内容,-t =web_<uuid>:则失败。要两种 target 形态:=只限定 target 的 session 部分,所以 session target(has-session/kill-session)用=name,而capture-pane收的是 pane target,裸=name会被 tmux 直接拒("can't find pane"),必须写=name:。已加集成测试:真的立一个 decoy,断言列表不含它、预览和 DELETE 都 404、且它依然活着。- 另外三条:① 改为
max(window_activity, session_activity)—— 两个时钟没有偏序,window_activity跟输出,而"attach 但没输出"只推session_activity;本机实测有一个 session 的session_activity比window_activity新 4 秒(另一个反过来新 25 天)。问题是"这儿有没有发生过任何事",答案就是两者取晚。②parseSessionList曾接受空数字字段:Number('')是 0 且 0 是有限值,于是空时钟被读成"纪元创建、之后一直闲置" → 立刻符合清理条件;改为必须是纯数字。③ 批量清理杀之前重读一遍世界:候选列表是快照而循环要花时间,期间 session 可能被 attach 或被收养进表 —— 这是不可逆路径,不信快照。④captureOrphan去掉了多余的hasSession预探测:那是每张缩略图第二次 tmux spawn、而且是同步的、还在整格刷新的路径上,而capture-pane本来就会在 session 不存在时干净失败。护栏拆成纯函数半边(mayActOnOrphan:UUID 形状 + 不在表里,两条路共用)和存在性探测(只有毁灭性路径需要)。
- 另外三条:① 改为
- 验证(第二轮): 单测 2213 全过,孤儿集成 9/9(含 decoy 回归)。确认
has-session -t =<name>仍能解析本机 69 个真实 session,Case 3.5 重连路径未受影响。 - 已知抖动(非本次引入):
test/integration/server.test.ts的 "H1 shell state survives a server restart" 在 27 个用例并发跑且机器满载时约 1/3 概率失败,单独跑 4/4 通过 —— 就是日志里早已记录的真实 PTY 时序抖动。 - commit:
4892fa7(功能)+f6ef19e(第一轮审查修复)+d39a0ab(第二轮:精确 target + 双时钟)+5833529(桌面 App tmux 默认值),合并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