From 77a45c9106f2d5be47cc4916f6559888666b8f23 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 05:31:51 +0000 Subject: [PATCH] fix(mcp): drop doctor --fix from unfinished tick and fuse envelopes The first unfinished-run tick and the pending_redirect_loop fuse still listed evolver doctor --fix as a current step. Strip that exact command the same way boot, status, and doctor already do, and leave the soft-gap flags unchanged. Co-authored-by: Steper Lin --- AGENTS.md | 7 +-- CHANGELOG.md | 1 + DEBUG.md | 9 ++++ README.md | 2 +- README.zh.md | 2 +- examples/swarm-quickstart/README.md | 2 +- src/evolver/mcp_server.py | 10 +++-- src/evolver/swarm.py | 20 +++++++-- .../test_mcp_host_usability_pending_smoke.py | 44 +++++++++++++++++-- tests/test_swarm.py | 4 ++ 10 files changed, 84 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9529966..fb80cc5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -405,9 +405,10 @@ 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, -或固化之后,列出这条命令仍然合适。本工具仍不执行它。 +这条引导上 `fixes` 不把 `evolver doctor --fix` 列为当前步骤;boot/status,以及 +第一次未完成 run 的 tick 与保险丝,其 `commands` 与 `next_steps` 同样不把它当成 +现在要执行的修复。没有未完成 run,或固化之后,列出这条命令仍然合适。本工具仍不执行它。 +tick / 保险丝上的 `gate_soft_continue` 仍缺席,不写成 false。 硬阻断的 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 4097054..3f09514 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 宿主可用性:未完成 run 的第一次 tick 与保险丝也不把 `evolver doctor --fix` 当当前步骤**:软缺口固化引导上,第一次 `swarm_tick` 与 `loop_fuse=pending_redirect_loop` 的 `commands` 去掉精确的 `evolver doctor --fix`,`next_steps` 里点名它的句子改成先 `swarm_solidify`(`next_action` / preserve / `action_required`)。`gate_soft_continue` 在这两份信封上仍缺席,不写成 false。`tick_again=false` 与保险丝的 `ok=false` 不动。没有未完成 run 的 tick、boot、doctor 仍列出这条命令。本工具仍不执行 `--fix`。不结算这条 run,不写 Parent baseline,不放宽固化门。 - **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`。 diff --git a/DEBUG.md b/DEBUG.md index 2f54a99..5bae27e 100644 --- a/DEBUG.md +++ b/DEBUG.md @@ -62,6 +62,7 @@ | 57 | 失败现场 diff 看不见未跟踪文件:MCP 验证脚本新建 notes.md 走拒绝后 episode diff 为空,成功侧同 helper 同盲区(未修,待单独一轮) | solidify/episode | 评估会话 | 未发版 | | 58 | 外部 MCP 评审 P1 四项全属实并修:solidify/propose 未取实例锁、journal 裸句柄、instructions 漏新工具、测试名实不符 + 版本硬编码 | swarm/mcp/tests | 评估会话 | 未发版 | | 69 | 未完成 run 的第一次 tick 重定向不写 `retryable`,缺席被当成再 tick | mcp/swarm | MCP 易用性 | 未发版 | +| 74 | 未完成 run 的第一次 tick 与保险丝仍把 `evolver doctor --fix` 列成当前步骤 | mcp/swarm | MCP 易用性 | 未发版 | ## 条目 @@ -1256,3 +1257,11 @@ - **修复**:仅当未完成 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)没有做假修复。 + +### 74. 第一次 tick 与保险丝把 `evolver doctor --fix` 列成当前步骤(MCP 易用性,已修) + +- **症状**:#73 之后,未完成 run 的软缺口 `swarm_doctor` / `swarm_boot` / `swarm_status` 已经从当前步骤拿掉精确的 `evolver doctor --fix`。第一次未完成 run 的 `swarm_tick`,以及同一条 run 再 tick 的 `loop_fuse=pending_redirect_loop`,`commands` 仍列出这条命令,`next_steps` 仍把它写成建议现在执行。只读清单的宿主会去跑 `doctor --fix`,而不是 `swarm_solidify`。 +- **根因**:tick 重定向经 `apply_soft_gap` 挂上开箱目录。#73 的剥离只接在 doctor、boot、status,没有接到 `_pending_tick_redirect`。 +- **修复**:第一次未完成 run 的 tick 与保险丝在 `next_action=swarm_solidify` 时,`commands` 去掉精确的 `evolver doctor --fix`,`next_steps` 里点名它的句子改成先 `swarm_solidify`。`gate_soft_continue` 仍缺席,不写成 false。`tick_again=false` 不动。保险丝的 `ok` 仍为 false。没有未完成 run 的 tick 仍列出这条命令。不结算,不写 Parent baseline,不放宽固化门,不执行 `--fix`。目录缓存(DEBUG #61)未做假修复。 +- **经验**:**当前步骤的清单要盖住宿主下一步会读的每一份信封。** 开机回执拿掉一条命令之后,重定向信封若仍列出它,宿主会从 tick 的返回里把它捡回来。 +- **仍在**:只看 `soft_continue=true` 或把缺席的 `gate_soft_continue` 当成继续、完全不看 `tick_again` / `next_action` / `commands` 的宿主,仍可能再 tick。没有把这些旗标写成 false(会重开 DEBUG #67)。`commands` 里仍可能有 `evolver bench freeze` 与 `evolver bench baseline`。把保险丝 `ok=false` 当成整段失败、且不读 `session_not_failed` 与 `loop_fuse` 的宿主(DEBUG #68 余项 2)仍会停掉会话;`ok` 留着 false。目录缓存(DEBUG #61)没有做假修复。 diff --git a/README.md b/README.md index 2eb12f2..2bbd0f6 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`. 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. +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 first unfinished-run tick and the `loop_fuse=pending_redirect_loop` envelope do the same in `commands` and `next_steps`. `gate_soft_continue` stays omitted on those tick envelopes. 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 26fc532..4b2b097 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`。这条引导上 `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` 时,`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 的 tick 与保险丝的 `commands` / `next_steps`,同样不把它当成现在要执行的修复。这两份 tick 信封上 `gate_soft_continue` 仍缺席,不写成 false。没有未完成 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 f66a006..d6d8641 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`。这条路径的 `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`。 +连上之后,本条消息已有别的任务就做那件事。没有别的任务时宿主调用 `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`,以及第一次未完成 run 的 tick 与保险丝的 `commands` / `next_steps`,不把 `evolver doctor --fix` 当成当前步骤(tick 上 `gate_soft_continue` 仍缺席,不写成 false);没有未完成 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 438c9ad..6c89dae 100644 --- a/src/evolver/mcp_server.py +++ b/src/evolver/mcp_server.py @@ -510,9 +510,13 @@ async def tool_swarm_tick( run, call swarm_solidify. A soft gap stays in warning. gate_soft_continue is omitted on the first soft-gap redirect and on the fuse: omitted is not false, so do not tick and do not treat the session - as failed. The first redirect keeps ok true and sets no error. Pass - supersede_pending=true to replace that run. This does not weaken - the solidify gate and does not write a Parent baseline. + as failed. The first redirect keeps ok true and sets no error. + This steer does not list evolver doctor --fix as the current step + in commands or next_steps. The command stays listed when no run is + unfinished. + This call does not execute it. Pass supersede_pending=true to replace + that run. This does not weaken the solidify gate and does not write + a Parent baseline. """ from evolver.swarm import swarm_tick diff --git a/src/evolver/swarm.py b/src/evolver/swarm.py index bc550b3..67ded82 100644 --- a/src/evolver/swarm.py +++ b/src/evolver/swarm.py @@ -407,8 +407,9 @@ def build_instrument_prompt(state: dict[str, Any]) -> str: `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,或固化之后,列出它仍然合适。 + 此时 `swarm_doctor` 的 `fixes`、boot/status 的 `commands`,以及第一次 + tick 与保险丝的 `commands` 和 `next_steps`,都不把 `evolver doctor --fix` + 当当前步骤。没有未完成 run,或固化之后,列出它仍然合适。 soft-accept 可发布但会警告;装包仍强烈推荐。 - `reason=no_baseline`:**可继续**。建议在另一上下文写好 val 沙箱后执行 `evolver bench baseline`(或 doctor --fix 在沙箱齐时测量)。基线不由候选 @@ -847,8 +848,10 @@ 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`. + is replaced with a solidify steer. Boot, status, the first unfinished-run + tick, and the loop fuse call this while ``next_action`` is + ``swarm_solidify``. A tick with no unfinished run does not. Doctor + ``fixes`` use :func:`_commands_without_current_doctor_fix`. """ if "commands" in payload: payload["commands"] = _commands_without_current_doctor_fix(payload.get("commands")) @@ -1279,6 +1282,10 @@ def _pending_tick_redirect(gap: str | None) -> dict[str, Any]: ``session_not_failed`` stays absent there. ``supersede_pending=true`` is the explicit replace. No Parent baseline is written. + On a soft gap, ``commands`` drops the exact ``evolver doctor --fix`` + and a ``next_steps`` entry that names that command is rewritten so the + current step is ``swarm_solidify``. ``gate_soft_continue`` stays omitted. + ``tick_again`` stays false. This does not execute ``--fix``. """ meta = _pending_solidify_meta() count = _bump_pending_redirect(str(meta.get("run_id") or "")) @@ -1329,6 +1336,7 @@ def _pending_tick_redirect(gap: str | None) -> dict[str, Any]: ] if gap is None: _prefer_solidify_steps(payload, hold_tick=False) + _defer_doctor_fix_on_steer(payload) return payload from evolver.ops.open_box import apply_soft_gap @@ -1383,6 +1391,7 @@ def _pending_tick_redirect(gap: str | None) -> dict[str, Any]: # Drop gate_soft_continue. ok and error stay as set above: the missing # flag only withdraws another tick, and it is not a failed session. _prefer_solidify_steps(surfaced, hold_tick=True) + _defer_doctor_fix_on_steer(surfaced) return surfaced @@ -1811,6 +1820,9 @@ async def swarm_tick( Read ``session_not_failed`` before other flags. A soft gap omits ``gate_soft_continue`` on the first redirect and on the fuse. The first call keeps ``ok`` true and sets no ``error``. + This steer does not list ``evolver doctor --fix`` as the current step + in ``commands`` or ``next_steps``. The command stays listed when no + run is unfinished. This call does not execute it. Neither call starts a cycle or replaces ``last_run``. ``supersede_pending=True`` is the explicit replace. Checkpoint and pause still outrank that flag and do not consume the redirect count. diff --git a/tests/test_mcp_host_usability_pending_smoke.py b/tests/test_mcp_host_usability_pending_smoke.py index 95c6b70..26f02c0 100644 --- a/tests/test_mcp_host_usability_pending_smoke.py +++ b/tests/test_mcp_host_usability_pending_smoke.py @@ -232,6 +232,28 @@ def _host_next_call(payload: dict[str, Any]) -> str: return "stop" +def _assert_doctor_fix_deferred(payload: dict[str, Any]) -> None: + """Soft solidify steer: exact ``evolver doctor --fix`` is not current. + + Other catalog commands stay. The preserve triad still names solidify. + """ + assert _checklist_runs_doctor_fix_now(payload) is False + commands = [str(item).strip() for item in payload.get("commands") or []] + assert "evolver doctor --fix" not in commands + gap = payload.get("gap") + if gap == "pack_absent": + assert "evolver bench freeze" in commands + if gap == "no_baseline": + assert "evolver bench freeze" in commands + assert "evolver bench baseline" in commands + if gap in ("pack_absent", "no_baseline"): + steps = [str(step).lower() for step in payload.get("next_steps") or []] + assert any("do not run evolver doctor --fix before solidify" in step for step in steps) + assert payload.get("action_required") == "swarm_solidify" + assert payload.get("preserve_pending") is True + assert payload.get("abandon_run") is False + + def _checklist_runs_doctor_fix_now(payload: dict[str, Any]) -> bool: """True when a list offers ``evolver doctor --fix`` as the current step. @@ -377,6 +399,8 @@ def test_prompt_mode_and_doctor_are_listed_with_descriptions(self) -> None: description = tick.description or "" assert "prompt_mode=short" in description assert "\n " not in description + joined = " ".join(description.split()) + assert "does not list evolver doctor --fix as the current step" in joined doctor = tools["swarm_doctor"] assert doctor.annotations is not None @@ -607,6 +631,7 @@ def test_instrument_names_the_read_only_alternative_only_while_pending(self) -> ) assert "先调用 `swarm_solidify`" in pending assert "不把 `evolver doctor --fix` 当当前步骤" in pending + assert "tick 与保险丝的 `commands` 和 `next_steps`" in pending assert "只读冒烟" in pending assert "swarm_doctor" in pending assert "cycle_started=false" in pending @@ -685,10 +710,12 @@ 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 + # Soft-gap solidify steer: doctor --fix is not the current step. + # Other catalog commands stay. The flag stays omitted, not false. + _assert_doctor_fix_deferred(result) + assert result.get("gate_soft_continue") is not False + assert "gate_soft_continue" not in result + assert result.get("soft_continue") is not False assert not (isolated_swarm_env / "evolution" / "swarm_state.json").exists() async def test_ready_gate_redirect_stays_soft( @@ -719,6 +746,7 @@ def ready() -> dict[str, Any]: assert "preserve_pending" in str(result.get("note") or "") _assert_solidify_step_first(result) assert _host_next_call(result) == "swarm_solidify" + assert _checklist_runs_doctor_fix_now(result) is False again = await swarm_tick() assert again["severity"] == "soft" @@ -781,6 +809,9 @@ async def boom(is_loop: bool = False) -> dict[str, Any]: assert first["ok"] is True assert first.get("error") is None assert first["redirect_count"] == 1 + _assert_doctor_fix_deferred(first) + assert "gate_soft_continue" not in first + assert first.get("gate_soft_continue") is not False assert first["progress"] is False assert first["tick_again"] is False assert first["tick_closed"] is False @@ -814,6 +845,11 @@ async def boom(is_loop: bool = False) -> dict[str, Any]: _assert_stop_is_not_failure(closed, closed=True, gap=True) _assert_solidify_step_first(closed) assert _host_next_call(closed) == "swarm_solidify" + _assert_doctor_fix_deferred(closed) + assert "gate_soft_continue" not in closed + assert closed.get("gate_soft_continue") is not False + assert closed.get("soft_continue") is not False + assert closed["ok"] is False note = str(closed.get("note") or "") assert "not a gate" in note.lower() assert "preserve_pending" in note diff --git a/tests/test_swarm.py b/tests/test_swarm.py index 4bcfdc0..42719ba 100644 --- a/tests/test_swarm.py +++ b/tests/test_swarm.py @@ -477,6 +477,10 @@ async def test_tick_soft_continues_while_the_gate_cannot_fully_publish( assert result.get("error") not in ("pack_absent", "no_baseline", "unmeasured") assert result.get("next_steps") assert result.get("warnings") + # No unfinished run yet: the catalog command stays a current step. + assert any( + str(item).strip() == "evolver doctor --fix" for item in result.get("commands") or [] + ) async def test_fresh_tick_carries_no_proposal_mandate(self, isolated_swarm_env: Path) -> None: """Charter step 1: the field exists on every dispatched tick; a novel