Windows 对接规范与故障排查
目的:nightme 的 bridge 层在 Windows 上有 4 个跨 CLI 的陷阱(env 格式、.cmd/.bat shim、argv quoting、信号语义),单独踩一次排查成本极高。本文把这些坑 + 修复 + 测试方法一次性沉淀下来。 关联文档:
- bridge 层协议细节 →
bridge/- 单 CLI 实测约定 →
bridge/CLAUDE.md§7(真机 vs mock)、§9(代码锚点)
0.0 跨平台经验规则(fix-stop 沉淀)
写跨平台 Go 代码时,优先用跨平台兼容方案;不行则按平台分别实现。
按经验重要度排:
| # | 规则 | 反例 | 正例 |
|---|---|---|---|
| 1 | 测试生产契约,别测子进程 stdout/stderr | assert stdout == "C:\\foo"(MSYS 翻译后是 /c/foo) |
让 child 写 sentinel file,用 os.Stat(sentinelPath) 验证 cmd.Dir 是否生效(filesystem 操作不经过 MSYS libc) |
| 2 | 跨平台逻辑放 tagless 文件 | cmd.Env = append(os.Environ(), "MSYS_NO_PATHCONV=1") 放在 exec.go(所有平台编译) |
applyMSYSEnvNoPathConv 在 exec_windows.go,no-op stub 在 exec_unix.go |
| 3 | 平台特定逻辑放 //go:build 文件 | path_windows.go 的 isWindowsDriveRel 提到 path.go 里加 runtime.GOOS 分支 |
path_unix.go(//go:build !windows)只管 POSIX;path_windows.go(//go:build windows)只管 Win32 盘符、UNC、root-relative |
| 4 | call site 不写 runtime.GOOS |
if runtime.GOOS == "windows" { runMSYS() } |
调用 applyMSYSEnvNoPathConv(env)(在 _windows.go / _unix.go 各有定义) |
| 5 | path 验证用 filepath.Clean + 平台 separator |
assert want == "/foo/.bar" |
assert want == filepath.FromSlash("/foo/.bar")(测试期望跟随平台) |
| 6 | env vars 跨平台覆盖用 os.Environ() + append |
自己造一份完整 env(漏 HOME / PATH) | cmd.Env = append(os.Environ(), "KEY=VALUE")(只追加需要的,不替换全部) |
| 7 | MSYS env var 仅控制 argv 翻译,不控制 getcwd | 设 MSYS_NO_PATHCONV=1 后断言 child 报告 exact path |
测 child 实际行为(写文件、读文件)——filesystem 不经过 MSYS libc |
0.1 当前 Windows 13 个测试失败的根因(不是 fix-stop 引入)
PR 186 引入 Windows CI runner 后,暴露了 13 个 pre-existing 平台问题。这些与 fix-stop 无关,已记在 docs/branch/fix-stop-windows-followups.md。按根因分类:
| # | 失败 | 根因 | 归类 |
|---|---|---|---|
| 1 | TestSendBlocks_FileStillUsesFileURL (opencode) |
opencode 把 C:\foo 渲染成非 file:// URL,跨平台差异 pre-existing |
bridge 协议问题 |
| 2 | TestDownloadInboxDir_CreatesPerSessionDir (feishu) |
inbox dir mode = 0777, want 0700 — Windows 不支持 POSIX mode 位 |
平台权限 API |
| 3 | TestFixRemote_HappyPath (gtw) |
git worktree 输出 MSYS 翻译路径(/c/Users/...) |
跨平台 git 行为 |
| 4 | TestFormatResults_ShowsFailure (gtw) |
cmd /c "echo oops 1>&2; exit 5" 在 Windows 上 ExitCode 不为 5(sh 与 cmd 重定向语义差异) |
dispatcher Windows shell |
| 5 | TestRefreshDefaultBranch_RebaseConflict (gtw) |
fatal: pathspec 'shared.txt' did not match any files — git on Windows pathspec 行为 |
跨平台 git 行为 |
| 6 | TestPreflightWorktreeCreate_ParentUnwritable (gtw) |
os.Getenv("GOOS") == "windows" 在 CI runner 上没正确返回 |
测试 guard 用错 API |
| 7 | TestWindowsPipePingStatusStop (daemoncontrol) |
Windows named pipe 在 server 关闭前就报 "pipe has been ended" | Windows pipe 时序 |
| 8-12 | TestLogger_* (logging) 5 个 |
(a) mode = 666, want 0600 — Windows chmod no-op (b) TempDir RemoveAll cleanup: file in use by another process — Windows 文件锁延迟释放 |
跨平台文件权限 + cleanup 竞争 |
| 13 | TestRenderANSI (feishu/qrencode) |
QR 渲染输出在 Windows console 宽度不同 | 跨平台终端宽度 |
所有 13 个 = pre-existing 平台问题,不在 fix-stop scope。cmd.Dir 契约相关的 5 个测试(TestRunCmd_DirBinding 系列)已通过 sentinel file 验证修复(PR 186 内 commit c2e3c2d)。
0. 一句话速查
| 现象 | 根因 | 修复锚点 |
|---|---|---|
fork/exec C:\WINDOWS\system32\cmd.exe: The parameter is incorrect. |
env 里有裸字符串(无 KEY=VALUE 格式),CreateProcess 拒绝 |
不要往 cmd.Env 追加裸字符串;详见 §2 |
fork/exec <path>.cmd: The parameter is incorrect. |
直接用 .cmd 路径作为 lpApplicationName |
用 proc.New,自动包 cmd.exe /d /c;详见 §3 |
.exe 装了但仍走 cmd.exe /d /c |
LookPath 顺序把 .cmd 排在前面 |
当前行为可以接受;如要绕开,配置 agent_cmd.exe 显式路径;详见 §3.4 |
| Agent 启动后立刻退出 | Setsid 不可用 + 进程组信号被误用 |
Windows 下 agent.SignalProcessGroup 已经退化为单 pid 信号;详见 §5 |
child 的 pwd 报告 /c/Users/... 而不是 C:\Users\... |
MSYS libc getcwd() 翻译(MSYS_NO_PATHCONV=1 不影响 getcwd) |
不要测 child stdout 验证 CWD;测 sentinel file(filesystem 操作不经过 MSYS libc) |
§0.1.1 详细案例:cmd.Dir 契约测试为什么用 sentinel file 而不是 pwd
问题:internal/command/gtw/exec_test.go 之前用 pwd 验证 runCmd 设置的 cmd.Dir:
stdout, _, _ := runCmd(ctx, real, "pwd")
assert stdout == real // "C:\foo" expected
Windows CI 上失败:
runCmd: child CWD = "/c/Users/runneradmin/AppData/Local/Temp/TestRunCmd_DirBinding4047655615/001"
want = "C:\\Users\\runneradmin\\AppData\\Local\\Temp\\TestRunCmd_DirBinding4047655615\\001"
根因:pwd 是 MSYS-bash 工具,glibc 的 getcwd() 被 MSYS libc 拦截,总是返回 MSYS 翻译后的路径(/c/...)。这是 MSYS libc 层的硬编码行为,MSYS_NO_PATHCONV=1 不影响(该 env var 只控制 argv 翻译,不控制 getcwd)。
真正修复:测试契约而不是 child 的 cwd 报告。Child 在 cmd.Dir 写 sentinel file,test 在 EXACT path 用 os.Stat 验证文件存在:
sentinelName := fmt.Sprintf("nightme-sentinel-%d", os.Getpid())
runCmd(ctx, real, "sh", "-c", "echo X > "+sentinelName) // or cmd.exe on Windows
os.Stat(filepath.Join(real, sentinelName)) // verifies cmd.Dir was honored
os.Stat 直接走 kernel(不经过 MSYS libc)。如果 cmd.Dir 没被设到 real,sentinel 会写到别的目录,stat 会失败。
关键点:
- test 用 cmd.exe(Windows-native)做写文件,不走 MSYS shell 层
- stat 验证用真实 path(不是
filepath.FromSlash转换的) - 契约:"child 真的能在 cmd.Dir 操作" — 这才是 nightme 的生产承诺
适用规则:#1(测试生产契约)+ #7(MSYS env 不影响 getcwd)。
1. 背景
nightme 是单进程 daemon,把 Claude Code / Codex / OpenCode / Pi 四种 CLI 桥接到 IM 通道。bridge 层(internal/bridge/<name>/)通过 spawn 子进程 + 接管 stdin/stdout/stderr/stderr pipe 驱动协议。
Windows 的 4 个跨平台差异会在这里集中暴露:
CreateProcess严格校验lpEnvironment——必须是KEY=VALUE数组,裸字符串直接拒绝。.cmd/.bat不能作为lpApplicationName——必须显式包cmd.exe /d /c。- 没有
Setsid等价物——Setsid 在 Linux/macOS 上用来脱离 controlling TTY,Windows 走STARTUPINFO句柄传 console handle,问题不一样。 - 没有进程组 /
SIGINT到整个 pgroup——Windows 只能Process.Signal(pid, sig),子进程的工具子进程会泄漏。
下面逐条展开。
2. ⚠️ Env 格式校验(最隐蔽的 bug)
2.1 现象
Windows 端启动 pi / claude 报:
[pi] Start cmd.Start failed err="fork/exec C:\\WINDOWS\\system32\\cmd.exe: The parameter is incorrect."
错误指向 cmd.exe 而非被启动的 pi.cmd——这是因为 wrapper 已经触发(把 pi.cmd 包给了 cmd.exe),但 cmd.exe 启动时被拒。
2.2 根因
bridge 在拼 env 时多写了一句:
env := append([]string(nil), cfg.Env...)
env = append(env, s.command) // ← 把裸字符串 "pi" / "claude" 加进 env
Windows 的 CreateProcess 拿到 lpEnvironment 后,会逐条验证格式 KEY=VALUE。裸字符串 "pi" 不带 = → 返回 ERROR_INVALID_PARAMETER (87) → fork/exec 失败。
Unix 上的 execve(2) 没有这条校验,所以同样的代码在 Linux 跑没问题——这正是 PR #158 引入的 .cmd wrapping 在 Unix CI 上通过、移植到 Windows 才炸的根因。
2.3 修复
删掉裸追加。只接受 KEY=VALUE 形式:
env := append([]string(nil), cfg.Env...)
// env 永远保持 []string{"KEY=VALUE", ...}
cmd.Env = append(os.Environ(), env...)
涉及位置(commit 修复):
internal/bridge/claudecode/claudecode.go:165— 删除env = append(env, s.command)internal/bridge/pi/agent.go:268— 同上
codex / opencode 当时没有同款 bug,无需改动。
2.4 怎么测出来
永远在 Windows 上做 end-to-end 冒烟,不要相信 Unix CI。
最小复现(已写在修复的 regression 思路里):
cmd := exec.CommandContext(ctx, `C:\WINDOWS\system32\cmd.exe`, "/d", "/c", `C:\path\pi.cmd`, "--mode", "rpc")
cmd.Dir = `D:\workspace`
cmd.Env = []string{"pi"} // ← 任何裸字符串都触发
stdin, _ := cmd.StdinPipe()
stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
err := cmd.Start() // ❌ The parameter is incorrect.
os.Environ() 默认是合规的——问题永远是 bridge 自己拼 env 时引入了裸项。
2.5 怎么防止回归
- bridge 包
env构造时,只接受 cfg.Env(已通过配置加载,格式合规)。 - 不要为「防御性」往 env 加任何裸字符串——没有用,反而炸 Windows。
- 如果真要给子进程传 agent 名作为环境变量,用
AGENT_COMMAND=pi形式(命名 + 显式 key),不要裸追加。
3. .cmd / .bat shim 启动矩阵
3.1 Windows 安装生态
nightme 支持的 4 个 CLI,在 Windows 上有以下分发形式(按 PATHEXT 顺序 .COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC):
| CLI | 常见形式 | LookPath 解析结果 |
|---|---|---|
claude |
npm 包内的 @anthropic-ai/claude-code.cmd + 真实 .exe |
claude.cmd(先命中) |
codex |
npm 包内的 codex.cmd + codex.exe |
codex.cmd(先命中) |
opencode |
npm 包内的 opencode.cmd + opencode.exe(实际就是 node_modules\opencode-ai\bin\opencode.exe) |
opencode.cmd(先命中) |
pi |
pi-node 的 pi.cmd + pi(无扩展的 PE 二进制) |
pi.cmd(先命中) |
关键事实:几乎所有分发都是「先放个 .cmd shim 在 PATH 顶层,shim 内部再去调 .exe」。所以 LookPath 100% 返回 .cmd 路径。
3.2 启动矩阵(proc.New)
internal/proc/exec_windows.go 的 launchOnWindows 是单一入口,按扩展名路由:
| 扩展名 | 启动方式 | 何时触发 |
|---|---|---|
.exe / .com / 无扩展 |
exec.CommandContext(resolved, args...) 直调 |
用户显式配置绝对 .exe 路径时 |
.cmd / .bat |
exec.CommandContext(cmd.exe, /d, /c, resolved, args...) |
默认(npm shim) |
.ps1 |
exec.CommandContext(powershell.exe, -NoProfile -NonInteractive -ExecutionPolicy Bypass -File resolved, args...) |
备用 |
.js |
exec.CommandContext(node.exe, resolved, args...) |
备用 |
所有 4 个 bridge 都通过 proc.New 启动——没有「各自实现一套」的地方。
// claudecode.go / pi/agent.go / codex/session.go / opencode/server.go
cmd := proc.New(ctx, command, args...) // ← 统一入口
cmd.Dir = cfg.Workspace
cmd.Env = append(os.Environ(), cfg.Env...) // ← 注意 §2
3.3 为什么必须包 cmd.exe /d /c
CreateProcess 拒绝以 .cmd / .bat 路径作为 lpApplicationName,错误码 ERROR_INVALID_PARAMETER (87)。Microsoft 官方 workaround:
lpApplicationName = C:\Windows\System32\cmd.exe
lpCommandLine = cmd /d /c "<resolved>" <args...>
/d跳过 AutoRun registry 命令,与 cmd.exe 交互模式一致/c执行命令后退出
3.4 .exe 装了但仍走 wrapper
正常情况:LookPath("pi") → C:\...\pi.cmd → 命中 .cmd 分支 → cmd.exe 包一层 → cmd.exe 调 pi.cmd → pi.cmd 再调 pi.exe。两次间接,argv 已经正确传递,不会出错。
如果你想跳过 .cmd 这一层(理论上少一层 cmd.exe):
- 在
cmd/nightme/agents.go注册时改成绝对.exe路径:agent.Builtins.Register(pi.NewStarter("pi", `C:\path\pi.exe`, nil)) - 或在用户配置里覆盖
command字段。
不要在 bridge 包里做 LookPath 重定向——那是 proc.New 的活。
4. 测试方法
4.1 真机 / Mock 分层
| 类型 | 何时用 | PATH 守卫 |
|---|---|---|
| 真机(spawn 真实 cli) | stream-json、resume、多轮 stdin、env 行为 | 必须 requireReal<Name>(t) |
Mock(testdata/*.sh / .py) |
argv / 解析 / 协议单测 | 不要 skip — CI 必跑 |
参考 bridge/CLAUDE.md §7.1 的真机守卫 pattern。
4.2 Windows 上必须覆盖的真机场景
go test 在 Unix 上跑通 ≠ Windows 上没问题。至少 跑一次下列矩阵:
| 场景 | 验证目标 | 命令 |
|---|---|---|
pi Start 成功 + handshake 成功 |
.cmd 包 + env 合规 | 见 §4.3 |
claude Start 成功 |
同上 | 同上 |
codex Start 成功 + initialize + thread/start |
同上 | 同上 |
opencode Start 成功 + 解析 banner |
同上 + banner regex 兼容 | 同上 |
| 三 pipe(stdin/stdout/stderr)+ cmd 启动 | §2.4 的最小复现 | 见 §4.4 |
ComSpec 自定义 |
fallback 不写死 C:\Windows\... |
单测 |
4.3 一次性验证全部 4 个 agent
// cmd/repro_test/main.go(修复期临时建;CI 不入仓)
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/cnlangzi/nightme/internal/agent"
"github.com/cnlangzi/nightme/internal/bridge/claudecode"
"github.com/cnlangzi/nightme/internal/bridge/codex"
"github.com/cnlangzi/nightme/internal/bridge/opencode"
"github.com/cnlangzi/nightme/internal/bridge/pi"
)
func main() {
os.Setenv("PATH", `C:\Users\<you>\AppData\Local\pi-node\current;`+os.Getenv("PATH"))
os.Setenv("NIGHTME_PI_DEBUG", "0")
os.Setenv("NIGHTME_CODEX_DEBUG", "0")
os.Setenv("NIGHTME_OPENCODE_DEBUG", "0")
os.Setenv("NIGHTME_OPENCODE_HOME", `D:\andy`)
agents := []struct {
name string
starter interface {
Start(ctx context.Context, cfg agent.StartConfig) (*agent.Agent, error)
}
}{
{"pi", pi.NewStarter("pi", "pi", nil)},
{"claude", claudecode.NewStarter("claude", "claude", nil)},
{"codex", codex.NewStarter("codex", "codex", nil)},
{"opencode", opencode.NewStarter("opencode", "opencode", nil)},
}
for _, a := range agents {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
ag, err := a.starter.Start(ctx, agent.StartConfig{Workspace: `D:\andy`})
cancel()
if err != nil {
fmt.Printf("%s Start error: %v\n", a.name, err)
continue
}
fmt.Printf("%s Start ok, pid=%d\n", a.name, ag.PID())
_ = ag.Close()
}
}
期望:4 个 Start ok, pid=...,没有 error。Handshake / banner / 后续事件再单独断言。
4.4 最小 env 复现
任何 Windows 上「fork/exec 失败」的修复都要先跑通这段(修复后应 ok):
cmd := exec.CommandContext(ctx, `C:\WINDOWS\system32\cmd.exe`,
"/d", "/c", `C:\path\pi.cmd`, "--mode", "rpc")
cmd.Env = []string{"FOO=BAR"} // 唯一一行 KEY=VALUE
cmd.StdinPipe(); cmd.StdoutPipe(); cmd.StderrPipe()
err := cmd.Start() // ✅ nil
如果 env 多写一行裸字符串(如 "pi") → err: The parameter is incorrect.,对照实验成立。
4.5 CI 覆盖
internal/proc/exec_windows_test.go 已有 5 个回归测试(TestNew_*),任何 CI runner 上跑 go test ./internal/proc/... 都会覆盖 launch matrix。不要靠 Unix CI 替代 Windows 真机测。
5. 信号 / 进程组差异
5.1 agent.SignalProcessGroup 行为
| 平台 | 行为 |
|---|---|
| Unix | syscall.SysProcAttr{Setsid: true} 让子进程成为 pgroup leader;kill(-pgid, SIGINT) 广播到整个子树 |
| Windows | 没有 pgroup 等价物 → p.Signal(os.Interrupt) 退化为单 pid 信号 |
→ claude / pi 里 Bash 工具子进程在 Windows 上可能泄漏。
5.2 影响范围
- 功能:不影响单次 prompt 完成(CLI 自己收到 SIGINT 退出)
- 资源:Bash 子进程可能滞留 → 重启 daemon 时
nightme kill --all应清理(详见 §6) - 修复:需要 Windows Job Object 跟踪子进程 → 未来 feature;当前已知 limitation
5.3 当前 workaround
Close()调Process.Kill()兜底:5s 内不退出就 SIGKILL(claudecode / pi 的shutdownGrace)- 退出时主动
cmd.Wait(),避免僵尸进程 - 不要在 Windows 上依赖「Ctrl-C 传到子进程的工具子进程」
6. 调试 checklist(Windows)
复制
bridge/CLAUDE.md §8之后追加。
按顺序排除:
- 错误信息是「The parameter is incorrect」吗?
- 是 → 检查
cmd.Env有没有裸字符串(§2) - 不是 → 转 §6.2
- 是 → 检查
- 错误信息是「not a valid Win32 application」?
- spawn 了一个非 PE 文件(如
.sh/.js没包解释器) - 检查是否走的是测试 mock(
testdata/*.sh);真机用cmd/nightme跑
- spawn 了一个非 PE 文件(如
- 错误信息是「executable file not found」或 PATH 找不到?
where <cli>验证 PATHos.Getenv("PATH")是否包含C:\Users\<you>\AppData\Local\pi-node\current- 注意:用户用 git-bash 启动 nightme 时,PATH 是 git-bash 风格(
/c/...),不是 Windows 风格——nightme 是 PE 二进制,直接看%PATH%
- 进程 spawn 成功但立刻退出?
- 看 stderr 是否有「permission denied」 / 「api key」 / 「model not configured」
- 检查
~/.claude/settings.json/~/.codex/config.toml等用户配置是否完整
- spawn 成功但 events / pipe 没有数据?
- 看 stdout / stderr 是不是空——可能是 CLI 在等 stdin(PTY vs JSON mode 不匹配)
- 确认
DefaultArgs里有--print/--mode rpc等「非交互」开关
- 进程已退出但 nightme 不感知?
- 看 readpump / lifecycle goroutine 是否在跑
cmd.Process.Wait()是否被调用(不调用 →cmd.Wait永不返回)
7. 代码锚点(改 Windows 行为前先读)
| 主题 | 位置 |
|---|---|
| 启动矩阵(路由) | internal/proc/exec_windows.go → launchOnWindows |
| 启动矩阵(单元测试) | internal/proc/exec_windows_test.go → TestNew_* |
| Unix 端等价(Setsid) | internal/proc/exec_unix.go → proc.New |
| 信号 / pgroup 退路 | internal/proc/exec_windows.go(hideWindow / applyHideWindow) |
| pi bridge spawn 调用 | internal/bridge/pi/agent.go → newDriver |
| claude bridge spawn 调用 | internal/bridge/claudecode/claudecode.go → newDriver |
| codex bridge spawn 调用 | internal/bridge/codex/session.go → newSession |
| opencode bridge spawn 调用 | internal/bridge/opencode/server.go → startServer |
| Bridge 注册表 | cmd/nightme/agents.go |
| REPL console 门控(VT 检测) | cmd/nightme/repl_console_windows.go → readlineUsable;stub 在 repl_console_unix.go |
| REPL 三路分发 | cmd/nightme/repl.go → runREPL(tty+VT → runREPLInteractive / tty 无 VT → runREPLScanner / non-tty → runREPLWith) |
| REPL 版本检查共享 helper | cmd/nightme/repl.go → runStartupUpdateCheck + scanRePLLoop |
8. 已知 limitation / 未做
- Bash 工具子进程泄漏:Windows 上 SIGINT 不到 pgroup 下的
Bash子进程;需要 Job Object 才能完整清理。 .ps1桥:当前实现走powershell.exe -File,未实测;Bypass -ExecutionPolicy在受限机器可能被组策略覆盖。.js直调:当前实现走node.exe,但 pi-node / npm shim 路径上几乎没有用户只装.js没装.cmd的场景——这是 defensive,未来按需启用。%ComSpec重定向:用户自定义ComSpec时comspecOrDefault会尊重;测试覆盖。PATHEXT顺序:当前.cmd永远赢过.exe,因为 npm shim 把.cmd放在 PATH 顶层;如果用户只有.exe没.cmd,自动命中.exe分支(无 wrapper),无需手工配置。- cmd.exe 行编辑 / 历史缺失:经典 cmd.exe(输出 console 无
ENABLE_VIRTUAL_TERMINAL_PROCESSING)上reeflective/readline不可用——VT 关则 CSI 乱码(nightme> [1 q[?25l[120D...),VT 开则库启动时挂起(“Plan C” 回归,commit6d29c03)。runREPL在此主机回退到runREPLScanner(scanner + 版本更新 prompt),但无行内编辑 / ↑/↓ 历史。Windows Terminal / ConPTY 不受影响(走runREPLInteractive,完整 readline)。恢复 cmd.exe 行编辑需 Win32-native 编辑器(ReadConsoleInputW+SetConsoleCursorPosition,不走 ANSI)——列为 follow-up。
9. 变更记录
-
2026-08-21:REPL Windows console 门控 + 版本更新 prompt 回到 cmd.exe
runREPL改三路分发:tty+VT →runREPLInteractive(readline);tty 无 VT(经典 cmd.exe)→ 新runREPLScanner;non-tty →runREPLWith- 新增
readlineUsable()(repl_console_windows.go/_unix.go):检测 stdout 的ENABLE_VIRTUAL_TERMINAL_PROCESSING,绝不SetConsoleMode开启(开启会让 reeflective 启动挂起——Plan C 回归6d29c03;关闭则 CSI 乱码) - 版本更新 prompt 抽成共享 helper
runStartupUpdateCheck,runREPLInteractive与runREPLScanner都跑;runREPLWith(测试 / non-tty)仍跳过以保 transcript 干净。scanner 循环抽成scanRePLLoop共享 - cmd.exe 上行编辑 / ↑/↓ 历史仍缺(reeflective 在该 host 不可用),需 Win32-native 编辑器 follow-up
paint()保持 identity(WindowsstyleEnabled恒 false)
-
2026-08-15:fix-stop 沉淀跨平台经验规则
- 新增 §0.0 七条跨平台经验规则(按重要度排序)
- 新增 §0.1 当前 Windows 13 个测试失败的根因分类(pre-existing,不在 fix-stop scope)
- 新增 §0.1.1 cmd.Dir 契约测试详细案例(为什么用 sentinel file 而不是 pwd)
- §0 一句话速查新增第 5 行(MSYS getcwd 翻译)
- 包含 fix-stop 的 cmd.Dir / MSYS / platform-split 修复
-
2026-08-13:建立本文档
- §2 env 格式校验 bug(claudecode / pi 已修)
- §3 启动矩阵统一进
proc.New - §4 真机测试方法 + 一次性 4-agent 复现脚本
- §5-6 Windows 专属信号 / 调试 checklist
- §7-8 代码锚点 + 已知 limitation