From 96fefcf41853b5ad83f1b56410a3c008972fd2b1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 02:32:33 +0000 Subject: [PATCH] fix(mcp): drop doctor --fix from soft solidify steers Unfinished soft-gap doctor fixes and boot/status command lists no longer present evolver doctor --fix as the current step. The steer stays swarm_solidify. The command remains when no run is unfinished. Co-authored-by: Steper Lin --- AGENTS.md | 3 + CHANGELOG.md | 1 + DEBUG.md | 8 +++ README.md | 2 +- README.zh.md | 2 +- examples/swarm-quickstart/README.md | 2 +- src/evolver/mcp_server.py | 12 ++-- src/evolver/swarm.py | 71 ++++++++++++++++++- .../test_mcp_host_usability_pending_smoke.py | 41 ++++++++++- 9 files changed, 130 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f1584c4..9529966 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -405,6 +405,9 @@ boot 自动 ack 本 agent 的 `swarm.hello`。安装或拉代码后须重启 MCP Parent。软缺口(`soft_continue=true`)下 `swarm_doctor` 同样给出 `next_action=swarm_solidify`、`tick_again=false` 与 preserve 三元组,不写 `gate_soft_continue`、不写 `session_not_failed`;`soft_continue` 仍为 true。 +这条引导上 `fixes` 不把 `evolver doctor --fix` 列为当前步骤;boot/status 的 +`commands` 与 `next_steps` 同样不把它当成现在要执行的修复。没有未完成 run, +或固化之后,列出这条命令仍然合适。本工具仍不执行它。 硬阻断的 doctor 仍是 `ok=false`,不另给这个动作,也不写 `tick_again`。 此时 `boot_once` 为空。忽略 `next_action` 再调 `swarm_tick` 时默认不启新周期。 第一次 `ok=true`、`severity=soft`、`cycle_started=false`、`progress=false`、 diff --git a/CHANGELOG.md b/CHANGELOG.md index f3e48c6..4097054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Fixed +- **MCP 宿主可用性:软缺口固化引导不再把 `evolver doctor --fix` 当当前步骤**:未完成 run 且 soft-continue 时,`swarm_doctor` 的 `fixes` 去掉这条命令。boot / status 的 `commands` 同样去掉它,`next_steps` 里点名它的句子改成先 `swarm_solidify`(`next_action` / preserve / `action_required`)。`soft_continue` 仍为 true,`gate_soft_continue` 在 boot/status 仍为 true,doctor 仍不写这个键,也不写 `session_not_failed`。没有未完成 run 时,`fixes` / `commands` 仍列出 `evolver doctor --fix`。固化之后同样可以再列。本工具仍不执行 `--fix`。第一次 tick 与保险丝的 `commands` 不动。不结算这条 run,不写 Parent baseline,不放宽固化门。 - **MCP 宿主可用性:未完成 run 的软缺口 boot/status 写 `tick_again=false`**:`swarm_boot` 与 `swarm_status` 在未完成 run 且软缺口、`next_action=swarm_solidify` 时带 `tick_again=false`。`gate_soft_continue` 仍为 true,不写 `session_not_failed`。preserve 三元组不动。没有未完成 run 的 soft-continue,以及压缩检查点盖过固化时,不写 `tick_again`。doctor、第一次 tick 与保险丝本来的 `tick_again=false` 不动。不结算这条 run,不写 Parent baseline,不放宽固化门。 - **MCP 宿主可用性:未完成 run 的软缺口 doctor 写 `tick_again=false`**:只在 `soft_continue=true` 且有未完成 run、没有硬阻断时,`swarm_doctor` 带 `tick_again=false`。缺席不再被读成「可以再 tick」。`soft_continue` 仍为 true,不写 `gate_soft_continue`,不写 `session_not_failed`。`next_action` 仍是 `swarm_solidify`,preserve 三元组不动。hint 写明 `fixes` 里的 `evolver doctor --fix` 本工具不执行。没有未完成 run,或 doctor 是硬阻断时,不写 `tick_again`。第一次 tick 与保险丝本来就有 `tick_again=false`,这次不改。不结算这条 run,不写 Parent baseline,不放宽固化门,不执行 `--fix`。 - **MCP 宿主可用性:未完成 run 的软缺口 doctor 指向固化**:`readonly_smoke` 点名的 `swarm_doctor` 在 `soft_continue=true` 且有未完成 run 时带 `next_action=swarm_solidify`、`pending_solidify` 与 preserve 三元组。`hint` 先写现在固化,软缺口仍是建议。不写 `gate_soft_continue`,不写 `session_not_failed`,不结算这条 run,不写 Parent baseline,不执行 `--fix`。没有未完成 run,或 doctor 本身是硬阻断(`ok=false`)时,不另给这个动作。 diff --git a/DEBUG.md b/DEBUG.md index f3b52c8..2f54a99 100644 --- a/DEBUG.md +++ b/DEBUG.md @@ -1248,3 +1248,11 @@ - **修复**:仅当未完成 run、软缺口已经挂上(`gate_soft_continue` 仍为 true)、`next_action=swarm_solidify` 时,boot 顶层与 status 写 `tick_again=false`。不写 `session_not_failed`。preserve 三元组不动。没有未完成 run 的 soft-continue 不写 `tick_again`。压缩检查点盖过固化时,boot 顶层与嵌套 status 副本都不留这个键。doctor、第一次 tick 与保险丝的 `tick_again=false` 不动。不结算,不写 Parent baseline,不放宽固化门,不执行 `evolver doctor --fix`。目录缓存(DEBUG #61)未做假修复。 - **经验**:**同一个「不要再 tick」要写在宿主已经认识的字段上,包括只读开机回执。** boot / status 不能借 `gate_soft_continue=false` 来停。 - **仍在**:只看 `gate_soft_continue=true`、完全不看 `tick_again` / `next_action` 的宿主,boot 或 status 之后仍可能再 tick。只读 `soft_continue=true`、完全不看 `tick_again` 的 doctor 冒烟(DEBUG #71 余项)仍可能再 tick。把保险丝 `ok=false` 当成整段失败、且不读 `session_not_failed` 与 `loop_fuse` 的宿主(DEBUG #68 余项 2)仍会停掉会话;`ok` 留着 false,是只看 `ok` 的宿主靠它停止空转。`fixes` 仍列出 `evolver doctor --fix`;本工具不执行它。目录缓存(DEBUG #61)没有做假修复。 + +### 73. 软缺口固化引导把 `evolver doctor --fix` 列成当前步骤(MCP 易用性,已修) + +- **症状**:#72 之后,未完成 run 的软缺口 `swarm_doctor` / `swarm_boot` / `swarm_status` 已经指向 `swarm_solidify`,并带 preserve 三元组与 `tick_again=false`。doctor 的 hint 写明固化前不要跑 `evolver doctor --fix`,本工具不执行它。`fixes` 仍把这条命令列在最前。boot / status 的 `commands` 与 `next_steps` 同样把它写成建议现在执行。只读清单、不读 hint 的宿主会去跑 `doctor --fix`,而不是 `swarm_solidify`。 +- **根因**:`fixes` / `commands` 来自开箱目录,软缺口一律附上 `evolver doctor --fix`。固化引导只加了 `next_action`,没有从这份清单里拿掉当前不该跑的那条。 +- **修复**:仅当未完成 run 且 soft-continue、没有硬 `blockers` 时,doctor 的 `fixes` 去掉精确的 `evolver doctor --fix`。boot / status 在 `next_action=swarm_solidify` 时,`commands` 去掉同一条,`next_steps` 里点名它的句子改成先 `swarm_solidify`。`soft_continue` 仍为 true。boot / status 的 `gate_soft_continue` 仍为 true。不写 `session_not_failed`。没有未完成 run 时清单不变。硬阻断不改这份清单,也不另给 `next_action`。第一次 tick 与保险丝的 `commands` 不动。不结算,不写 Parent baseline,不放宽固化门,不执行 `--fix`。目录缓存(DEBUG #61)未做假修复。 +- **经验**:**当前步骤只能有一份清单。** `next_action` 指向固化时,`fixes` 不能再把另一条命令放在可执行清单的前面。 +- **仍在**:只读 `soft_continue=true` 或 `gate_soft_continue=true`、完全不看 `tick_again` / `next_action` / `fixes` 的宿主,冒烟或开机之后仍可能再 tick。没有把这些旗标写成 false(会重开 DEBUG #67)。`fixes` / `commands` 里仍可能有 `evolver bench freeze` 与 `evolver bench baseline`;它们不是这次要拿掉的那条,也不是 MCP 工具会执行的命令。第一次 tick 与保险丝的 `commands` 仍可能列出 `evolver doctor --fix`。把保险丝 `ok=false` 当成整段失败、且不读 `session_not_failed` 与 `loop_fuse` 的宿主(DEBUG #68 余项 2)仍会停掉会话。目录缓存(DEBUG #61)没有做假修复。 diff --git a/README.md b/README.md index a364bb0..2eb12f2 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ Live `tools/list` already publishes `swarm_tick` parameters `prompt_mode` (`full If a previous run is unfinished, `swarm_boot` keeps `next_action=swarm_solidify`, sets top-level `pending_solidify`, and adds `readonly_smoke` (`tools`: `swarm_doctor` and `swarm_status`, `settles_run`: false). On a soft gap it also sets `tick_again=false` while `gate_soft_continue` stays true. `hint` says to solidify first. A soft gap (`no_baseline` / `pack_absent` / `unmeasured`) still attaches `warning` and `next_steps`, and it does not replace that hint or weaken the gate. A read-only smoke can call `swarm_doctor` or `swarm_status`; neither settles the run nor invents a Parent baseline. A `swarm_tick` in that state does not start a cycle. The first call stays soft (`ok` true, `severity` soft, `cycle_started` false, `progress` false, `redirect_count` 1, `retryable` false, `next_action` still `swarm_solidify`, `last_run` unchanged). An omitted `retryable` is not a license to tick again. The same call again on that run returns `ok` false, `loop_fuse=pending_redirect_loop`, `redirect_reason=pending_redirect_loop`, `severity` soft, `retryable` false, and no `error` field. It still does not start a cycle. `loop_fuse` and that `redirect_reason` are the tick-loop fuse. The code is not an `error` and not a `next_action`. `next_action` stays `swarm_solidify`. `dispatch_reason` stays `pending_solidify`. The first redirect keeps `redirect_reason=pending_solidify` and does not set `loop_fuse`. Connect-time instructions name `loop_fuse=pending_redirect_loop` the same way. The fuse is not a gate failure and not an abandon signal; a soft gap stays on `warning` and `next_steps`. Both returns set `preserve_pending` true, `abandon_run` false, and `action_required` to `swarm_solidify` (boot and status do too while solidify is still next). Do not drop the unfinished run. When a soft gap is present, `gate_soft_continue` is omitted on the first redirect and on the fuse (`tick_again` is false). Omitted is not false: the first redirect keeps `ok` true and sets no `error`, so a host that treats only a strict false as session death stays in the session and does not tick again. Both tick returns set `session_not_failed` true. Read that field before other flags: true means the session is not failed, and `loop_fuse` does not abandon the run when the preserve flags say to call `swarm_solidify`. `next_steps` starts with `swarm_solidify`. Boot and status keep `gate_soft_continue` true while a run is still unfinished, set `tick_again` false on that soft gap, and they do not set `session_not_failed`. Pass `supersede_pending=true` to replace that run. Pause and the compaction checkpoint still refuse the cycle. -A cold `swarm_status` (this process has not called `swarm_boot`) returns `next_action=swarm_boot` and a short hint. After boot, status keeps `agent_name`, `source_revision`, and any soft `gap` / warnings. `tool_asset_search`, `tool_mailbox_poll`, `tool_cycle_timeline`, and `swarm_approvals` always return `{ok, items, hint}` — an empty match is not a blank body. `swarm_approvals` also keeps `pending` (the same rows as `items`) and `recent`. Read-only `swarm_doctor` mirrors `evolver doctor` without `--fix` (`ok`, soft warnings, command hints in `fixes`). `no_baseline` / `pack_absent` / `unmeasured` stay soft-continue. When a run is unfinished and that report is soft-continue, doctor also sets `next_action=swarm_solidify`, `tick_again=false`, `pending_solidify`, and the preserve triad (`preserve_pending`, `abandon_run=false`, `action_required=swarm_solidify`). `soft_continue` stays true. It still does not settle the run, write a Parent baseline, or execute `evolver doctor --fix`. `gate_soft_continue` and `session_not_failed` stay absent. A hard blocker keeps `ok` false and does not add that action or `tick_again`. The first unfinished-run tick and the fuse already set `tick_again` false. +A cold `swarm_status` (this process has not called `swarm_boot`) returns `next_action=swarm_boot` and a short hint. After boot, status keeps `agent_name`, `source_revision`, and any soft `gap` / warnings. `tool_asset_search`, `tool_mailbox_poll`, `tool_cycle_timeline`, and `swarm_approvals` always return `{ok, items, hint}` — an empty match is not a blank body. `swarm_approvals` also keeps `pending` (the same rows as `items`) and `recent`. Read-only `swarm_doctor` mirrors `evolver doctor` without `--fix` (`ok`, soft warnings, command hints in `fixes`). `no_baseline` / `pack_absent` / `unmeasured` stay soft-continue. When a run is unfinished and that report is soft-continue, doctor also sets `next_action=swarm_solidify`, `tick_again=false`, `pending_solidify`, and the preserve triad (`preserve_pending`, `abandon_run=false`, `action_required=swarm_solidify`). `soft_continue` stays true. It still does not settle the run, write a Parent baseline, or execute `evolver doctor --fix`. On that steer, `fixes` does not list `evolver doctor --fix` as a current step. Boot and status `commands` and `next_steps` do not present it as the current action either. The command stays listed when no run is unfinished, and after solidify. `gate_soft_continue` and `session_not_failed` stay absent. A hard blocker keeps `ok` false and does not add that action or `tick_again`. The first unfinished-run tick and the fuse already set `tick_again` false. `swarm_boot(project=...)` binds a directory only when that path exists. `bound_paths.repo_root` is the code root (the git root walked from that directory, or the directory itself). `bound_paths.workspace_root` is the mutation directory. When they differ, or when `project` is a free-text label rather than a directory, the same object carries `warning` and `next_steps`. That is a soft warning: `ok` stays true and the loop still continues. A label is not turned into a new workspace. diff --git a/README.zh.md b/README.zh.md index 870addb..26fc532 100644 --- a/README.zh.md +++ b/README.zh.md @@ -242,7 +242,7 @@ uv run evolver migrate-state 若上一轮 run 还没固化,`swarm_boot` 的 `next_action` 仍是 `swarm_solidify`,顶层带 `pending_solidify`,并多一个 `readonly_smoke`(`tools` 为 `swarm_doctor` 与 `swarm_status`,`settles_run` 为 false)。软缺口上同时写 `tick_again=false`,`gate_soft_continue` 仍为 true。`hint` 写明先固化。软缺口(`no_baseline` / `pack_absent` / `unmeasured`)仍附 `warning` 与 `next_steps`,但不盖掉这句 hint,也不放宽固化门。只读冒烟可以调 `swarm_doctor` 或 `swarm_status`:它们不结算这条 run,也不伪造 Parent baseline。 -本进程尚未 `swarm_boot` 时,`swarm_status` 返回 `next_action=swarm_boot` 和一句 hint。boot 之后 status 仍带 `agent_name`、`source_revision`,以及软缺口的 `gap` / warnings。`tool_asset_search`、`tool_mailbox_poll`、`tool_cycle_timeline`、`swarm_approvals` 一律返回 `{ok, items, hint}`,没有命中也不是空正文。`swarm_approvals` 另外保留 `pending`(与 `items` 同一队列)和 `recent`。只读 `swarm_doctor` 等同 `evolver doctor` 且不执行 `--fix`(`ok`、软警告、`fixes` 里的命令提示)。`no_baseline` / `pack_absent` / `unmeasured` 仍是 soft-continue。有未完成 run 且这份报告是 soft-continue 时,doctor 也给出 `next_action=swarm_solidify`、`tick_again=false`、`pending_solidify` 和 preserve 三元组(`preserve_pending`、`abandon_run=false`、`action_required=swarm_solidify`)。`soft_continue` 仍为 true。它仍不结算这条 run,不写 Parent baseline,也不执行 `evolver doctor --fix`。`gate_soft_continue` 与 `session_not_failed` 缺席。硬阻断保持 `ok=false`,不另给这个动作,也不写 `tick_again`。第一次未完成 run 的 tick 与保险丝本来就写了 `tick_again=false`。 +本进程尚未 `swarm_boot` 时,`swarm_status` 返回 `next_action=swarm_boot` 和一句 hint。boot 之后 status 仍带 `agent_name`、`source_revision`,以及软缺口的 `gap` / warnings。`tool_asset_search`、`tool_mailbox_poll`、`tool_cycle_timeline`、`swarm_approvals` 一律返回 `{ok, items, hint}`,没有命中也不是空正文。`swarm_approvals` 另外保留 `pending`(与 `items` 同一队列)和 `recent`。只读 `swarm_doctor` 等同 `evolver doctor` 且不执行 `--fix`(`ok`、软警告、`fixes` 里的命令提示)。`no_baseline` / `pack_absent` / `unmeasured` 仍是 soft-continue。有未完成 run 且这份报告是 soft-continue 时,doctor 也给出 `next_action=swarm_solidify`、`tick_again=false`、`pending_solidify` 和 preserve 三元组(`preserve_pending`、`abandon_run=false`、`action_required=swarm_solidify`)。`soft_continue` 仍为 true。它仍不结算这条 run,不写 Parent baseline,也不执行 `evolver doctor --fix`。这条引导上 `fixes` 不把 `evolver doctor --fix` 列为当前步骤。boot / status 的 `commands` 与 `next_steps` 同样不把它当成现在要执行的修复。没有未完成 run,或固化之后,列出这条命令仍然合适。`gate_soft_continue` 与 `session_not_failed` 缺席。硬阻断保持 `ok=false`,不另给这个动作,也不写 `tick_again`。第一次未完成 run 的 tick 与保险丝本来就写了 `tick_again=false`。 `swarm_boot(project=...)` 只在路径是已存在目录时绑定。`bound_paths.repo_root` 是代码根(从该目录向上找到的 git 根,否则就是该目录本身)。`bound_paths.workspace_root` 是变异所用目录。两者不一致,或 `project` 只是一段自由文本而不是目录时,同一对象带 `warning` 和 `next_steps`。这是软警告:`ok` 仍为 true,循环继续。自由文本不会被当成新的工作区。 diff --git a/examples/swarm-quickstart/README.md b/examples/swarm-quickstart/README.md index 2ca9a60..f66a006 100644 --- a/examples/swarm-quickstart/README.md +++ b/examples/swarm-quickstart/README.md @@ -50,7 +50,7 @@ swarm_boot ─▶ swarm_status ─▶ swarm_hook_event(信号采集) | `claude-code.mcp.json` | Claude Code | 项目根 `.mcp.json` | | `cursor-mcp.json` | Cursor | `.cursor/mcp.json` | -连上之后,本条消息已有别的任务就做那件事。没有别的任务时宿主调用 `swarm_boot`,再用一句话说明实际状态。若只调了 `swarm_status` 且本进程还没 boot,`next_action` 是 `swarm_boot`。空的 `tool_asset_search` / `tool_mailbox_poll` / `tool_cycle_timeline` / `swarm_approvals` 返回 `{ok, items, hint}`(`swarm_approvals` 另含 `pending` 与 `recent`)。`swarm_tick` 的 `prompt_mode` 默认 `full`;短载荷须显式 `prompt_mode=short` 或 `checklist_only=true`,此时 `engine_log` 不再内联 GEP 全文。只读 `swarm_doctor` 等同 `evolver doctor`(不执行 `--fix`)。有未完成 run 且 doctor 为 soft-continue 时,它的 `next_action` 也是 `swarm_solidify`,并写 `tick_again=false`(`soft_continue` 仍为 true;不写 `gate_soft_continue` / `session_not_failed`,不结算,不写 Parent,不执行 `evolver doctor --fix`)。宿主工具目录若仍看不到 `prompt_mode` / `checklist_only` 或 `swarm_doctor`,那是客户端对 `tools/list` 的缓存:重启 MCP 进程后新开一次会话才会重读。有未完成 run 时 `swarm_boot` 的 `next_action` 仍是 `swarm_solidify`,软缺口同时写 `tick_again=false`(`gate_soft_continue` 仍为 true),`hint` 写明先固化,并带 `readonly_smoke`(`swarm_doctor` / `swarm_status` 不结算、不放宽门);软缺口留在 `warning`。此时直接 `swarm_tick` 不启新周期(第一次 `cycle_started=false`、`ok=true`、`progress=false`,不改 `last_run`;同一条 run 再 tick 得到 `ok=false`、`loop_fuse=pending_redirect_loop`、`redirect_reason=pending_redirect_loop`,没有 `error` 字段,`next_action` 仍是 `swarm_solidify`(保险丝码不是 `next_action`;`dispatch_reason` 仍是 `pending_solidify`;第一次的 `redirect_reason` 仍是 `pending_solidify`),仍不启周期,不是门禁失败,也不是放弃;两次都带 `preserve_pending=true`、`abandon_run=false`,不要丢掉这条 run。有软缺口时第一次 tick 与保险丝都不带 `gate_soft_continue`(缺席不是 false;`ok` 第一次仍为 true,没有 `error`;`tick_again=false` 与 `retryable=false` 都表示不要再 tick,第一次也写 `retryable=false`;两次都带 `session_not_failed=true`,先读它,会话没有失败,`loop_fuse` 不放弃这条 run;`next_steps` 第一条是 `swarm_solidify`)。boot / status 的 `gate_soft_continue` 仍为 true,未完成软缺口同时写 `tick_again=false`,且不带 `session_not_failed`)。要替换才传 `supersede_pending=true`。`boot_once` 只登记会话,不在开机时同步技能。冻结包未装或没有基线时 boot / `swarm_tick` **soft-continue**(带 warnings + next_steps,可进入循环);Accept/publish 可 soft-accept(不伪造 Parent baseline、不打开 val 密封材料)。已暂停时仍返回 `await_supervisor_resume`。用户说「停」,宿主调用 `swarm_supervise` action=pause;说「继续」,宿主转达 resume。也可以对它说「启动蜂群进化」。返回的 instrument 按步执行 `swarm_tick → 执行 → swarm_distill → swarm_hypothesis → swarm_solidify → swarm_feedback`,直到终止条件。`swarm_hypothesis` 可带 `expected_outcome` 写预计通过的检查,结算时引擎对照实测写 `prediction_delta`;上一轮输出被截断时按问题调用 `episode_inspect` 读回原文;收到压缩信号或连续 4 次固化失败时先调 `swarm_checkpoint_working` 写下 `WORKING_SESSION.md`。`dispatch_prompt` 标 **PROPOSAL REQUIRED** 时改走 `swarm_propose`,不得自由编辑。`next_action=await_human_approval` 停住:`swarm_approvals` 读给人,人决定后才 `swarm_approval_resolve`。 +连上之后,本条消息已有别的任务就做那件事。没有别的任务时宿主调用 `swarm_boot`,再用一句话说明实际状态。若只调了 `swarm_status` 且本进程还没 boot,`next_action` 是 `swarm_boot`。空的 `tool_asset_search` / `tool_mailbox_poll` / `tool_cycle_timeline` / `swarm_approvals` 返回 `{ok, items, hint}`(`swarm_approvals` 另含 `pending` 与 `recent`)。`swarm_tick` 的 `prompt_mode` 默认 `full`;短载荷须显式 `prompt_mode=short` 或 `checklist_only=true`,此时 `engine_log` 不再内联 GEP 全文。只读 `swarm_doctor` 等同 `evolver doctor`(不执行 `--fix`)。有未完成 run 且 doctor 为 soft-continue 时,它的 `next_action` 也是 `swarm_solidify`,并写 `tick_again=false`(`soft_continue` 仍为 true;不写 `gate_soft_continue` / `session_not_failed`,不结算,不写 Parent,不执行 `evolver doctor --fix`。这条路径的 `fixes` 与 boot/status 的 `commands`、`next_steps` 不把 `evolver doctor --fix` 当成当前步骤;没有未完成 run,或固化之后,列出它仍然合适)。宿主工具目录若仍看不到 `prompt_mode` / `checklist_only` 或 `swarm_doctor`,那是客户端对 `tools/list` 的缓存:重启 MCP 进程后新开一次会话才会重读。有未完成 run 时 `swarm_boot` 的 `next_action` 仍是 `swarm_solidify`,软缺口同时写 `tick_again=false`(`gate_soft_continue` 仍为 true),`hint` 写明先固化,并带 `readonly_smoke`(`swarm_doctor` / `swarm_status` 不结算、不放宽门);软缺口留在 `warning`。此时直接 `swarm_tick` 不启新周期(第一次 `cycle_started=false`、`ok=true`、`progress=false`,不改 `last_run`;同一条 run 再 tick 得到 `ok=false`、`loop_fuse=pending_redirect_loop`、`redirect_reason=pending_redirect_loop`,没有 `error` 字段,`next_action` 仍是 `swarm_solidify`(保险丝码不是 `next_action`;`dispatch_reason` 仍是 `pending_solidify`;第一次的 `redirect_reason` 仍是 `pending_solidify`),仍不启周期,不是门禁失败,也不是放弃;两次都带 `preserve_pending=true`、`abandon_run=false`,不要丢掉这条 run。有软缺口时第一次 tick 与保险丝都不带 `gate_soft_continue`(缺席不是 false;`ok` 第一次仍为 true,没有 `error`;`tick_again=false` 与 `retryable=false` 都表示不要再 tick,第一次也写 `retryable=false`;两次都带 `session_not_failed=true`,先读它,会话没有失败,`loop_fuse` 不放弃这条 run;`next_steps` 第一条是 `swarm_solidify`)。boot / status 的 `gate_soft_continue` 仍为 true,未完成软缺口同时写 `tick_again=false`,且不带 `session_not_failed`)。要替换才传 `supersede_pending=true`。`boot_once` 只登记会话,不在开机时同步技能。冻结包未装或没有基线时 boot / `swarm_tick` **soft-continue**(带 warnings + next_steps,可进入循环);Accept/publish 可 soft-accept(不伪造 Parent baseline、不打开 val 密封材料)。已暂停时仍返回 `await_supervisor_resume`。用户说「停」,宿主调用 `swarm_supervise` action=pause;说「继续」,宿主转达 resume。也可以对它说「启动蜂群进化」。返回的 instrument 按步执行 `swarm_tick → 执行 → swarm_distill → swarm_hypothesis → swarm_solidify → swarm_feedback`,直到终止条件。`swarm_hypothesis` 可带 `expected_outcome` 写预计通过的检查,结算时引擎对照实测写 `prediction_delta`;上一轮输出被截断时按问题调用 `episode_inspect` 读回原文;收到压缩信号或连续 4 次固化失败时先调 `swarm_checkpoint_working` 写下 `WORKING_SESSION.md`。`dispatch_prompt` 标 **PROPOSAL REQUIRED** 时改走 `swarm_propose`,不得自由编辑。`next_action=await_human_approval` 停住:`swarm_approvals` 读给人,人决定后才 `swarm_approval_resolve`。 ## 开箱 / Open box(首次准备的一条命令) diff --git a/src/evolver/mcp_server.py b/src/evolver/mcp_server.py index bad214c..438c9ad 100644 --- a/src/evolver/mcp_server.py +++ b/src/evolver/mcp_server.py @@ -422,8 +422,9 @@ def tool_swarm_boot( (code root) and workspace_root. A difference, or a label that is not a directory, is a soft warning; the loop still continues. An unfinished run on a soft gap keeps next_action=swarm_solidify, - gate_soft_continue=true, and tick_again=false. This call does not - settle the run or write a Parent baseline. + gate_soft_continue=true, and tick_again=false. This steer does not + list evolver doctor --fix as a current step in commands or next_steps. + This call does not settle the run or write a Parent baseline. """ from evolver.swarm import swarm_boot @@ -649,8 +650,9 @@ def tool_swarm_status() -> dict[str, Any]: """Read-only. Lightweight engine status for swarm agents (no cycle side effects). After boot, an unfinished run on a soft gap keeps next_action=swarm_solidify, - gate_soft_continue=true, and tick_again=false. This call does not settle - the run or write a Parent baseline. + gate_soft_continue=true, and tick_again=false. This steer does not + list evolver doctor --fix as a current step in commands or next_steps. + This call does not settle the run or write a Parent baseline. """ from evolver.swarm import swarm_status @@ -661,6 +663,8 @@ def tool_swarm_doctor() -> dict[str, Any]: While a run is unfinished and the report is soft-continue, next_action is swarm_solidify and tick_again=false. soft_continue stays true. + fixes does not list evolver doctor --fix as a current step. + That command stays listed when no run is unfinished, and after solidify. This call does not settle the run, write a Parent baseline, or execute evolver doctor --fix. gate_soft_continue and session_not_failed stay absent. """ diff --git a/src/evolver/swarm.py b/src/evolver/swarm.py index 7bcd8c4..bc550b3 100644 --- a/src/evolver/swarm.py +++ b/src/evolver/swarm.py @@ -393,6 +393,7 @@ def build_instrument_prompt(state: dict[str, Any]) -> str: - 要看状态、不跑周期:`swarm_status`。资源 `evolver://status` 是同一份。 本进程还没 `swarm_boot` 时,status 的 `next_action` 是 `swarm_boot`。 - 要看开箱就绪、不改状态:`swarm_doctor`(只读,等同 `evolver doctor` 摘要,不执行 --fix)。 + 有未完成 run 且为 soft-continue 时,`fixes` 不把 `evolver doctor --fix` 当当前步骤。 - 要看最近周期:`tool_cycle_timeline`,或资源 `evolver://events/recent`。 投影陈旧时再 `tool_rebuild_views`。 - 多节点:`tool_mailbox_poll` / `tool_mailbox_send`。 @@ -405,6 +406,9 @@ def build_instrument_prompt(state: dict[str, Any]) -> str: - `bench_pack.reason=pack_absent`:**可继续**。建议人按 `examples/swarm-quickstart/README.md`「开箱 / 首次准备」执行 `evolver setup` / `evolver doctor --fix` / `evolver bench freeze`。 + 有未完成 run 时,这条命令不是当前步骤:先 `swarm_solidify`。 + 此时 `swarm_doctor` 的 `fixes` 与 boot/status 的 `commands` 不列出 + `evolver doctor --fix`。没有未完成 run,或固化之后,列出它仍然合适。 soft-accept 可发布但会警告;装包仍强烈推荐。 - `reason=no_baseline`:**可继续**。建议在另一上下文写好 val 沙箱后执行 `evolver bench baseline`(或 doctor --fix 在沙箱齐时测量)。基线不由候选 @@ -681,6 +685,7 @@ def _attach_host_session(status: dict[str, Any]) -> dict[str, Any]: if gap is None: if steer_solidify: _prefer_solidify_steps(status, hold_tick=False, run_id=pending_run) + _defer_doctor_fix_on_steer(status) return status from evolver.ops.open_box import apply_soft_gap @@ -694,6 +699,7 @@ def _attach_host_session(status: dict[str, Any]) -> dict[str, Any]: # stops only on strict false does not tick again after this status. if surfaced.get("gate_soft_continue") is True: surfaced["tick_again"] = False + _defer_doctor_fix_on_steer(surfaced) return surfaced @@ -801,6 +807,55 @@ def swarm_status() -> dict[str, Any]: return _surface_bound_paths(_attach_host_session(body), bound) +_DOCTOR_FIX_COMMAND: Final = "evolver doctor --fix" +_DOCTOR_FIX_NOT_CURRENT: Final = ( + "Current action is swarm_solidify " + "(next_action, preserve_pending, action_required). " + "Do not run evolver doctor --fix before solidify. " + "That command is appropriate after this run is solidified, " + "or when no run is unfinished." +) + + +def _commands_without_current_doctor_fix(items: list[Any] | None) -> list[str]: + """Drop the exact ``evolver doctor --fix`` command. Other commands stay.""" + kept: list[str] = [] + for item in items or []: + text = str(item).strip() + if not text or text == _DOCTOR_FIX_COMMAND: + continue + if text not in kept: + kept.append(text) + return kept + + +def _steps_without_current_doctor_fix(items: list[Any] | None) -> list[str]: + """Replace a step that tells the host to run ``evolver doctor --fix`` now.""" + kept: list[str] = [] + for item in items or []: + text = str(item).strip() + if not text: + continue + if text != _DOCTOR_FIX_NOT_CURRENT and _DOCTOR_FIX_COMMAND in text: + text = _DOCTOR_FIX_NOT_CURRENT + if text not in kept: + kept.append(text) + return kept + + +def _defer_doctor_fix_on_steer(payload: dict[str, Any]) -> None: + """Stop offering ``evolver doctor --fix`` as the current solidify-steer step. + + ``commands`` loses the exact command. A ``next_steps`` entry that names it + is replaced with a solidify steer. Tick and fuse envelopes do not call + this. Doctor ``fixes`` use :func:`_commands_without_current_doctor_fix`. + """ + if "commands" in payload: + payload["commands"] = _commands_without_current_doctor_fix(payload.get("commands")) + if "next_steps" in payload: + payload["next_steps"] = _steps_without_current_doctor_fix(payload.get("next_steps")) + + def _attach_unfinished_run_to_doctor(report: dict[str, Any]) -> dict[str, Any]: """Steer a soft doctor smoke at solidify while a run is unfinished. @@ -816,7 +871,9 @@ def _attach_unfinished_run_to_doctor(report: dict[str, Any]) -> dict[str, Any]: only on strict false would tick again. ``soft_continue`` stays true: that bit means the gap is not a hard stop. Unfinished soft-gap boot and status set the same ``tick_again`` false and keep ``gate_soft_continue`` true. - This does not run ``--fix`` and does not write a Parent baseline. + ``fixes`` does not list ``evolver doctor --fix`` as a current step. That + command stays listed when no run is unfinished. This does not run + ``--fix`` and does not write a Parent baseline. """ if not _pending_solidify_state(): return report @@ -837,9 +894,14 @@ def _attach_unfinished_run_to_doctor(report: dict[str, Any]) -> dict[str, Any]: "This doctor call does not settle the run, does not write a Parent " "baseline, and does not relax the solidify gate. " "Soft gaps stay advisory. Do not apply fixes before swarm_solidify. " - "fixes may list `evolver doctor --fix`; this tool does not execute it. " + "fixes does not list `evolver doctor --fix` as a current step. " + "Do not run evolver doctor --fix before solidify. " + "This tool does not execute it. " + "That command is appropriate after this run is solidified, " + "or when no run is unfinished. " ) out["hint"] = lead + str(out.get("hint") or "") + out["fixes"] = _commands_without_current_doctor_fix(out.get("fixes")) return out @@ -851,7 +913,9 @@ def swarm_doctor() -> dict[str, Any]: hints for the host to show a person; this function never runs them. A soft-continue report with an unfinished run also sets ``next_action=swarm_solidify`` and ``tick_again=false``. ``soft_continue`` - stays true. A hard blocker does not gain either field. + stays true. On that steer, ``fixes`` does not list ``evolver doctor --fix`` + as a current step. The command stays listed when no run is unfinished. + A hard blocker does not gain either field. """ from evolver.ops.open_box import SOFT_GAP_ERRORS, doctor @@ -1637,6 +1701,7 @@ def swarm_boot( # boot envelope is a separate dict. Keep gate_soft_continue true. if payload.get("gate_soft_continue") is True: payload["tick_again"] = False + _defer_doctor_fix_on_steer(payload) bound = describe_bound_paths(bind) if isinstance(payload.get("state"), dict): payload["state"]["bound_paths"] = bound diff --git a/tests/test_mcp_host_usability_pending_smoke.py b/tests/test_mcp_host_usability_pending_smoke.py index 448c31b..95c6b70 100644 --- a/tests/test_mcp_host_usability_pending_smoke.py +++ b/tests/test_mcp_host_usability_pending_smoke.py @@ -232,6 +232,26 @@ def _host_next_call(payload: dict[str, Any]) -> str: return "stop" +def _checklist_runs_doctor_fix_now(payload: dict[str, Any]) -> bool: + """True when a list offers ``evolver doctor --fix`` as the current step. + + Exact entries in ``fixes`` or ``commands`` count. A next step that names + the command counts unless it says not to run it before solidify. + """ + for key in ("fixes", "commands"): + for item in payload.get(key) or []: + if str(item).strip() == "evolver doctor --fix": + return True + for step in payload.get("next_steps") or []: + text = str(step) + if "evolver doctor --fix" not in text: + continue + if "do not run evolver doctor --fix before solidify" in text.lower(): + continue + return True + return False + + def _steps_invite_another_tick(payload: dict[str, Any]) -> bool: """True when a next step tells the host to call ``swarm_tick``.""" for step in payload.get("next_steps") or []: @@ -367,6 +387,7 @@ def test_prompt_mode_and_doctor_are_listed_with_descriptions(self) -> None: assert "Parent baseline" in description assert "session_not_failed" in description assert "tick_again=false" in description + assert "does not list evolver doctor --fix as a current step" in description boot_desc = " ".join((tools["swarm_boot"].description or "").split()) status_desc = " ".join((tools["swarm_status"].description or "").split()) @@ -374,6 +395,8 @@ def test_prompt_mode_and_doctor_are_listed_with_descriptions(self) -> None: assert "gate_soft_continue" in boot_desc assert "tick_again=false" in status_desc assert "gate_soft_continue" in status_desc + for text in (boot_desc, status_desc): + assert "does not list evolver doctor --fix as a current step" in text class TestPendingBootSmokeHint: @@ -441,6 +464,10 @@ def test_pending_boot_keeps_solidify_and_names_a_read_only_smoke( _assert_solidify_step_first(status) assert _host_next_call(boot) == "swarm_solidify" assert _host_next_call(status) == "swarm_solidify" + # Command lists are the checklist. doctor --fix is not the current step. + assert _checklist_runs_doctor_fix_now(boot) is False + assert _checklist_runs_doctor_fix_now(status) is False + assert _checklist_runs_doctor_fix_now(state) is False def test_soft_boot_without_a_pending_run_does_not_grow_a_smoke_block( self, isolated_swarm_env: Path @@ -460,6 +487,8 @@ def test_soft_boot_without_a_pending_run_does_not_grow_a_smoke_block( assert _steps_invite_another_tick(boot) is True hint = str(boot.get("hint") or "") assert "recommended" in hint or "Soft-accept" in hint or "soft-continue" in hint + # No unfinished run: doctor --fix stays a listed setup command. + assert _checklist_runs_doctor_fix_now(boot) is True def test_checkpoint_outranks_the_smoke_block(self, isolated_swarm_env: Path) -> None: from evolver.gep.compact_gate import request_checkpoint @@ -506,6 +535,7 @@ def test_pending_doctor_on_a_soft_gap_names_solidify( assert "session_not_failed" not in idle assert "gate_soft_continue" not in idle assert "tick_again" not in idle + assert _checklist_runs_doctor_fix_now(idle) is True _write_pending("run_doctor_soft") report = swarm_doctor() @@ -536,12 +566,14 @@ def test_pending_doctor_on_a_soft_gap_names_solidify( assert "tick_again=false" in hint assert "Parent baseline" in hint assert "Do not apply fixes before swarm_solidify" in hint - assert "evolver doctor --fix" in hint + assert "does not list `evolver doctor --fix` as a current step" in hint + assert "do not run evolver doctor --fix before solidify" in hint.lower() assert "does not execute" in hint assert "not installed" in hint.lower() or "do not block the loop" in hint.lower() assert not baseline_path().exists() assert not frozen_pack_path().exists() - assert any("evolver doctor --fix" in item for item in report["fixes"]) + assert _checklist_runs_doctor_fix_now(report) is False + assert report["action_required"] == "swarm_solidify" def test_pending_doctor_hard_blocker_does_not_invent_solidify( self, isolated_swarm_env: Path @@ -574,6 +606,7 @@ def test_instrument_names_the_read_only_alternative_only_while_pending(self) -> {"agent_name": "a", "workspace_root": "/ws", "pending_solidify": True} ) assert "先调用 `swarm_solidify`" in pending + assert "不把 `evolver doctor --fix` 当当前步骤" in pending assert "只读冒烟" in pending assert "swarm_doctor" in pending assert "cycle_started=false" in pending @@ -652,6 +685,10 @@ async def boom(is_loop: bool = False) -> dict[str, Any]: assert "not installed" not in hint.lower() assert result.get("gap") in _SOFT_GAPS assert any("cycle_started=false" in str(item) for item in result.get("warnings") or []) + # Tick and fuse envelopes stay as they were: this command may still + # be listed there. Doctor, boot, and status are the lists this change + # stops treating as the current step. + assert _checklist_runs_doctor_fix_now(result) is True assert not (isolated_swarm_env / "evolution" / "swarm_state.json").exists() async def test_ready_gate_redirect_stays_soft(