zterm-mac Dev Skill
适用场景
mac/下的 Electron / renderer / preload / local tmux / remote bridge 开发- Mac terminal 渲染、分屏、tab、local tmux、remote 连接、资源占用审计
- 用户要求先自测闭环、拿证据,再让 Jason 手测
一、必读顺序
~/.codex/AGENTS.md~/.codex/USER.mdcoding-principals/SKILL.mdandroid/docs/decisions/0001-cross-platform-layout-profile.mdandroid/docs/architecture.mdandroid/docs/dev-workflow.mdmac/MEMORY.md- 本
SKILL.md
二、Mac 开发硬规则
2.0 Skill 使用边界
- 本仓库 Mac 线默认只使用这一个项目 dev skill:
zterm-mac-dev。 - 不要再为同一条 Mac 开发链路额外切换/串联新的本地 dev skill,避免规则分叉。
- 若确实需要别的 skill,必须是 Jason 显式点名。
2.1 Terminal / renderer
- renderer 只消费 canonical buffer / render projection;禁止在 view 层继续造第二份 terminal 真相。
- terminal 优先 terminal-first:少 chrome、少常驻面板、主空间给 terminal pane/tab/split。
- 分屏默认是一行多列、垂直分屏;不要把上下堆叠当主方案。
- Mac 分屏视觉压缩只改 shared
PaneStage/resolvePaneProfiletoken 与 Mac shell chrome CSS;禁止在 runtime / renderer / buffer 层补外观。验证至少跑 pane targeted tests、type-check/build,并用渲染 smoke 证明 split DOM 与 spacing token(如 stage gap / divider width)已进入最新 bundle。 - reading/backfill 里
missingRanges必须从 view -> runtime -> transport 原样透传;任一层清空它,scroll prefetch 都会静默失效。 - 若桌面端要接快捷按键组合语义,优先复用 shared composer 叶子模块;不要在 Mac renderer 再复制一套
Ctrl + 字母编码/默认 label 规则。 - 若桌面端接入 terminal 主题选择 UI,只要界面显示“Active/正在使用”,点击动作就必须立即持久化到 shared
BridgeSettings.terminalThemeId;不能只改本页 draft,避免出现“看起来切了主题,切页后又回默认”的假激活。
2.2 连接 / tmux
- remote 连接与 local tmux 都必须走真实 runtime,不允许静态占位冒充 live terminal。
- “能列 session” 不等于 “已 attach”;需要真实 connect / attach / resize / input 路径验证。
- 修改 local tmux / remote bridge / renderer 任一层后,必须至少做一次实际 smoke,不只看编译通过。
- Electron local tmux head/sync 的 canonical capture 必须包含 scrollback + visible pane bottom;
capture-pane -E -1会停在历史尾部,不能用于 app buffer truth,否则 packaged DOM 会落后真实tmux capture-pane尾部。
2.3 资源与生命周期
- 不允许只凭代码阅读宣称“没有内存泄漏/没有孤儿进程”;必须有运行态证据。
- 旧 app 必须先退出,再打开新包;不要叠多个实例污染结论。
- 禁止 broad kill;退出旧 app 用应用级 quit 或明确 PID 级关闭。
- tmux / daemon / CDP smoke 必须先盘点现有资源,再复用本轮已有专用 session / port / app 实例;禁止每次验证都新建 timestamp session。
- 只有两类 session 允许写入或重置:本轮明确创建的专用 session,或带项目 gate marker 且 owner/case 匹配的固定 gate session。已有用户 session 只允许只读观测。
- 每个 live / blackbox smoke 结束前必须复核生命周期:列出本轮新增 session / pipe-pane / app PID / debug port,关闭临时资源;若固定 gate session 需要保留供复用,必须说明 marker 与名称。
- API / 坐标 / 截图这类探索实验也按同一资源协议执行:优先复用现有 iTerm2 / tmux / daemon 观测资源;必须新建窗口、session、端口、venv、临时目录时,用固定 marker 或可追踪 run id 标识,并在
finally/ 结束清单中关闭或说明保留理由。禁止只跑完实验不清理测试窗口、临时服务、pipe-pane、debug port。 - iTerm2 Python API pane 坐标进入 daemon/stream 真源时,leaf
frame.x/y只保证相对其 immediate splitter;嵌套 split 必须先按 splitter cursor/offset 递归 flatten,再套一次 window top-left + content inset。禁止在度量阶段再次累加已定位 leaf offset;必须用真实复杂 split tree 回归和 live crop bounds gate 锁cropRect不越过 window。 - Remote window catalog 不能把 target 限死在 tmux 或 iTerm2:generic macOS app-window 走 daemon-side window catalog truth;无 tmux 映射的 iTerm2 pane 仍是 selectable
iterm2-pane,不能造假 tmux id 或拒绝选择。live gate 至少断言nonItermAppWindows >= 1、nonTmuxPanes >= 1、outOfBounds == 0。
2.4 Desktop workspace owner gate
- Mac desktop workspace / multi-window / pane-tab-runtime / file browser 重构必须先查并同步:
mac/docs/function-map.mdmac/docs/mainline-call-map.jsonmac/docs/testing/mac-desktop-workspace-test-design.md
- 未落地 owner 只标
binding pending,禁止伪造 symbol / caller / callee。 - 初始架构 gate 只锁当前已成立事实与 map parseability;不要把后续切片才会修的 transitional debt 提前做成误报红测。
- 当实现进入对应切片后,必须把 pending 规则升级为 hard gate,例如 runtime 创建只能在
MacRuntimeRegistry,pane UI 不得直接connectRemote/connectLocalTmux。
三、强制闭环流程(Jason 新冻结)
3.1 默认执行顺序
任何 Mac 改动,默认按下面顺序闭环;没走完,不要向 Jason 报“可以手测”。
改代码
-> type-check
-> build
-> package(若影响 packaged 行为)
-> 退出旧 app
-> 启动新 app / 新包
-> 自己完成 smoke
-> 采集证据(命令输出 / 截图 / 资源快照)
-> 只有证据闭环后,才能汇报 Jason
3.2 触发 packaged smoke 的场景
出现以下任一项,必须跑 packaged app smoke,而不只 dev server:
- Electron main / preload / IPC 改动
- 本地 tmux 接入改动
- 窗口恢复 / 启动 / 单实例 / app 生命周期改动
- renderer 资源占用 / 退出态 / orphan process / memory leak 排查
- 用户明确说“我要实际使用”“我要重新编译安装”
3.3 最低验证门槛
A. 静态门槛
pnpm --filter @zterm/mac type-check
pnpm --filter @zterm/mac build
B. 核心连接门槛
Mac terminal / transport / runtime 改动必须先跑本地客户端核心连接 gate,不能用 daemon-only probe 替代。
pnpm --dir mac test -- --reporter dot
pnpm --dir mac run type-check
最低覆盖面:
bridge-transport:remote daemon WebSocket 两阶段握手、connected state、head/body/input 发到 live socket、stale socket 不污染当前连接。local-tmux-transport:Electron local tmux API connect、connected event、head/body request、input/resize/activity/disconnect 同一 clientId。terminal-runtime:head 变化触发 body sync,same-end revision 变化不能被去重吞掉。- workbench active target:tab/pane 切换不重复 reconnect,不把 local/remote target 混成第二状态机。
证明范围:
- 证明 Mac client transport/runtime 核心连接逻辑可用。
- 不证明 packaged
.app、真实窗口、DOM 输入、资源/退出态已闭环。
C. packaged 门槛(按需)
pnpm --filter @zterm/mac package
D. 运行态 smoke 门槛
至少覆盖本轮改动直接影响的主路径:
- terminal 能打开
- input / resize / scroll / split / tab 中与本轮相关的关键路径
- local tmux 或 remote bridge 至少一条真实链路
- terminal buffer/render 正确性必须比较 session truth 和 app render output:
- session truth:
tmux capture-pane - input oracle:专用 session 的
tmux pipe-pane - app output:packaged app DOM rendered rows / 截图
- 必跑 gate:
pnpm --dir mac run blackbox:terminal-buffer -- --case=all - 必须包含持续刷新底部 TUI case;只看到
connected、底部几何对齐或静态截图不算 terminal 数据闭环 - blackbox gate 必须复用固定专用 tmux session:
zterm_mac_gate_sequence/zterm_mac_gate_tui/zterm_mac_gate_large,并用 tmux option marker 验证 owner/case 后才允许 respawn / clear-history / cleanup;禁止 timestamp 新建一串 session,禁止碰无 marker 的用户 session - packaged app 启动必须先 ad-hoc 重签 bundle,再带显式
--user-data-dir直接启动二进制;禁止open -nunsigned package(Launch Services 会在第二次或之后把 unsigned bundle 重新走 Gatekeeper,常见“恶意软件/移到废纸篓”回归)。正确顺序见 §3.6 Packaged App 启动协议。 - blackbox gate 默认保留这三个固定 session 作为复用池;只有显式
--cleanup-sessions才能在 marker 验证通过后精确关闭它们。运行结束必须复核tmux list-sessions,确认没有遗留新的zterm_mac_*临时 session - TUI fixture 每次 run 前必须重置内容和清 history;持续刷新只比较当前可见 screen 与 app rendered rows,历史/overscan 只能作为 raw evidence,不能进入 lag 判定
- large-reading fixture 必须证明真实 scroll 容器进入 reading:
scroll.atBottom=false、append 后 reading rows 不变、scroll-to-bottom 后 app tail 与 tmux tail 一致;若clientHeight === scrollHeight,先修父容器高度约束,不准把 DOM 全量内容当作 reading 通过
- session truth:
- 若改的是资源/生命周期:补
ps/top资源采样 + 退出态进程检查
E. 证据门槛
证据至少二选二:
- 命令输出
- app 截图
- 进程 / RSS / CPU 快照
- 必要时日志 / sample
证据落点:
mac/evidence/<date>-<topic>/
四、资源/泄漏专项闭环
4.1 资源审计最低动作
ps -axo pid,ppid,pgid,%cpu,rss,vsz,etime,comm | egrep 'PID|ZTerm|Electron Helper'
top -pid <renderer_pid> -stats pid,cpu,mem,threads,state,time -l 2
4.2 退出态检查
- 先退出旧实例
- 确认旧 PID 消失
- 再启动新实例
- 不允许跳过这一步就汇报“没有孤儿进程”
4.3 报告规则
- 先给证据,再给结论
- 若只完成编译、未完成运行态 smoke,只能报告“代码已编译,通过静态验证,未完成运行态闭环”
- 若只完成 dev server、未完成 packaged smoke,不能向 Jason 说“可安装使用”
五、反模式
- 编译过了就让 Jason 手测
- daemon/tmux probe 绿了就宣称 Mac client 连接正常
- Mac client core tests 绿了就宣称 packaged
.app正常 - 只在浏览器里验证,却汇报 packaged app 可用
- 没退出旧 app 就直接打开新 app
- 没有运行态证据就下“无泄漏 / 无 orphan / 性能已优化”结论
- 让 Jason 帮忙补你本该先完成的基础 smoke
六、完成态汇报模板
只在闭环完成后使用:
Jason,已完成本轮自闭环:
1. 改动:
2. 静态验证:type-check/build/package 结果
3. 运行态 smoke:做了哪些真实操作
4. 证据:截图/命令输出/资源采样位置
5. 结论:哪些已验证通过,哪些仍未覆盖
6. 现在才轮到你手测的部分:
3.4 本地 package 签名授权规则
- 本地
pnpm --filter @zterm/mac package必须默认跳过 macOS code signing:CSC_IDENTITY_AUTO_DISCOVERY=false+build.mac.identity=null。 - 禁止让
electron-builder自动发现 distribution identity;否则每次 package 都可能触发 Keychain 授权弹窗。 - 只有正式发布/分发签名任务才允许显式启用签名 identity,并必须单独说明签名和 notarization 验证。
- 本机安装 unsigned package 前,先对
.app做 ad-hoc 重签:codesign --force --deep --sign - <ZTerm.app>,再复制到实际目标路径并对目标再签一次。目标路径必须从运行中进程或 Jason 实际点击入口确认,优先检查/Applications/ZTerm.app、$HOME/Applications/ZTerm.app、~/Downloads、~/Desktop、~/.Trash;不要只修/Applications后宣称完成。 - 不要使用
xattr -cr处理.appbundle;它可能生成._*AppleDouble 文件并破坏 sealed resources。若误生成,只能在该.app内精确删除._*后重新签名。可精确删除com.apple.quarantine;不要把spctl --assess rejected当作 unsigned internal alpha 的启动失败证据,真实判定必须用 Finder/open 启动和进程路径。 - 若 Finder 提示“恶意软件并移到废纸篓”,先查实际入口包的
codesign --verify --deep --strict与spctl --assess --type execute --verbose=4。若输出notarization indicates this code has been revoked,根因是旧 revoked 包仍在实际路径,必须退出该路径的运行中明确 PID、把旧包改名备份、安装当前构建、重签、再从同一路径启动验证;osascript tell application "ZTerm" to quit可能被 revoked app 挂住,卡住时只结束该明确osascriptPID,再用旧 ZTerm 明确 PID 关闭。
3.6 Packaged App 启动协议(2026-08-27 冻结)
Unsigned 本地 package 启动必须严格按下列顺序,禁止改换步骤顺序。
- 断言 bundle 存在:
<workspace>/mac/out/mac-arm64/ZTerm.app/Contents/MacOS/ZTerm必须存在;缺失则直接失败并提示先跑pnpm --dir mac run package。 - ad-hoc 重签 bundle 一次(每次启动前都重签,避免上次遗留 attribute 被 Launch Services 缓存):
codesign --force --deep --sign - "$APP_PATH" - 删除 quarantine xattr(仅作用于本 bundle,绝不
xattr -cr):xattr -d com.apple.quarantine "$APP_PATH" - 用
--user-data-dir直接执行二进制,绝对不要open -nunsigned package:"$APP_PATH/Contents/MacOS/ZTerm" \ --remote-debugging-port=<port> \ --user-data-dir="$EVIDENCE/user-data" \ --no-sandbox \ > "$EVIDENCE/launch-stdout.txt" 2> "$EVIDENCE/launch-stderr.txt" & - 必须捕获子进程 exit/stderr/stdout 写入 evidence。gate 启动后立刻
tail -fstderr 不能阻挡主循环;用child.on('exit', ...)/child.on('error', ...)+spawn(不是spawnSync),把 exit code、stderr 写到${EVIDENCE}/launch-exit.json/launch-stderr.txt。 - 强规则:
- 禁止
open -n <unsigned.app>启动 unsigned package;这是 Launch Services 重走 Gatekeeper 的常见触发。 - 禁止
xattr -cr <unsigned.app>;会生成._*AppleDouble 破坏 sealed resources。 - 禁止把
spctl --assess rejected当作 unsigned internal alpha 的启动失败证据。真实判定必须看 Finder/open 启动 + 进程路径 + CDP/json/version+/json/list返回目标 page target。 - 重签后必须
codesign --verify --deep --strict立即确认valid on disk。
- 禁止
- 重复启动前必须清理旧进程:先
ps -axo pid,comm,args | egrep 'ZTerm|remote-debugging-port=<port>',再按 PID kill;禁止叠多个 instance 污染端口和 evidence。
3.5 状态 / Alpha 汇报对账门禁
- 触发:Jason 问“今天完成了什么”“Mac 版本状态”“离 alpha 多远”“能不能手测/alpha 测试”,或上下文压缩/恢复后需要汇报 Mac 进度。
- 汇报前必须对账:
git log --oneline -- mac packages/shared .agents/skills/zterm-mac-devgit status --shortmac/MEMORY.mdmac/task.mdmac/docs/function-map.mdmac/docs/testing/mac-desktop-workspace-test-design.mdmac/docs/alpha-readiness.md(若不存在或过期,先补齐)mac/evidence/<date>-*证据目录索引
- 汇报必须按证据层级拆开:已提交代码、白盒测试、packaged smoke、真实 daemon/local tmux/live UI、未验证缺口。
- 不得只根据最近 handoff、loop run、Android note 或当前聊天上下文汇报 Mac 状态;Mac 进度以 Mac docs/MEMORY/task/evidence/git 提交共同对账为准。
- 如果发现 evidence 仍是 untracked,只能报告 retention/cleanup 待决策;未经授权不得删除、移动、stage 或提交。
六、Mac dev runtime 单实例验证规则(2026-06-02)
- 调试 Electron dev app 时,同一轮只能保留一个
--remote-debugging-port实例;新开前先用明确 PID/app-level quit 收掉旧实例,禁止多端口并发导致证据串线。 - 截图/DOM probe/输入验证必须指向同一个 CDP target、同一个 tmux session、同一个 evidence JSON;不得在多个 Electron 窗口之间交叉取证。
- 临时 CDP probe 不要往页面注入会持久影响事件链的监听器/异常代码;若注入失败导致 renderer error,必须 reload 或重启唯一实例后再验证。
- local tmux 颜色真源是
tmux capture-pane -e的 SGR 输出;纯capture-pane -p只保留文本,会把fg/bg全部退成默认色。 - local tmux 数据真源还必须覆盖 visible pane bottom:
LocalTmuxManager的 head/sync capture 保留-e -p,但禁止加-E -1;若黑盒出现 tmux/pipe 有完整尾部而 app DOM 缺尾部,先查该 capture 参数,不要在 renderer 补偿。 - local tmux TUI/alternate-screen 类刷新不得走 full-history live payload;
readSessionCapture检测alternate_on后只用 bounded visible capture(-S -<paneRows>)作为当前 screen truth,避免历史帧累计成刷新延迟或旧行上移。 - Packaged multi-window smoke 不依赖
System Events注入快捷键作为真源;优先通过正式 preload IPC / menu owner 触发MacWindowManager.createWindow(),再用 CDP 验证 page target、rendererwindowId、workspace key、quit/reopen restore。若System Events卡住,只中断该明确 osascript 会话,不能用它证明失败或成功。 - Packaged app 实际使用
preload.cts -> preload.cjs。凡修改window.ztermMacbridge 或 IPC surface,必须同步更新preload.ts与preload.cts,并用 packaged smoke 证明真实 preload bridge 可用;不能只看 renderer type 或preload.ts。 - Packaged React 表单/controlled input smoke 不把直接
input.value = ...当真源;自动化应先 focus/select 目标 input,再用 CDPInput.insertText或等价真实输入路径触发 React state,最后点击正式 UI command。直接 setter 只可作诊断,不能作为 browse/connect/save 成功证据。 - Packaged QuickConnect/session discovery smoke 必须走真实 UI input path 和真实 daemon
list-sessions路径:focus/select input 后用Input.insertText输入 host/port/token,点击正式 Discover / Save & connect;证明 discovery 不创建 runtime,Save & connect 才创建 remote runtime;只允许 dedicated marked tmux session;写入 evidence 前必须 redactedauthToken/targetAuthToken/ storage token 字段。 - Packaged CDP smoke helper 必须在 websocket
close/error时 reject pending command,尤其是Browser.close;否则数据对比已绿也会因为未 settle 的 top-level await 退出 13,并且缺少process-after-closeevidence。 - Packaged runtime A/B input isolation smoke 优先用本轮专用 tmux session +
tmux pipe-pane -o <log>作为输入 oracle;capture-pane对 detachedcatfixture 可能不稳定,不能单独证明 app input 到达或串线。完成后用tmux pipe-pane -t <session>关闭观测管道,避免后台持续写日志。 - Runtime split/tab smoke 中,resize 必须同时看 DOM pane width 和 workspace record pane size;只看拖拽动作或 divider 存在不算 resize 闭环。关闭 active pane 后必须证明 renderer root 仍 mounted、workspace
activePaneId指向现存 pane、剩余 runtime 还能输入。 - Server rail remote refresh smoke 是 read-only daemon observation:只能发
list-sessions/Refresh,允许用现有用户 sessions 做列表观测,但禁止写 input、create、kill、rename。证据必须同时证明 refresh 后 live sessions 进入 rail、workspace pane/tab 数不变、terminal stage 未自动打开 session、错误时显示 error 且 saved/open sessions 保留。 - Server rail remote open packaged smoke 必须分两阶段证明:Refresh 后只更新 live projection 且
runtimeEnsureCalls=0,explicit rail session click 后才创建 remote runtime 并渲染 dedicated marked session 输出。只能使用本轮 dedicated marked session,evidence/storage 里的authToken/targetAuthToken必须 redacted,结束时复核 debug port、ZTerm/Electron helper、tmux session 已精确清理。 - Disconnect/reconnect packaged smoke 必须诱发 transport owner close/error,不能用 UI Disconnect 冒充断线。local smoke-only forced close 只能挂在
--zterm-alpha-smoke下,证据必须同时证明 active runtimeerror -> Reconnect -> connected、hidden runtime connect count 为0、windowId稳定、process-after-close*为空或有明确解释。 - Legacy workspace cleanup closeout 不能只扫入口 import。必须同时证明旧 all-in-one source 文件物理不存在、生产源码无
ShellWorkspace引用、architecture truth gate 锁MAC-16-LegacyRemoval、packaged DOM 无.shell-workspace-root/ forbidden root。历史zterm:mac:shell-workspace:v1localStorage 残留只说明用户数据未清理,不可当作 fallback 存在或已读取的证据。
七、单 session 操作铁律(2026-06-02 新增)
7.1 禁止向任意 session 写入 input
- 绝对禁止:
tmux send-keys、send-text到任何不在本次测试范围内的 tmux session - 绝对禁止:用 IPC / CDP / AppleScript 向非目标 window/pane 注入按键
- 原因:会污染用户真实工作 session(如
fin、rcc、server等已有 session) - 触发场景:任何 input echo 验证、按键注入测试、session 列表遍历
- 正确做法:只操作本次 smoke 专用 session(如
zterm_mac_color),不碰其他 session - 验证方式:
tmux capture-pane -p -t <session>确认只含测试内容,无污染