tianshu-mcp 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.en.md CHANGED
@@ -12,12 +12,55 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
12
12
 
13
13
  ### Planned
14
14
 
15
+ - **Visual acceptance phase 2 — AI visual content validation** (issue #13): validate whether image/page-screenshot *content* matches the task description (logo elements, style match, page semantics, etc.). Marked in issue #3 as an "optional extension"; its pixel-comparison phase 1 shipped with v0.5.0. Requires settling the model/credential source (without breaking the "zero credential management" red line), judgement debouncing, and gate placement (warning-only by default is suggested).
15
16
  - More external AI-Agent adapters (a new agent = one profile + an optional adapter file).
16
17
  - TraeWork executable discovery and native-dialog driving on macOS (currently fail-closed).
17
18
  - Optional project-level skill seeding (by default nothing is written into target repos).
18
19
  - Cancel/rework/new-project matrices for the Codex and ZCode GUI drivers on macOS (both remain `research` on darwin).
19
20
  - Best-effort stop of a GUI-side pending session (via a temporary CDP connection) when cancelling
20
21
  a task in the `needs_user` state.
22
+ - Real-machine verification of project-less dispatch on macOS (this round covers Windows 10 only).
23
+ - ZCode's **automatic import of an unregistered project** cannot complete on Windows: the native-panel script relies on `SetForegroundWindow` to bring the dialog forward before activating its address bar, but a child process of a background MCP server is refused by Windows, so the address-bar Edit never appears and the script spins until its deadline (measured: 56s, then classified by `budget.check()` as an exhausted setup budget). PowerShell's stdout is also block-buffered through a pipe, so killing the process loses the buffer and not a single `native:` stage reaches the log, misdirecting diagnosis. Workaround: add the target directory to the ZCode project list manually first; the fix direction is to grab the foreground with `AttachThreadInput` inside the script, or to use a supported ZCode registration entry point.
24
+
25
+ ---
26
+
27
+ ## [0.5.3] — 2026-09-15
28
+
29
+ **ZCode hardware-revisit fixes (issue #12, second round)**: three defects found on hardware are fixed — GUI instances could not outlive the server on Windows, "new task" did not switch pages and everything waited silently, and send-failure attribution was misleading. See the [v0.5.3 release notes](docs/release-v0.5.3.en.md) and the [Windows 10 acceptance record](docs/zcode-issue-12-windows-evidence.en.md).
30
+
31
+ ### Fixed
32
+
33
+ - **ZCode / Codex desktop instances could not outlive the server on Windows (found on hardware)**: the `zcode` and `codex` GUI instances were spawned behind a platform branch (`detached: process.platform !== "win32"`) while `traework` already used an unconditional `detached: true`. A minimal experiment (Windows 10 / Node 24.18.0) on the same spawn shows a non-detached child's survival after the parent exits is 0 and a detached child's is 1. As a result, as soon as the MCP server (or a one-shot smoke / probe script) exited on Windows, ZCode was killed along with it and `keptInstance`'s "instance survives the server exit" was a no-op — `needs_user` told the user to "handle it in ZCode, then call continue_task" while the window was already gone. The invariant now lives in one place, `guiInstanceSpawnOptions()`, shared by all three GUI instances (`codex` even branched `unref()` by platform; that branch is gone). The execution-type children in `verify/runner`, `visual/services` and `agents/spawn` keep their platform branch because their semantics are the opposite — they must be terminable as a group.
34
+ - **"New task" reported success without switching pages, then everything waited silently (found on hardware)**: when ZCode is parked on an existing conversation, the top-bar `conversation-new-task` is a lazily mounted icon — the click is dispatched and returns `true` but the page does not switch, and a conversation page's composer does **not** mount `composer-workspace-trigger`. Both the project-less default confirmation and the project-binding wait therefore idled until their deadline and reported nothing better than `needs_user/setup_recovery` (measured: 30 seconds of dead waiting). The draft is now verified by "the project trigger is mounted" after clicking new-task; when it is not, the flow falls back to the sidebar `task-new-button` (reliable on Windows 3.11.2; that fallback previously existed only on the project branch) and only fails closed with `setup_failed` — reporting "the trigger is still not mounted" — when both entry points fail.
35
+ - **Misleading attribution for send failures (found on hardware)**: when the window is minimised or fully occluded, Chromium throttles the page (`visibilityState=hidden`) and the send button becomes unclickable even though it sits inside the viewport — `elementFromPoint` does not hit the button itself. The old message said only "the ZCode send button was not enabled or was covered within the observation window", pointing users at the button; the driver now recognises that state and reports "the ZCode window is not in the foreground" together with the instruction to bring it forward. `Page.bringToFront` was measured to be **unable** to restore an occluded Electron window, so no automatic recovery is pretended.
36
+
37
+ ### Tests
38
+
39
+ - Full suite: **532 passed / 10 skipped** (Windows 10 x64, Node 24.18.0), a net gain of 7 cases over v0.5.2: falling back to the sidebar entry when no draft is established and still dispatching, failing closed without sending when neither entry point creates a draft, attributing a send failure to "window not in the foreground" when the page is throttled, keeping the original button attribution when the page is visible, plus 2 regression cases for the GUI-instance spawn invariant (unconditional detached + unref across platforms).
40
+
41
+ ### Docs
42
+
43
+ - The [issue #12 Windows 10 acceptance record](docs/zcode-issue-12-windows-evidence.en.md) gains a "second visit (after v0.5.2)" section: results and on-site evidence for 6 hardware runs, the reproduction criteria for the new-task page-switch failure, evidence for each send-stage failure mode, and the facts that were **not** reproduced or **not** verified this round (including that the true cause of the first `send_unknown` is still undetermined and that the new send diagnostic was never reached on hardware).
44
+
45
+ ---
46
+
47
+ ## [0.5.2] — 2026-09-14
48
+
49
+ **Project-less dispatch for ZCode (issue #12)**: `run_task`'s `projectPath` is now optional, letting ZCode run tasks in its `default` workspace; the companion `allowCreateProject` can forbid automatic project import. See the [v0.5.2 release notes](docs/release-v0.5.2.en.md) and the [Windows 10 acceptance record](docs/zcode-issue-12-windows-evidence.en.md).
50
+
51
+ ### Added
52
+
53
+ - **Project-less dispatch for ZCode (issue #12)**: `run_task`'s `projectPath` is now optional. When omitted, ZCode runs the task in its `default` workspace — no directory assigned, no project registered or imported, no Git baseline, no project snapshot freeze, no project lock, no project acceptance. On success the task is marked structurally as `not_applicable: no_project` and the terminal message states "no project acceptance performed". `query_task` / `list_tasks` display such tasks normally; `verify_task` / `get_task_report` return an explicit not-applicable explanation instead of deriving a directory from cwd.
54
+ - **`allowCreateProject` (ZCode-only, optional boolean)**: omitted keeps the existing "auto-import when the target is unregistered" behaviour; explicit `false` stops dispatch **before any import side effect** when the target is unregistered, returning a recognisable `project_not_registered` reason with remediation (no native folder dialog, no project added). Other agents passing this parameter get an explicit "not supported" error rather than a silent ignore.
55
+ - **Windows 10 hardware acceptance record** (`docs/zcode-issue-12-windows-evidence{,.en}.md`): complete evidence for project-less dispatch and `allowCreateProject=false` on ZCode 3.11.2.6792, including the before/after comparison "ZCode project entries 34 → 34, 0 added / 0 removed".
56
+
57
+ ### Fixed
58
+
59
+ - **Divergent project-trigger readiness criteria (issue #12 §5)**: waiting used `exists` (element has width/height only) while clicking went through `pick` (exactly one unclipped visible node in the winning tier), so an `exists=true` / `click=false` window existed. Waiting and clicking now share one **structured probe** that distinguishes not-mounted / mounted-but-invisible-or-clipped / ambiguous / disabled / covered / ready, plus a post-click condition: the project menu must actually open, and `menu-not-open` is classified separately.
60
+ - **Error message contradicting behaviour**: "waiting for the project trigger timed out" is no longer used for early exits (multiple matches, disabled) or a menu that never opened; failure text carries `selector`, match count and minimal hit-node attributes, and diagnostics log attempt count, elapsed time and remaining budget.
61
+ - **Centralised timeout**: new `gui.projectTriggerTimeoutMs` (default 15s) replaces the two hard-coded `15_000` literals; the whole "wait → one sidebar fallback → wait" sequence shares a single deadline, retries do not reset the budget, and it is clamped by the setup-recovery budget and the task deadline.
62
+ - **`projectPath` was never opened up in the MCP schema (found on hardware)**: the handler already had the project-less branch, but `RunTaskParamsSchema.projectPath` was still required, so a real `run_task` was rejected by the SDK with `-32602 Required at projectPath`. Unit tests call the handler directly and therefore bypass `inputSchema`, which is why a green suite missed it. Changed to `AbsPath.optional()`, plus a protocol-level regression case in `test/integration/task-flow.test.ts` asserting neither `-32602` nor `Input validation error` appears.
63
+ - **No "work outside a project" switch, and the menu click was undone by toggle semantics (found on hardware)**: ZCode's "New task" inherits the previous binding, so project-less dispatch parked at `needs_user` forever; meanwhile `clickProjectTriggerAndConfirm` kept clicking the trigger even when the project menu was **already open**, closing the Radix dropdown and then polling until its deadline, misreported as "the project menu did not open". Added the `workOutsideProject` selector and `enterDefaultWorkspace()` for an explicit switch (confirmed by `workspaceBinding` read-back, not by click success), made the click check the menu state first, and made `confirmDefaultWorkspace` return immediately for "definitely bound to a project".
21
64
 
22
65
  ---
23
66
 
package/CHANGELOG.md CHANGED
@@ -11,11 +11,54 @@
11
11
 
12
12
  ### 计划中
13
13
 
14
+ - **视觉验收第二阶段——AI 视觉内容校验**(issue #13):校验图片/页面截图**内容**是否符合任务描述(Logo 元素、风格匹配、页面语义等)。issue #3 中标注为「可选扩展」,其像素级对比第一阶段已随 v0.5.0 完成。需先确认模型与凭证来源(不得破坏「凭证零管理」红线)、判定防抖与门禁定位(建议默认仅告警)。
14
15
  - 更多外部 AI-Agent 适配(新 agent = 一个 profile +(如需)一个 adapter 文件)。
15
16
  - TraeWork 在 macOS 下的可执行探测与原生对话框驱动(当前 macOS 分支 fail-closed)。
16
17
  - 可选的项目级技能播种(默认不写入目标项目仓库)。
17
18
  - Codex 与 ZCode GUI 的 macOS 取消/返修/新建项目矩阵(当前两者 darwin 均保持 `research`)。
18
19
  - `needs_user` 状态下取消任务时经临时 CDP 连接尽力停止 GUI 内等待中的会话。
20
+ - ZCode 无项目派发在 macOS 上的真机验证(本轮仅 Windows 10 实测)。
21
+ - ZCode **未登记项目**的自动导入在 Windows 上无法完成:原生面板脚本靠 `SetForegroundWindow` 抢前台来激活地址栏,而后台 MCP server 的子进程会被 Windows 拒绝,地址栏 Edit 永不出现,脚本空转到 deadline(实测 56s 后由 `budget.check()` 归类为 setup 预算耗尽);且 PowerShell 的 stdout 在管道里被缓冲、进程被 kill 后缓冲丢失,日志里连一条 `native:` 阶段都看不到,排障方向被误导。临时对策:先在 ZCode 中手动把目标目录加入项目列表;修复方向是脚本内改用 `AttachThreadInput` 抢前台(或改走 ZCode 受支持的登记入口)。
22
+
23
+ ---
24
+
25
+ ## [0.5.3] — 2026-09-15
26
+
27
+ **ZCode 真机回访修复(issue #12 第二轮)**:修复 Windows 上 GUI 实例无法跨 server 退出驻留、新建任务不切页导致静默空等、发送失败归因误导三个真机缺陷。完整说明见 [v0.5.3 发布说明](docs/release-v0.5.3.md),真机证据见 [Windows 10 验收记录](docs/zcode-issue-12-windows-evidence.md)。
28
+
29
+ ### 修复
30
+
31
+ - **ZCode / Codex 桌面实例在 Windows 上无法跨 server 退出驻留(真机发现)**:`zcode`、`codex` 的 GUI 实例此前按平台分支 spawn(`detached: process.platform !== "win32"`),而 `traework` 用的是无条件 `detached: true`。最小实验(Windows 10 / Node 24.18.0)显示同一段 spawn:non-detached 子进程在父进程退出后存活 0,detached 存活 1。因此 Windows 上 MCP server(或一次性 smoke / probe 脚本)一退出,ZCode 就被连坐杀掉,`keptInstance` 的「实例跨 server 退出驻留」形同虚设——`needs_user` 提示「请在 ZCode 中处理后再调用 continue_task」,而窗口其实已经消失。现把该不变量收敛为单一来源 `guiInstanceSpawnOptions()`,三处 GUI 实例共用(`codex` 原先连 `unref()` 都带平台分支,一并去掉);`verify/runner`、`visual/services`、`agents/spawn` 这些**需要整组终止**的执行型子进程语义相反,继续按平台分支,不受影响。
32
+ - **新建任务点击返回成功但页面不切换,后续一路静默空等(真机发现)**:ZCode 停在已有会话时,顶部 `conversation-new-task` 是惰性挂载的图标——点击派发成功(返回 `true`)却不切换页面,而会话页的 composer **不挂载** `composer-workspace-trigger`。于是无项目模式的 default 确认、有项目模式的绑定等待都会空等到截止时间,最后只报一句 `needs_user/setup_recovery`(实测空转 30 秒)。现在新建任务后以「项目触发器已挂载」验证草稿**真的**建立;未建立则回退侧栏 `task-new-button`(Windows 3.11.2 实测可靠,此前该兜底只覆盖有项目模式),两者都失败才以 `setup_failed` fail-closed 并报出「触发器仍未挂载」。
33
+ - **发送失败归因误导(真机发现)**:窗口被最小化或完全遮挡时页面被 Chromium 节流(`visibilityState=hidden`),发送按钮「明明在视口内」却点不到,`elementFromPoint` 命中的也不是按钮本身。旧文案只报「ZCode 发送按钮未在观察期内启用或被遮挡」,会把用户引向按钮;现在会识别该状态并报出「ZCode 窗口当前不在前台」及把窗口置于前台的操作指引。`Page.bringToFront` 经真机实测**无法**恢复被遮挡的 Electron 窗口,因此不假装能自动恢复。
34
+
35
+ ### 测试
36
+
37
+ - 全量 **532 passed / 10 skipped**(Windows 10 x64,Node 24.18.0),较 v0.5.2 净增 7 项用例——草稿未建立时回退侧栏入口并完成派发、两个入口都建立不了草稿时 fail-closed 且不发送、页面被节流时发送失败归因为窗口不在前台、页面可见时保留原有的按钮归因文案,以及 GUI 实例 spawn 不变量(跨平台无条件 detached + unref)的 2 项回归。
38
+
39
+ ### 文档
40
+
41
+ - [issue #12 Windows 10 真机验收记录](docs/zcode-issue-12-windows-evidence.md) 追加「第二轮回访(v0.5.2 之后)」:6 次真机运行的结果与现场取证、`newTask` 不切页的复现判据、发送阶段各失败面的证据,以及本轮**未复现 / 未验证**的事实(含第 1 次 `send_unknown` 的真因仍未定、`sendMessage` 诊断文案在真机上未走到)。
42
+
43
+ ---
44
+
45
+ ## [0.5.2] — 2026-09-14
46
+
47
+ **ZCode 无项目派发(issue #12)**:`run_task` 的 `projectPath` 变为可选,ZCode 在 `default` 工作区承接任务;配套 `allowCreateProject` 可禁止自动导入项目。完整说明见 [v0.5.2 发布说明](docs/release-v0.5.2.md),真机证据见 [Windows 10 验收记录](docs/zcode-issue-12-windows-evidence.md)。
48
+
49
+ ### 新增
50
+
51
+ - **ZCode 无项目派发(issue #12)**:`run_task` 的 `projectPath` 改为可选。省略时 ZCode 在 `default` 工作区承接任务——不分配目录、不登记/导入项目、不采集 Git 基线、不冻结项目快照、不进入项目锁与项目验收;成功后以结构化字段标注 `not_applicable: no_project`,终态文案明示「未进行项目验收」。`query_task` / `list_tasks` 正常展示该类任务;`verify_task` / `get_task_report` 返回明确的不适用说明,不从 cwd 推导目录。
52
+ - **`allowCreateProject`(ZCode 专用,可选布尔)**:省略 = 保持既有「目标未登记即自动导入」行为;显式 `false` 时,目标目录未登记即在**任何导入副作用之前**停止派发,返回可识别的 `project_not_registered` 与处理说明(不打开原生文件夹对话框、不添加项目)。其他 agent 显式传入该参数会得到明确的「不支持」错误,而不是被静默忽略。
53
+ - **Windows 10 真机验收记录**(`docs/zcode-issue-12-windows-evidence{,.en}.md`):ZCode 3.11.2.6792 上无项目派发与 `allowCreateProject=false` 的完整证据,含「派发前后 ZCode 项目条目 34 → 34、新增 0 / 消失 0」的对比。
54
+
55
+ ### 修复
56
+
57
+ - **ZCode 项目触发器就绪判据不一致(issue #12 §五)**:等待用的是 `exists`(只看元素宽高),点击走的是 `pick`(要求该优先级层恰好一个未被裁剪的可见节点),两者判据不同,因此存在 `exists=true` 但 `click=false` 的窗口。现在等待与点击**共用同一份结构化探测**,区分未挂载 / 已挂载但不可见或被裁剪 / 不唯一 / 禁用 / 被遮挡 / 就绪;并新增点击后置检查——项目菜单必须真正打开,`menu-not-open` 单独归类。
58
+ - **错误信息与行为失实**:「等待项目触发器超时」不再被用来描述早退(多匹配、禁用)或菜单未打开;失败文案携带 `selector`、匹配数与命中节点最小属性,诊断日志记录尝试次数、实际耗时与剩余预算。
59
+ - **集中超时**:新增 `gui.projectTriggerTimeoutMs`(默认 15s)替换原先写死的两处 `15_000`;整个「等待 → 回退一次侧栏新建任务 → 再等待」共享同一截止时间,重试不重置预算,并被 setup 恢复预算与任务总时限夹住。
60
+ - **`projectPath` 未在 MCP schema 层放开(真机发现)**:handler 已支持无项目分支,但 `RunTaskParamsSchema.projectPath` 仍是必填,真实 `run_task` 会在协议层被 SDK 拒成 `-32602 Required at projectPath`;而单元测试直接调 handler、绕过了 `inputSchema`,所以全绿也没抓到。已改为 `AbsPath.optional()`,并在 `test/integration/task-flow.test.ts` 补协议层回归用例(断言文本不出现 `-32602` / `Input validation error`)。
61
+ - **缺少「不在项目中工作」切换,且项目菜单已开时点击被 toggle 反噬(真机发现)**:ZCode「新建任务」会继承上一次绑定,使无项目派发永久停在 `needs_user`;同时 `clickProjectTriggerAndConfirm` 在项目菜单**已经打开**时仍点击触发器,把 Radix 下拉关掉后一路轮询到 deadline,误报「项目菜单未打开」。现新增 `workOutsideProject` 选择器与 `enterDefaultWorkspace()` 显式切换(切换后以 `workspaceBinding` 回读为准),点击前先查菜单是否已开;`confirmDefaultWorkspace` 对「明确绑定着项目」立即返回而非空等。
19
62
 
20
63
  ---
21
64
 
package/README.en.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  # tianshu-mcp
10
10
 
11
- Visual acceptance (since v0.5.0): [English guide](docs/visual-acceptance.en.md) · [Validation record](docs/visual-validation.en.md) · [Latest release notes](<docs/release-v0.5.1.en.md>).
11
+ Visual acceptance (since v0.5.0): [English guide](docs/visual-acceptance.en.md) · [Validation record](docs/visual-validation.en.md) · [Latest release notes](<docs/release-v0.5.3.en.md>).
12
12
 
13
13
  **Tianshu × AI-Agent orchestration MCP server**
14
14
 
@@ -42,6 +42,7 @@ Tianshu plays the role of the overall commander; this MCP server is the **schedu
42
42
  - **Acceptance parallelism**: command checks run **bounded-parallel** by default (`verifyConcurrency`, default 2, range 1–4). When checks depend on an order (a later check reading build output, `--fix`, shared cache dirs), set it to `1` for fully serial behaviour; a project can override it in `.tianshu-mcp/acceptance.json`, and the server level lives in `config.json`. Report and log formats are unchanged (results are returned in declaration order).
43
43
  - **Rework loop**: automatic rework (`autoFixRounds`) + manual `rework_task`; on verification failure a repair-plan file is generated and fed back to the agent; when rounds run out → `needs_attention` awaiting Tianshu's verdict.
44
44
  - **Execution surfaces**: `driver: "gui"` selects an explicit, isolated Codex/TraeWork/ZCode CDP adapter; `driver: "spawn"` runs an external CLI child process.
45
+ - **Project-less dispatch (ZCode, issue #12)**: `run_task`'s `projectPath` may be omitted — ZCode runs the task in its `default` workspace without registering/importing a project, collecting a Git baseline, or running project acceptance (the result is marked structurally as `verificationNotApplicable: "no_project"` and `verify_task`/`get_task_report` return a not-applicable explanation). The companion `allowCreateProject: false` stops dispatch before any import side effect when the target directory is unregistered. See the [ZCode CDP adapter](docs/zcode-cdp.en.md).
45
46
  - **Scheduling discipline**: per-project serial queue + global concurrency cap (default 2, configurable).
46
47
  - **No key handling**: each agent uses its own login state; this server never stores or forwards any API key.
47
48
  - **Extensible**: a new agent = one profile (data) + (if needed) one adapter file — no changes to the orchestration core.
@@ -195,6 +196,8 @@ Use `server.log` when troubleshooting connections; do not treat stderr output it
195
196
  | [docs/codex-gui-cdp.en.md](docs/codex-gui-cdp.en.md) | Codex desktop GUI driver: MSIX COM activation, CDP attach, selectors, run detection, verify/repair |
196
197
  | [docs/codex-windows-smoke.en.md](docs/codex-windows-smoke.en.md) | Codex Windows hardware record (incl. verify-fail → auto plan → repair-pass loop) |
197
198
  | [docs/release-v0.3.4.en.md](<docs/release-v0.3.4.en.md>) | v0.3.4 release notes (ZCode project/model read-back, initialization recovery, session dispatch confirmation, issues #8/#9/#10) |
199
+ | [docs/release-v0.5.3.en.md](<docs/release-v0.5.3.en.md>) | v0.5.3 release notes (ZCode hardware-revisit fixes: instance survival across server exit, new-task page switch, send-failure attribution) |
200
+ | [docs/release-v0.5.2.en.md](<docs/release-v0.5.2.en.md>) | v0.5.2 release notes (ZCode project-less dispatch and `allowCreateProject`, issue #12) |
198
201
  | [docs/release-v0.5.1.en.md](<docs/release-v0.5.1.en.md>) | v0.5.1 release notes (skill/validation docs, archived platform evidence, lockfile version sync; no runtime changes) |
199
202
  | [docs/release-v0.5.0.en.md](<docs/release-v0.5.0.en.md>) | v0.5.0 release notes (optional visual acceptance: screenshots, image specs, baseline approval, offline report) |
200
203
  | [docs/visual-acceptance.en.md](<docs/visual-acceptance.en.md>) | Visual acceptance primer and full configuration: three page sources, baseline candidates/approval, rule freezing, thresholds and troubleshooting |
@@ -239,7 +242,7 @@ Use `server.log` when troubleshooting connections; do not treat stderr output it
239
242
  - **Engineering / CI** ✅
240
243
  - GitHub Actions: `CI` (`build-test` ubuntu/windows/macos × Node 20/22/24 + `pack-check`, plus a `visual-browser` real-browser matrix ubuntu/windows/macos-15-intel/macos-15 × Node 20/22/24, all green with the v0.5.1 tag) and `Release` (tag-triggered) both green
241
244
  - Skill self-install verified idempotent on this machine's real `~/.rivet/skills/tianshu-mcp`
242
- - npm package name `tianshu-mcp` published continuously since v0.1.1 (currently `0.5.1`)
245
+ - npm package name `tianshu-mcp` published continuously since v0.1.1 (currently `0.5.3`)
243
246
  - **Real Tianshu host integration (DoD #6)** ✅ (2026-09-07)
244
247
  - Configured the local mode in the real `D:\Tianshu` desktop host `mcp.servers` → sidecar reported `MCP: 2 servers connected, 10 tools` (including this server's 8 tools), spawned the child process and connected over stdio
245
248
  - Exposed and fixed a skill-install source-path bug (fileURLToPath, commit 55cf2d0)
@@ -385,7 +388,7 @@ Behavior and limits:
385
388
 
386
389
  | Document | Content |
387
390
  |---|---|
388
- | [CHANGELOG.en.md](<CHANGELOG.en.md>) | Version history (v0.1.0 → v0.5.1) |
391
+ | [CHANGELOG.en.md](<CHANGELOG.en.md>) | Version history (v0.1.0 → v0.5.3) |
389
392
  | [CONTRIBUTING.en.md](CONTRIBUTING.en.md) | Dev setup, conventions, commit/release flow, adding an agent |
390
393
  | [SECURITY.en.md](SECURITY.en.md) | Security model (zero credentials / command whitelist / process & desktop-automation boundaries) and private reporting |
391
394
  | [CODE_OF_CONDUCT.en.md](CODE_OF_CONDUCT.en.md) | Contributor Code of Conduct |
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  # tianshu-mcp
10
10
 
11
- 视觉验收(v0.5.0 起):[中文指南](docs/visual-acceptance.md) · [验证记录](docs/visual-validation.md) · [最新发布说明](<docs/release-v0.5.1.md>)。
11
+ 视觉验收(v0.5.0 起):[中文指南](docs/visual-acceptance.md) · [验证记录](docs/visual-validation.md) · [最新发布说明](<docs/release-v0.5.3.md>)。
12
12
 
13
13
  **天枢 × AI-Agent 编排 MCP server**
14
14
 
@@ -42,6 +42,7 @@
42
42
  - **验收并行度**:命令检查默认**有界并行**(`verifyConcurrency`,默认 2、范围 1–4)。检查项之间有顺序依赖时(后续检查读取 build 产物、带 `--fix`、共享缓存目录)请设 `1` 完全退化为串行;项目级 `.tianshu-mcp/acceptance.json` 可覆盖,server 级在 `config.json`。报告与日志格式不变(结果按声明顺序返回)。
43
43
  - **失败返修闭环**:自动返修(`autoFixRounds`)+ 手动 `rework_task`;验收失败时自动生成修复计划文件并回填给 agent;轮次用尽 → `needs_attention` 等天枢裁决。
44
44
  - **执行面**:`driver: "gui"` 由显式 adapter 驱动桌面 UI(Codex / TraeWork / ZCode 各自使用隔离的 CDP 流程);`driver: "spawn"` 走外部 CLI 子进程。
45
+ - **无项目派发(ZCode,issue #12)**:`run_task` 的 `projectPath` 可省略——ZCode 在 `default` 工作区承接任务,不登记/导入项目、不采集 Git 基线、不执行项目验收(结果以 `verificationNotApplicable: "no_project"` 结构化标注,`verify_task`/`get_task_report` 返回不适用说明)。配套 `allowCreateProject: false` 可在目标目录未登记时于任何导入副作用之前停止派发。详见 [ZCode CDP 适配器](docs/zcode-cdp.md)。
45
46
  - **调度纪律**:每项目串行队列 + 全局并发上限(默认 2,可配)。
46
47
  - **不碰密钥**:各 agent 用自己的登录态;本 server 不保存/转发任何 API key。
47
48
  - **可扩展**:新 agent = 一个 profile(数据)+(如需)一个 adapter 文件,零改编排核心。
@@ -190,6 +191,8 @@ ZCode 提问、需要登录、旧实例无 CDP、系统权限不足,或自动
190
191
  | [docs/zcode-windows-smoke.md](docs/zcode-windows-smoke.md) | ZCode Windows 真机开发、同会话返修与提问续跑验收记录 |
191
192
  | [docs/codex-gui-cdp.md](docs/codex-gui-cdp.md) | Codex 桌面端 GUI 驱动:MSIX COM 激活、CDP 接管、选择器、运行检测、验收返修 |
192
193
  | [docs/codex-windows-smoke.md](docs/codex-windows-smoke.md) | Codex Windows 真机验收记录(含验收失败→自动生成计划→返修通过闭环) |
194
+ | [docs/release-v0.5.3.md](<docs/release-v0.5.3.md>) | v0.5.3 发布说明(ZCode 真机回访修复:实例跨 server 驻留、新建任务切页、发送失败归因) |
195
+ | [docs/release-v0.5.2.md](<docs/release-v0.5.2.md>) | v0.5.2 发布说明(ZCode 无项目派发与 `allowCreateProject`,issue #12) |
193
196
  | [docs/release-v0.5.1.md](<docs/release-v0.5.1.md>) | v0.5.1 发布说明(技能/验证文档补齐、平台证据归档、锁文件版本同步;无运行时变更) |
194
197
  | [docs/release-v0.5.0.md](<docs/release-v0.5.0.md>) | v0.5.0 发布说明(可选视觉验收模块:截图对比、图片规格、基准批准、离线报告) |
195
198
  | [docs/visual-acceptance.md](<docs/visual-acceptance.md>) | 视觉验收入门与完整配置:三种页面来源、基准候选/批准、规则冻结、阈值与排查 |
@@ -238,7 +241,7 @@ ZCode 提问、需要登录、旧实例无 CDP、系统权限不足,或自动
238
241
  - **工程 / CI** ✅
239
242
  - GitHub Actions:`CI`(`build-test` ubuntu/windows/macos × Node 20/22/24 + `pack-check`,另加 `visual-browser` 真实浏览器矩阵 ubuntu/windows/macos-15-intel/macos-15 × Node 20/22/24,随 v0.5.1 tag 全绿)与 `Release`(tag 触发)均绿
240
243
  - 技能自检安装已在本机真实 `~/.rivet/skills/tianshu-mcp` 验证生效且幂等
241
- - npm 包名 `tianshu-mcp` 自 v0.1.1 起持续发布(当前 `0.5.1`)
244
+ - npm 包名 `tianshu-mcp` 自 v0.1.1 起持续发布(当前 `0.5.3`)
242
245
  - **天枢宿主真实接入(DoD #6)** ✅(2026-09-07,[host-integration-record.md](docs/host-integration-record.md))
243
246
  - 在真实 `D:\Tianshu` 桌面宿主 `mcp.servers` 配置本地模式 → sidecar `MCP: 2 servers connected, 10 tools`(含本 server 8 工具),spawn 子进程并 stdio 连通
244
247
  - 实测暴露并修复技能安装源路径 bug(fileURLToPath,提交 55cf2d0)
@@ -385,7 +388,7 @@ run_task(projectPath=/path/to/项目, agentId=codex-cli, task="任务书", autoV
385
388
  | 文档 | 内容 |
386
389
  |---|---|
387
390
  | [HANDOFF.md](HANDOFF.md) | 项目交接文档:当前状态快照、架构导览、硬性红线、已知限制、接手建议 |
388
- | [CHANGELOG.md](<CHANGELOG.md>) | 版本变更日志(v0.1.0 → v0.5.1) |
391
+ | [CHANGELOG.md](<CHANGELOG.md>) | 版本变更日志(v0.1.0 → v0.5.3) |
389
392
  | [CONTRIBUTING.md](CONTRIBUTING.md) | 开发环境、工程规范、提交与发布流程、如何新增 agent |
390
393
  | [SECURITY.md](SECURITY.md) | 安全模型(凭证零管理/命令白名单/进程与桌面自动化边界)与私密报告渠道 |
391
394
  | [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) | 贡献者行为准则 |
@@ -12,6 +12,7 @@ import { activateCodexApp, buildActivationArgs, buildSpawnArgs } from "./launche
12
12
  import { expandEnvPath } from "../../util/path.js";
13
13
  import { execFileAsync } from "../../verify/exec.js";
14
14
  import { TtlCache } from "../../util/ttl-cache.js";
15
+ import { guiInstanceSpawnOptions } from "../gui-instance.js";
15
16
  export function parseProcessRows(raw) {
16
17
  const out = [];
17
18
  for (const line of raw.split(/\r?\n/)) {
@@ -214,15 +215,11 @@ export async function ensureCodexInstance(candidate, gui, logger) {
214
215
  else {
215
216
  const argv = buildSpawnArgs(userDataDir, port);
216
217
  logger.info(`[codex] 启动受管实例:${candidate.path},CDP 端口 ${port},profile=${userDataDir}`);
217
- // POSIX 下 detached 让受管实例自成进程组组长——server/probe 退出不连坐(保留实例语义);
218
- // unref 使其不拖住父进程退出。实测:不 detached 时父进程退出会被进程组连坐杀掉主进程。
219
- const child = spawn(candidate.path, argv, {
220
- detached: process.platform !== "win32",
221
- stdio: "ignore",
222
- });
218
+ // 桌面实例必须 detached:不变量与实测依据见 guiInstanceSpawnOptions(Windows 同样适用);
219
+ // unref 使其不拖住父进程退出。
220
+ const child = spawn(candidate.path, argv, guiInstanceSpawnOptions(false));
223
221
  spawnState.child = child;
224
- if (process.platform !== "win32")
225
- child.unref();
222
+ child.unref();
226
223
  child.on("error", (e) => {
227
224
  spawnState.error = e instanceof Error ? e : new Error(String(e));
228
225
  });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * GUI 桌面实例(ZCode / Codex / TraeWork)的 spawn 选项。
3
+ *
4
+ * `detached: true` 是**不变量**,不是平台偏好:桌面实例必须自成进程组、脱离父进程,否则父进程
5
+ * (MCP server,或一次性 smoke / probe 脚本)退出时会把它连坐杀掉,`run_task` 的 `keptInstance`
6
+ * (「实例跨 server 退出驻留」)语义随之失效——表现为 `needs_user` 提示「请在 ZCode 中处理后再
7
+ * 调用 continue_task」,而窗口其实已经消失,用户根本无从操作。
8
+ *
9
+ * 真机实测(Windows 10 / Node 24.18.0,2026-09-15):同一段 spawn,non-detached 子进程在父进程
10
+ * 退出后存活 0,detached 存活 1;对照组是 detached 启动的 ZCode 实例跨多次 server 退出仍驻留。
11
+ *
12
+ * 注意:不要把它套到「执行型子进程」上:`verify/runner`、`visual/services`、`agents/spawn` 的语义是
13
+ * **可被整组 SIGTERM/SIGKILL 终止**,因此它们继续按平台分支(POSIX detached、Windows 用
14
+ * taskkill /T),与这里的驻留语义相反。
15
+ */
16
+ export function guiInstanceSpawnOptions(windowsHide = true) {
17
+ return { detached: true, stdio: "ignore", windowsHide };
18
+ }
@@ -14,6 +14,7 @@ import path from "node:path";
14
14
  import { spawn } from "node:child_process";
15
15
  import { TraeworkCdpClient } from "./cdp/client.js";
16
16
  import { execFileAsync } from "../../verify/exec.js";
17
+ import { guiInstanceSpawnOptions } from "../gui-instance.js";
17
18
  /** 端口是否空闲(可绑定) */
18
19
  export function isPortFree(port, host = "127.0.0.1") {
19
20
  return new Promise((resolve) => {
@@ -101,11 +102,8 @@ export function launchInstance(opts) {
101
102
  const args = gui.exeArgs.map((a) => a.replace(/<port>/g, String(port)));
102
103
  // 原生 Windows 路径:确保 exePath 用反斜杠形式传给 GUI 进程
103
104
  const nativeExe = process.platform === "win32" ? path.win32.normalize(exePath) : exePath;
104
- const child = spawn(nativeExe, args, {
105
- detached: true,
106
- stdio: "ignore",
107
- windowsHide: false, // 窗口必须可见:发送依赖模拟输入
108
- });
105
+ // 窗口必须可见:发送依赖模拟输入
106
+ const child = spawn(nativeExe, args, guiInstanceSpawnOptions(false));
109
107
  child.unref();
110
108
  const pid = child.pid ?? -1;
111
109
  const commandLine = [nativeExe, ...args].join(" ");
@@ -2,7 +2,7 @@ import { TraeworkCdpClient, CdpDisconnectedError, CdpUnavailableError, } from ".
2
2
  import { candidateExpr } from "./selectors.js";
3
3
  import { projectDisplayName } from "./project.js";
4
4
  import { normalizeZcodeModelSelection } from "./model.js";
5
- import { projectTriggerDom, workspaceBindingExpression, modelSelectionExpression, sendButtonPointExpression, } from "./dom.js";
5
+ import { projectTriggerDom, projectTriggerProbeExpression, projectMenuOpenExpression, workOutsideProjectExpression, workspaceBindingExpression, modelSelectionExpression, sendButtonPointExpression, } from "./dom.js";
6
6
  export { CdpDisconnectedError, CdpUnavailableError };
7
7
  export async function retryZcodeEvaluation(operation, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
8
8
  for (let attempt = 0;; attempt++) {
@@ -18,6 +18,14 @@ export async function retryZcodeEvaluation(operation, sleep = (ms) => new Promis
18
18
  }
19
19
  }
20
20
  }
21
+ /** 点击后确认菜单打开的轮询间隔(不是独立超时;窗口由调用方从集中配置与总预算算出)。 */
22
+ const PROJECT_MENU_POLL_MS = 100;
23
+ /**
24
+ * 触发器点击被吞后的重试间隔。真机实测:ZCode 窗口被其他窗口完全遮挡时,Chromium 判定
25
+ * occluded 并节流页面,合成鼠标事件常被吞掉(首次点击无效、第二次才打开菜单),
26
+ * 所以「点一次然后干等」会必然失败。重试有界,不重置调用方给的 deadline。
27
+ */
28
+ const TRIGGER_RECLICK_MS = 1_500;
21
29
  export class ZcodeCdpClient {
22
30
  selectors;
23
31
  inner;
@@ -86,10 +94,11 @@ export class ZcodeCdpClient {
86
94
  }
87
95
  async click(key) {
88
96
  if (key === "projectTrigger") {
89
- const point = await this.evaluate(`(function(){${projectTriggerDom(this.selectors)}if(!trigger)return null;const r=trigger.getBoundingClientRect();return {x:r.left+r.width/2,y:r.top+r.height/2}})()`);
90
- if (!point)
97
+ // 与等待共用同一就绪判据:多匹配、不可见、禁用、被遮挡一律不算就绪,不发鼠标事件。
98
+ const probe = await this.probeProjectTrigger();
99
+ if (!probe.ready || !probe.point)
91
100
  return false;
92
- await this.clickAt(point.x, point.y);
101
+ await this.clickAt(probe.point.x, probe.point.y);
93
102
  return true;
94
103
  }
95
104
  const point = await this.evaluate(`(function(){for(const s of ${candidateExpr(key, this.selectors)}){for(const e of document.querySelectorAll(s)){const r=e.getBoundingClientRect();if(r.width&&r.height)return {x:r.left+r.width/2,y:r.top+r.height/2}}}return null})()`);
@@ -98,6 +107,55 @@ export class ZcodeCdpClient {
98
107
  await this.clickAt(point.x, point.y);
99
108
  return true;
100
109
  }
110
+ /** 项目触发器结构化探测(只读,不发事件)。 */
111
+ async probeProjectTrigger() {
112
+ return this.evaluate(projectTriggerProbeExpression(this.selectors));
113
+ }
114
+ /** 项目菜单是否已真正可见(点击后置条件)。 */
115
+ async projectMenuOpen() {
116
+ return this.evaluate(projectMenuOpenExpression());
117
+ }
118
+ /**
119
+ * 探测 → 点击 → 确认项目菜单打开。事件发出不算成功:菜单没开就返回 menu-not-open。
120
+ * deadline 由调用方从集中配置与 setup/task 总预算取上限(重试不重置截止时间)。
121
+ */
122
+ async clickProjectTriggerAndConfirm(deadline) {
123
+ const probe = await this.probeProjectTrigger();
124
+ // 菜单可能**已经开着**(上一次尝试或外部操作的残留)。Radix 下拉是 toggle:此时再点
125
+ // 触发器会把它关掉,随后整段等待都会落到 menu-not-open(3.11.2 真机实测踩过)。
126
+ // 所以先查后点。
127
+ if (await this.projectMenuOpen())
128
+ return { opened: true, probe };
129
+ if (!probe.ready || !probe.point)
130
+ return { opened: false, reason: "not-ready", probe };
131
+ let lastClick = 0;
132
+ while (Date.now() < deadline) {
133
+ // 有界重试点击:窗口被遮挡时节流会让首次点击落空(真机实测第二次才生效)。
134
+ // 每轮都先确认菜单未开,因此不会把已经打开的菜单 toggle 掉。
135
+ if (Date.now() - lastClick >= TRIGGER_RECLICK_MS) {
136
+ // eslint-disable-next-line no-await-in-loop
137
+ await this.clickAt(probe.point.x, probe.point.y);
138
+ lastClick = Date.now();
139
+ }
140
+ // eslint-disable-next-line no-await-in-loop
141
+ if (await this.projectMenuOpen())
142
+ return { opened: true, probe };
143
+ // eslint-disable-next-line no-await-in-loop
144
+ await new Promise((resolve) => setTimeout(resolve, PROJECT_MENU_POLL_MS));
145
+ }
146
+ return { opened: false, reason: "menu-not-open", probe };
147
+ }
148
+ /**
149
+ * 点击「不在项目中工作」(要求该层唯一可见):进入 ZCode 的 default(无项目)工作区。
150
+ * 新建任务会继承上一次绑定,所以无项目派发必须先显式执行这一步。
151
+ */
152
+ async clickWorkOutsideProject() {
153
+ const found = await this.evaluate(workOutsideProjectExpression(this.selectors));
154
+ if (!found.point)
155
+ return { clicked: false, count: found.count };
156
+ await this.clickAt(found.point.x, found.point.y);
157
+ return { clicked: true, count: 1 };
158
+ }
101
159
  async clickExact(key, value) {
102
160
  const found = await this.evaluate(`(async function(){const norm=s=>(s||'').normalize('NFKC').trim().toLocaleLowerCase();const target=norm(${JSON.stringify(value)});const sels=${candidateExpr(key, this.selectors)};const visible=e=>{const r=e.getBoundingClientRect();if(!(r.width>0&&r.height>0&&r.bottom>0&&r.right>0&&r.top<innerHeight&&r.left<innerWidth))return false;const x=Math.max(0,Math.min(innerWidth-1,r.left+r.width/2)),y=Math.max(0,Math.min(innerHeight-1,r.top+r.height/2)),hit=document.elementFromPoint(x,y);return !!hit&&(hit===e||e.contains(hit))};const items=()=>{const out=[];for(const s of sels)for(const e of document.querySelectorAll(s))if(visible(e)&&!out.includes(e))out.push(e);return out};const leafTexts=e=>[...e.querySelectorAll('*')].filter(n=>n.children.length===0).map(n=>(n.textContent||'').trim()).filter(Boolean);const label=e=>(e.getAttribute('data-value')||e.getAttribute('data-model')||e.getAttribute('data-provider')||leafTexts(e)[0]||e.textContent||'').trim();let nodes=items();let scroller=nodes[0];while(scroller&&scroller!==document.body&&scroller.scrollHeight<=scroller.clientHeight)scroller=scroller.parentElement;const start=scroller?.scrollTop||0;if(scroller)scroller.scrollTop=0;const seen=new Map(),seenTestids=new Map(),matches=new Map();for(let i=0;i<60;i++){await new Promise(r=>setTimeout(r,25));nodes=items();for(const e of nodes){const text=label(e);const testid=e.getAttribute('data-testid')||'';const id=e.getAttribute('data-id')||e.getAttribute('data-model-id')||e.getAttribute('data-value')||e.getAttribute('data-model')||e.getAttribute('data-provider')||testid||text;const key=norm(id)+'|'+norm(text);if(testid)seenTestids.set(key,testid);if(!text)continue;seen.set(key,text);if(norm(text)===target)matches.set(key,{top:scroller?.scrollTop||0,text})}if(!scroller||scroller.scrollTop+scroller.clientHeight>=scroller.scrollHeight-1)break;const before=scroller.scrollTop;scroller.scrollTop=Math.min(scroller.scrollTop+Math.max(100,scroller.clientHeight*.8),scroller.scrollHeight);if(scroller.scrollTop===before)break}const result={available:[...seen.values()],testids:[...seenTestids.values()]};const matchesFound=[...matches.values()];if(matchesFound.length===1){if(scroller)scroller.scrollTop=matchesFound[0].top;await new Promise(r=>setTimeout(r,50));const exact=items().filter(e=>norm(label(e))===target);if(exact.length===1){exact[0].scrollIntoView({block:'center'});const r=exact[0].getBoundingClientRect();return {count:1,...result,point:{x:r.left+r.width/2,y:r.top+r.height/2}}}}if(scroller)scroller.scrollTop=start;return {count:matchesFound.length,...result}})()`);
103
161
  if (!found.point)
@@ -173,7 +231,21 @@ export class ZcodeCdpClient {
173
231
  }
174
232
  await this.evaluate("new Promise(resolve=>setTimeout(resolve,200))");
175
233
  }
176
- throw new Error("ZCode 发送按钮未在观察期内启用或被遮挡;未发送任务");
234
+ // 窗口被遮挡/最小化时 Chromium 会节流页面:合成鼠标事件与 elementFromPoint 都不可靠,
235
+ // 按钮「明明在视口内」却点不到(3.11.2-Windows 真机实测 visibilityState=hidden、
236
+ // elementFromPoint 命中非按钮节点)。此时只报按钮会把用户引向错误方向;
237
+ // Page.bringToFront 实测无法恢复被遮挡的 Electron 窗口,所以如实报出真因并给出
238
+ // 可操作指引,不假装能自动恢复。
239
+ let throttled = false;
240
+ try {
241
+ throttled = await this.evaluate("document.visibilityState === 'hidden' || document.hidden === true");
242
+ }
243
+ catch {
244
+ throttled = false;
245
+ }
246
+ throw new Error(throttled
247
+ ? "ZCode 发送按钮在观察期内不可点击:ZCode 窗口当前不在前台(页面被节流,合成点击不可靠)——请把 ZCode 窗口置于前台后重试;未发送任务"
248
+ : "ZCode 发送按钮未在观察期内启用或被遮挡;未发送任务");
177
249
  }
178
250
  async answerQuestion(answer) {
179
251
  const located = await this.evaluate(`(function(){const norm=s=>(s||'').normalize('NFKC').trim().toLocaleLowerCase();const target=norm(${JSON.stringify(answer)});const visible=e=>{const r=e.getBoundingClientRect(),s=getComputedStyle(e);return r.width>0&&r.height>0&&r.bottom>0&&r.right>0&&r.top<innerHeight&&r.left<innerWidth&&s.display!=='none'&&s.visibility!=='hidden'};const boxes=[...document.querySelectorAll('[role="listbox"][aria-label]:has([role="option"])')].filter(visible);if(boxes.length!==1)return {count:boxes.length,available:[],buttonStates:[]};const box=boxes[0];const options=[...box.querySelectorAll('[role="option"]')].filter(visible);const label=e=>{const leaves=[...e.querySelectorAll('*')].filter(n=>n.children.length===0).map(n=>(n.textContent||'').trim()).filter(Boolean);return leaves.find(x=>norm(x)===target)||e.getAttribute('aria-label')||e.getAttribute('data-value')||''};const available=options.map(e=>{const leaves=[...e.querySelectorAll('*')].filter(n=>n.children.length===0).map(n=>(n.textContent||'').trim()).filter(Boolean);return leaves.find(x=>x&&!/^\\d+[.\uff0e]$/.test(x))||(e.textContent||'').trim()}).filter(Boolean);const matches=options.filter(e=>norm(label(e))===target);let scope=box,buttons=[];while(scope.parentElement){scope=scope.parentElement;buttons=[...scope.querySelectorAll('button:not([role="option"])')].filter(visible);if(buttons.length)break}if(matches.length!==1)return {count:matches.length,available,buttonStates:buttons.map(e=>e.disabled||e.getAttribute('aria-disabled')==='true')};matches[0].scrollIntoView({block:'center'});const r=matches[0].getBoundingClientRect();return {count:1,available,point:{x:r.left+r.width/2,y:r.top+r.height/2},buttonStates:buttons.map(e=>e.disabled||e.getAttribute('aria-disabled')==='true')}})()`);
@@ -74,3 +74,63 @@ export function sendButtonPointExpression(overrides) {
74
74
  return hit && (hit===e || e.contains(hit)) ? {x,y} : null;
75
75
  })()`;
76
76
  }
77
+ /**
78
+ * 项目触发器结构化探测:等待与点击共用这一份就绪判据,消除「exists 说有、click 说没有」
79
+ * (exists 只看宽高,pick 还要求该优先级层唯一且未被祖先裁剪)。
80
+ *
81
+ * 返回 selector / 该层匹配数 count / 全部候选命中的挂载数 mounted / 命中节点最小诊断属性,
82
+ * 让上层区分未挂载、不可见、不唯一、禁用、遮挡与真实超时,而不是统统归成「等待超时」。
83
+ * 只用于项目触发器;登录页等其它 exists 调用语义不变。
84
+ */
85
+ export function projectTriggerProbeExpression(overrides) {
86
+ return `(function(){${ZCODE_DOM}
87
+ const describe = e => e ? String(e.tagName || '')
88
+ + (e.getAttribute('data-testid') ? '#' + e.getAttribute('data-testid') : '')
89
+ + (e.getAttribute('aria-label') ? '[' + e.getAttribute('aria-label') + ']' : '') : '';
90
+ const sels = ${candidateExpr("projectTrigger", overrides)};
91
+ const mountedNodes = new Set();
92
+ for (const s of sels) for (const e of document.querySelectorAll(s)) mountedNodes.add(e);
93
+ const mounted = mountedNodes.size;
94
+ const m = pick(sels);
95
+ // 窗口被其他窗口完全遮挡时 Chromium 判定 occluded 并节流页面,合成点击常被吞掉——
96
+ // 这个环境事实必须随探测结果一起回传,否则失败信息会把用户引向错误方向。
97
+ const pageHidden = document.visibilityState === 'hidden';
98
+ const base = { selector: m.selector, count: m.count, mounted, ready: false, pageHidden };
99
+ if (!mounted) return Object.assign({}, base, { state: 'missing' });
100
+ if (m.count === 0)
101
+ return Object.assign({}, base, { state: 'hidden', detail: describe(mountedNodes.values().next().value) });
102
+ if (m.count > 1) return Object.assign({}, base, { state: 'ambiguous' });
103
+ const node = m.node;
104
+ if (node.disabled || node.getAttribute('aria-disabled') === 'true')
105
+ return Object.assign({}, base, { state: 'disabled', detail: describe(node) });
106
+ const r = node.getBoundingClientRect(), x = r.left + r.width / 2, y = r.top + r.height / 2;
107
+ const hit = document.elementFromPoint(x, y);
108
+ // 命中目标本身或其子树才算命中目标;被别的元素盖住不是可点击状态。
109
+ if (!hit || !(hit === node || node.contains(hit)))
110
+ return Object.assign({}, base, { state: 'covered', detail: describe(hit) });
111
+ return Object.assign({}, base, { state: 'ready', ready: true, point: { x, y }, detail: describe(node) });
112
+ })()`;
113
+ }
114
+ /**
115
+ * 点击项目触发器后的后置条件:项目菜单必须真正可见。
116
+ * 鼠标事件发出去本身不算成功——菜单没开就说明这次点击没有生效。
117
+ */
118
+ export function projectMenuOpenExpression() {
119
+ return `(function(){${ZCODE_DOM}
120
+ if ([...document.querySelectorAll('[role="menu"]')].some(visible)) return true;
121
+ return [...document.querySelectorAll('[role="menuitemcheckbox"]')].some(visible);
122
+ })()`;
123
+ }
124
+ /**
125
+ * 「不在项目中工作」菜单项(`composer-work-outside-project`):ZCode 用它进入 default
126
+ * (无项目)工作区。要求命中层唯一可见,否则不点击——多匹配说明菜单结构已漂移,
127
+ * 猜一个点会把任务送到错误的工作区。
128
+ */
129
+ export function workOutsideProjectExpression(overrides) {
130
+ return `(function(){${ZCODE_DOM}
131
+ const found = pick(${candidateExpr("workOutsideProject", overrides)});
132
+ if (!found.node) return { count: found.count };
133
+ const r = found.node.getBoundingClientRect();
134
+ return { count: found.count, point: { x: r.left + r.width / 2, y: r.top + r.height / 2 } };
135
+ })()`;
136
+ }
@@ -6,6 +6,7 @@ import path from "node:path";
6
6
  import { TraeworkCdpClient } from "../traework/cdp/client.js";
7
7
  import { execFileAsync } from "../../verify/exec.js";
8
8
  import { TtlCache } from "../../util/ttl-cache.js";
9
+ import { guiInstanceSpawnOptions } from "../gui-instance.js";
9
10
  export function parseProcessRows(raw) {
10
11
  const out = [];
11
12
  for (const line of raw.split(/\r?\n/)) {
@@ -159,13 +160,9 @@ export async function ensureZcodeInstance(exePath, gui, logger, options = {}) {
159
160
  throw new Error(`ZCode CDP 端口 ${port} 已被占用`);
160
161
  const args = gui.exeArgs.map((arg) => arg.replaceAll("<port>", String(port)));
161
162
  options.signal?.throwIfAborted();
162
- // POSIX(macOS)必须 detached:实测父进程退出时非 detached 子进程会被进程组连坐杀掉
163
- //(codex-gui 同款问题,2026-09-13 真机结论);detached 后自成进程组组长,实例跨 server 退出驻留。
164
- const child = spawn(exePath, args, {
165
- detached: process.platform !== "win32",
166
- stdio: "ignore",
167
- windowsHide: false,
168
- });
163
+ // 桌面实例必须 detached:不变量与实测依据见 guiInstanceSpawnOptions。Windows 上此前按平台
164
+ // 分支给 false,父进程退出会连坐,实例无法跨 server 退出驻留(2026-09-15 真机实测修正)。
165
+ const child = spawn(exePath, args, guiInstanceSpawnOptions(false));
169
166
  let launchError;
170
167
  child.once("error", (error) => {
171
168
  launchError = error;
@@ -4,18 +4,31 @@ function inside(root, candidate) {
4
4
  const rel = path.relative(root, candidate);
5
5
  return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
6
6
  }
7
- export function validateTaskReferences(task, context, projectRoot) {
7
+ /** 从任务书/上下文中抽取「像本地路径」的候选值(URL、模型名、命令文本等已被排除)。 */
8
+ export function collectLocalReferences(task, context) {
8
9
  const text = `${task}\n${context ?? ""}`;
9
10
  const values = new Set();
10
11
  for (const m of text.matchAll(/`([^`]+)`/g))
11
12
  values.add(m[1].trim());
12
13
  for (const m of text.matchAll(/(?:^|\s)((?:[A-Za-z]:[\\/]|\/|\.\.?[\\/])[^\s,。;;]+)/gm))
13
14
  values.add(m[1].trim());
15
+ return [...values].filter((source) => path.isAbsolute(source) || /^\.\.?[\\/]/.test(source) || /[\\/]/.test(source));
16
+ }
17
+ /**
18
+ * 校验任务书里的本地引用。
19
+ *
20
+ * `projectRoot` 缺失(无项目模式,issue #12)时**不得**退回 cwd / 主目录 / 日志目录去解析:
21
+ * 识别到明确引用就直接说明需要 projectPath,没有引用则返回空表。
22
+ */
23
+ export function validateTaskReferences(task, context, projectRoot) {
24
+ const candidates = collectLocalReferences(task, context);
25
+ if (!projectRoot) {
26
+ if (candidates.length)
27
+ throw new Error(`任务书包含本地文件引用(${candidates.slice(0, 5).join("、")}${candidates.length > 5 ? ` 等 ${candidates.length} 项` : ""});无项目模式无法解析路径,请提供 projectPath`);
28
+ return [];
29
+ }
14
30
  const out = [];
15
- for (const source of values) {
16
- // Backticks frequently contain identifiers/commands. Only path-like values are interpreted.
17
- if (!path.isAbsolute(source) && !/^\.\.?[\\/]/.test(source) && !/[\\/]/.test(source))
18
- continue;
31
+ for (const source of candidates) {
19
32
  const absolutePath = path.resolve(projectRoot, source);
20
33
  if (!inside(path.resolve(projectRoot), absolutePath))
21
34
  throw new Error(`引用路径越出项目范围:${source}`);