From bda1b1671a3b6a804c31cac2b47d289537272f50 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 06:38:18 +0000 Subject: [PATCH] fix(mcp): keep gate_soft_continue true on soft unfinished ticks Unfinished soft-gap swarm_tick redirects and the pending_redirect_loop fuse dropped the true that apply_soft_gap writes. Soft-continue-only hosts then missed the advisory bit boot and status already show. Keep the key true and never write false. tick_again, retryable, fuse ok, and the solidify steer stay as they are. Co-authored-by: Steper Lin --- AGENTS.md | 6 +- CHANGELOG.md | 1 + DEBUG.md | 8 ++ README.md | 4 +- README.zh.md | 4 +- examples/swarm-quickstart/README.md | 2 +- src/evolver/mcp_server.py | 18 ++-- src/evolver/swarm.py | 86 ++++++++++--------- .../test_mcp_host_usability_pending_smoke.py | 37 ++++---- 9 files changed, 95 insertions(+), 71 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fb80cc5..2894092 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -408,7 +408,7 @@ Parent。软缺口(`soft_continue=true`)下 `swarm_doctor` 同样给出 这条引导上 `fixes` 不把 `evolver doctor --fix` 列为当前步骤;boot/status,以及 第一次未完成 run 的 tick 与保险丝,其 `commands` 与 `next_steps` 同样不把它当成 现在要执行的修复。没有未完成 run,或固化之后,列出这条命令仍然合适。本工具仍不执行它。 -tick / 保险丝上的 `gate_soft_continue` 仍缺席,不写成 false。 +有软缺口时,第一次 tick 与保险丝写 `gate_soft_continue=true`,不写成 false。 硬阻断的 doctor 仍是 `ok=false`,不另给这个动作,也不写 `tick_again`。 此时 `boot_once` 为空。忽略 `next_action` 再调 `swarm_tick` 时默认不启新周期。 第一次 `ok=true`、`severity=soft`、`cycle_started=false`、`progress=false`、 @@ -422,8 +422,8 @@ tick / 保险丝上的 `gate_soft_continue` 仍缺席,不写成 false。 不把裸码当成 `next_action`。软缺口留在 `warning`。两次返回都带 `preserve_pending=true`、`abandon_run=false`、 `action_required=swarm_solidify`(boot/status 在仍要固化时同样带上)。有软缺口时, -第一次 tick 与保险丝都不带 `gate_soft_continue`(缺席不是 false;`ok` 第一次仍为 true, -没有 `error`;`tick_again=false` 与 `retryable=false` 都表示不要再 tick,第一次也写 `retryable=false`,缺席不再当成可以再 tick)。两次 tick 都带 +有软缺口时,第一次 tick 与保险丝都写 `gate_soft_continue=true`(true 不是 false,也不是再 tick 的许可;`ok` 第一次仍为 true, +没有 `error`;`tick_again=false` 与 `retryable=false` 都表示不要再 tick,第一次也写 `retryable=false`)。两次 tick 都带 `session_not_failed=true`:先读它,会话没有失败。`loop_fuse` 不在 `error` 里, preserve 三元组仍指向 `swarm_solidify`。 boot/status 的 `gate_soft_continue` 仍为 true,且不带 `session_not_failed`。未完成 run 的软缺口 boot/status 同时写 `tick_again=false`(只看 `gate_soft_continue=true` 的宿主仍可能再 tick)。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f09514..1e11b0b 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 与保险丝写 `gate_soft_continue=true`**:第一次未完成 run 的 `swarm_tick`,以及同一条 run 再 tick 的 `loop_fuse=pending_redirect_loop`,在软缺口上保留 `apply_soft_gap` 写下的 `gate_soft_continue=true`,与 boot / status 同一口径。这个键不写成 false。`tick_again=false`、`retryable=false`、保险丝的 `ok=false`、`session_not_failed` 与 preserve 三元组不动。没有软缺口的重定向仍不写这个键。doctor 仍不写。不结算这条 run,不写 Parent baseline,不放宽固化门,不执行 `evolver doctor --fix`。 - **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,不放宽固化门。 diff --git a/DEBUG.md b/DEBUG.md index 5bae27e..68fa5e9 100644 --- a/DEBUG.md +++ b/DEBUG.md @@ -1265,3 +1265,11 @@ - **修复**:第一次未完成 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)没有做假修复。 + +### 75. 只认 `gate_soft_continue` 的宿主在 tick 与保险丝上看不到软缺口(MCP 易用性,已修) + +- **症状**:#67 之后,未完成 run 的软缺口 boot / status 写 `gate_soft_continue=true`。同一条 run 的第一次 `swarm_tick` 与 `loop_fuse=pending_redirect_loop` 把这个键删掉。只在键为 true 时才认出 soft-continue 的宿主,在这两份信封上错过软缺口,尽管 `warning` 里仍是建议、`tick_again=false` 已经表示不要再 tick。doctor 仍不写这个键。 +- **根因**:`apply_soft_gap` 把旗标写成 true。`_prefer_solidify_steps(hold_tick=True)` 再 `pop` 掉,用来同时躲开「只在 true 时 tick」和「只在 false 时判死」。boot / status 没有这次 pop。缺键对只认这个键的宿主等于「没有软缺口」。 +- **修复**:软缺口上的第一次 tick 与保险丝保留 true,并在 `hold_tick` 上再写一次 true,不写 false。停 tick 仍看 `tick_again=false`、`retryable=false`、`next_action=swarm_solidify`,保险丝的 `ok` 仍为 false。`session_not_failed` 与 preserve 三元组不动。没有软缺口的重定向不写这个键。doctor 仍不写。`commands` 里 `evolver doctor --fix` 的延后不动。不结算,不写 Parent baseline,不放宽固化门,不执行 `--fix`。目录缓存(DEBUG #61)未做假修复。 +- **经验**:**软缺口旗标的原义是「缺口是建议」,停 tick 要写在 `tick_again` 上。** 把 true 从 tick 信封拿掉,只认这个键的宿主就和 boot 对不上。写成 false 会重开 DEBUG #67。 +- **仍在**:只看 `gate_soft_continue=true`、完全不看 `tick_again` / `next_action` / `ok` / `retryable` 的宿主,第一次 tick 与保险丝之后仍可能再 tick。没有把这个旗标写成 false。doctor 仍不写 `gate_soft_continue`,只读 `soft_continue=true` 的冒烟余项仍在。把保险丝 `ok=false` 当成整段失败、且不读 `session_not_failed` 与 `loop_fuse` 的宿主(DEBUG #68 余项 2)仍会停掉会话;`ok` 留着 false。`commands` 里仍可能有 `evolver bench freeze` 与 `evolver bench baseline`。目录缓存(DEBUG #61)没有做假修复。 diff --git a/README.md b/README.md index 2bbd0f6..fc4584a 100644 --- a/README.md +++ b/README.md @@ -239,9 +239,9 @@ After `uv sync`, a pull, or any install, **restart the MCP server**. A long-live Live `tools/list` already publishes `swarm_tick` parameters `prompt_mode` (`full` or `short`) and `checklist_only`, with descriptions on those fields, and read-only `swarm_doctor`. A host catalog that still lists only `agent_name` and `include_prompt`, or that cannot find `swarm_doctor`, is a stale client session. Restarting the server reloads the code; start a new host session so the client reads `tools/list` again. -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. +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 true on the first redirect and on the fuse (`tick_again` is false). True is not false and not a license to tick: 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. Stop-tick stays on `tick_again` and `retryable`. This path never writes `gate_soft_continue=false`. 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 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. +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` is true on those soft-gap tick envelopes and is never false. The command stays listed when no run is unfinished, and after solidify. Doctor still leaves `gate_soft_continue` and `session_not_failed` 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 4b2b097..3017456 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 的 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` 时,`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=true`,不写成 false。没有未完成 run,或固化之后,列出这条命令仍然合适。doctor 仍不写 `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,循环继续。自由文本不会被当成新的工作区。 @@ -277,7 +277,7 @@ uv run evolver migrate-state soft-accept 选用 minimal T0 探针(`determine_t0_mode` 返回 `minimal`;`skip` 也已实现)。验收钩子(`solidify_hook`)在本轮已标 `soft_accept` 时按这个深度跑。硬接受仍跑完整 T0。若结算裁决带了 skip 或 minimal 探针,warnings 里会有 `t0_skipped_soft` 或 `t0_probe=minimal`。 -`pending_solidify` 仅在 `last_run.run_id` 与 `last_solidify.run_id` 不同时为真。成功的 solidify(含 soft-accept)会写下 `last_solidify`,下一次 status 或 boot 报告 `pending_solidify: false`,开局动作回到 `swarm_tick`。仍有待固化 run 时,`boot_once` 为空,宿主先固化。此时再调 `swarm_tick` 默认不启新周期。第一次仍是软返回(`ok` 为 true,`severity=soft`,`cycle_started=false`,`progress=false`,`redirect_count=1`,`next_action` 仍是 `swarm_solidify`,不改 `last_run`)。同一条 run 再 tick 返回 `ok=false`、`loop_fuse=pending_redirect_loop`、`redirect_reason=pending_redirect_loop`、`severity=soft`、`retryable=false`,并且没有 `error` 字段,仍然不启周期。`loop_fuse` 与这个 `redirect_reason` 是 tick 空转保险丝,不是 `error`,也不是 `next_action`。`next_action` 仍是 `swarm_solidify`。`dispatch_reason` 仍是 `pending_solidify`。第一次重定向的 `redirect_reason` 仍是 `pending_solidify`,没有 `loop_fuse`。常驻 instructions 写 `loop_fuse=pending_redirect_loop`。这不是门禁失败,也不是放弃;软缺口留在 `warning` 与 `next_steps`。两次返回都带 `preserve_pending=true`、`abandon_run=false`、`action_required=swarm_solidify`(boot 与 status 在仍要固化时同样带上)。不要丢掉这条未完成 run。有软缺口时,第一次 tick 与保险丝都不带 `gate_soft_continue`(缺席不是 false;`ok` 第一次仍为 true,没有 `error`;`tick_again=false` 与 `retryable=false` 都表示不要再 tick,第一次也写 `retryable=false`)。只把严格 false 当成整段会话失败的宿主因此不会停掉会话。两次 tick 都带 `session_not_failed=true`:先读这个字段,会话没有失败;`loop_fuse` 不在 `error` 里,preserve 三元组仍指向 `swarm_solidify`。`next_steps` 第一条是调用 `swarm_solidify`。boot 与 status 的 `gate_soft_continue` 仍为 true,未完成软缺口同时写 `tick_again=false`,且不带 `session_not_failed`。要替换这条 run 才传 `supersede_pending=true`。暂停与压缩检查点仍优先于这个参数。 +`pending_solidify` 仅在 `last_run.run_id` 与 `last_solidify.run_id` 不同时为真。成功的 solidify(含 soft-accept)会写下 `last_solidify`,下一次 status 或 boot 报告 `pending_solidify: false`,开局动作回到 `swarm_tick`。仍有待固化 run 时,`boot_once` 为空,宿主先固化。此时再调 `swarm_tick` 默认不启新周期。第一次仍是软返回(`ok` 为 true,`severity=soft`,`cycle_started=false`,`progress=false`,`redirect_count=1`,`next_action` 仍是 `swarm_solidify`,不改 `last_run`)。同一条 run 再 tick 返回 `ok=false`、`loop_fuse=pending_redirect_loop`、`redirect_reason=pending_redirect_loop`、`severity=soft`、`retryable=false`,并且没有 `error` 字段,仍然不启周期。`loop_fuse` 与这个 `redirect_reason` 是 tick 空转保险丝,不是 `error`,也不是 `next_action`。`next_action` 仍是 `swarm_solidify`。`dispatch_reason` 仍是 `pending_solidify`。第一次重定向的 `redirect_reason` 仍是 `pending_solidify`,没有 `loop_fuse`。常驻 instructions 写 `loop_fuse=pending_redirect_loop`。这不是门禁失败,也不是放弃;软缺口留在 `warning` 与 `next_steps`。两次返回都带 `preserve_pending=true`、`abandon_run=false`、`action_required=swarm_solidify`(boot 与 status 在仍要固化时同样带上)。不要丢掉这条未完成 run。有软缺口时,第一次 tick 与保险丝都写 `gate_soft_continue=true`(true 不是 false,也不是再 tick 的许可;`ok` 第一次仍为 true,没有 `error`;`tick_again=false` 与 `retryable=false` 都表示不要再 tick,第一次也写 `retryable=false`)。这条路径不写 `gate_soft_continue=false`。只把严格 false 当成整段会话失败的宿主因此不会停掉会话。两次 tick 都带 `session_not_failed=true`:先读这个字段,会话没有失败;`loop_fuse` 不在 `error` 里,preserve 三元组仍指向 `swarm_solidify`。`next_steps` 第一条是调用 `swarm_solidify`。boot 与 status 的 `gate_soft_continue` 仍为 true,未完成软缺口同时写 `tick_again=false`,且不带 `session_not_failed`。要替换这条 run 才传 `supersede_pending=true`。暂停与压缩检查点仍优先于这个参数。 下列情况仍硬停(不要重试同一次失败):diff 里的密钥或凭据、改写 `~/.evolver/` 下的引擎状态、`val_seal` 泄露。损坏的 bench 门(`gate_error`)以及验证面改动的 anchor 失败也会拒绝。没有假说时本轮仍被拒(`hypothesis_missing`),直到 `swarm_hypothesis` 成功。 diff --git a/examples/swarm-quickstart/README.md b/examples/swarm-quickstart/README.md index d6d8641..0434b10 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`,以及第一次未完成 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`。 +连上之后,本条消息已有别的任务就做那件事。没有别的任务时宿主调用 `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=true`,不写成 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=true`(true 不是 false,也不是再 tick 的许可;`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 6c89dae..e3c0436 100644 --- a/src/evolver/mcp_server.py +++ b/src/evolver/mcp_server.py @@ -473,10 +473,12 @@ async def tool_swarm_tick( "is not a gate failure and not an abandon signal. Both returns set " "session_not_failed=true, preserve_pending=true, " "abandon_run=false, and action_required=swarm_solidify. " - "A soft gap stays in warning. gate_soft_continue is omitted " - "on those redirects: 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. true starts a new cycle " + "A soft gap stays in warning. gate_soft_continue=true " + "on those redirects: true is not false and not a license to " + "tick, so do not treat the session as failed. This path never " + "writes gate_soft_continue=false. tick_again=false means do " + "not tick. The first redirect keeps ok true and sets no error. " + "true starts a new cycle " "and replaces that run. Does not weaken the solidify gate " "or write a Parent baseline. Checkpoint and pause still " "refuse the cycle." @@ -508,9 +510,11 @@ async def tool_swarm_tick( action_required=swarm_solidify. Read session_not_failed before other flags. True means the session is not failed: do not drop the unfinished 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. + On a soft gap, gate_soft_continue=true on the first redirect and on the + fuse. True is the soft gap, not a license to tick, and it is not false, + so do not treat the session as failed. This path never writes + gate_soft_continue=false. tick_again=false means do not tick. + 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. diff --git a/src/evolver/swarm.py b/src/evolver/swarm.py index 67ded82..7fb6909 100644 --- a/src/evolver/swarm.py +++ b/src/evolver/swarm.py @@ -303,9 +303,10 @@ def build_instrument_prompt(state: dict[str, Any]) -> str: `action_required=swarm_solidify`。 `session_not_failed=true` 表示会话没有失败, 先读它再读别的旗标。`tick_again=false` 与 `retryable=false` 都表示不要再 tick (第一次重定向也写 `retryable=false`,`ok` 仍为 true)。 - 有软缺口时 `gate_soft_continue is omitted` - (第一次也是,`ok` 仍为 true,没有 `error`):缺这个键不是 false, - 不是失败的会话,软缺口仍在 `warning`。再 tick 才是 `ok=false`、 + 有软缺口时第一次 tick 与保险丝都写 `gate_soft_continue=true` + (`ok` 第一次仍为 true,没有 `error`):true 不是 false, + 不是失败的会话,也不是再 tick 的许可,软缺口仍在 `warning`。 + 停 tick 看 `tick_again=false`。再 tick 才是 `ok=false`、 `loop_fuse=pending_redirect_loop`、`redirect_reason=pending_redirect_loop`、 没有 `error` 字段、`next_action` 仍是 `swarm_solidify`(保险丝码不是 `next_action`)、`severity=soft`、`retryable=false`:这不是门禁失败,也不放弃 @@ -357,7 +358,7 @@ def build_instrument_prompt(state: dict[str, Any]) -> str: 调用 `swarm_solidify`。`next_action` 仍是 `swarm_solidify`。没有 `error` 字段。 不是固化门失败,也不是整段会话失败。先读 `session_not_failed`,再读 `preserve_pending=true`、`abandon_run=false`,不要放弃这条 run。有软缺口时 - 第一次 tick 的 `gate_soft_continue is omitted`,不必再 tick 到这里。 + 第一次 tick 与保险丝的 `gate_soft_continue=true`,true 不是再 tick 的许可。 `retryable=true` 的拒绝是稳态,回到步骤 1。 @@ -1102,7 +1103,7 @@ def _stamp_preserve_pending(payload: dict[str, Any]) -> None: def _stamp_session_not_failed(payload: dict[str, Any]) -> None: """The unfinished-run tick redirect is not a failed session. - A soft-gap redirect omits ``gate_soft_continue``. ``loop_fuse= + A soft-gap redirect keeps ``gate_soft_continue`` true. ``loop_fuse= pending_redirect_loop`` is the tick fuse and is not an ``error``. Hosts read ``session_not_failed`` before either signal. True means call ``swarm_solidify`` and keep the run. The preserve triad still says how. @@ -1121,14 +1122,13 @@ def _prefer_solidify_steps( """Name ``swarm_solidify`` as the next call while a run is unfinished. ``hold_tick`` is only the pending tick redirect that already carries a - soft gap. ``apply_soft_gap`` sets ``gate_soft_continue`` true, which - invites another tick and is the only value a loop-while-true host reads. - This drops that key. Absent is not true, so that host stops. Absent is - not false, so a host that ends the session on strict false stays up - even when it ignores ``session_not_failed``. ``tick_again`` stays false. - ``ok`` and ``error`` are left alone. Boot and status keep the flag that - ``apply_soft_gap`` set, and those unfinished soft-gap envelopes also set - ``tick_again`` false. + soft gap. ``apply_soft_gap`` sets ``gate_soft_continue`` true. This keeps + that true on the first redirect and on the fuse, the same advisory bit + boot and status already show. True is not a license to tick: + ``tick_again`` stays false. This never writes false. Strict false is + session death for a host that ignores ``session_not_failed``. ``ok`` and + ``error`` are left alone. Boot and status keep the same true and also + set ``tick_again`` false. Doctor still omits the key. """ rid = str(run_id if run_id is not None else payload.get("run_id") or "").strip() who = f"run_id={rid}" if rid else "this unfinished run" @@ -1141,8 +1141,8 @@ def _prefer_solidify_steps( if hold_tick and not closed: lead += ( " session_not_failed=true. " - "gate_soft_continue is omitted. " - "Omitted is not false, so the session is not failed. " + "gate_soft_continue=true. " + "True is the soft gap, not a license to tick, and not a failed session. " "tick_again=false means do not tick. " "ok is still true and there is no error. " "retryable=false means do not tick." @@ -1150,8 +1150,8 @@ def _prefer_solidify_steps( elif hold_tick and closed: lead += ( " session_not_failed=true. " - "gate_soft_continue is omitted. " - "Omitted is not false, so the session is not failed. " + "gate_soft_continue=true. " + "True is the soft gap, not a license to tick, and not a failed session. " "tick_again=false means do not tick. " "loop_fuse=pending_redirect_loop is not an error field. " "redirect_reason=pending_redirect_loop is that fuse, not a next_action. " @@ -1182,11 +1182,12 @@ def _prefer_solidify_steps( payload["next_steps"] = rewritten if not hold_tick: return - payload.pop("gate_soft_continue", None) + # Keep the true apply_soft_gap wrote. Never assign false (DEBUG #67). + payload["gate_soft_continue"] = True if closed: steer = ( "session_not_failed=true. " - "gate_soft_continue is omitted: do not call swarm_tick. " + "gate_soft_continue=true: do not call swarm_tick. " "Call swarm_solidify. loop_fuse=pending_redirect_loop is not a failed session. " "redirect_reason=pending_redirect_loop is that fuse, not a next_action. " "There is no error field." @@ -1194,7 +1195,7 @@ def _prefer_solidify_steps( else: steer = ( "session_not_failed=true. " - "gate_soft_continue is omitted: do not call swarm_tick. " + "gate_soft_continue=true: do not call swarm_tick. " "Call swarm_solidify. ok is true and there is no error. " "The session is not failed. The soft gap stays advisory." ) @@ -1213,11 +1214,12 @@ def _pending_redirect_note(count: int, closed: bool, *, gap: bool) -> str: "Read session_not_failed before any other flag. " "Do not drop this unfinished run. Call swarm_solidify now." ) - omitted = "" + soft_flag = "" if gap: - omitted = ( - "gate_soft_continue is omitted. " - "Omitted is not false, so this is not a failed session. " + soft_flag = ( + "gate_soft_continue=true. " + "True is not false, so this is not a failed session. " + "True is the soft gap, not a license to tick. " "The soft gap stays in warning. " ) if not closed: @@ -1227,7 +1229,7 @@ def _pending_redirect_note(count: int, closed: bool, *, gap: bool) -> str: "ok stays true and this is not a hard gate. " "retryable=false means do not call swarm_tick. " f"{keep} " - f"{omitted}" + f"{soft_flag}" "Another swarm_tick on this same run returns " "loop_fuse=pending_redirect_loop, " "redirect_reason=pending_redirect_loop, ok false, and no error field. " @@ -1245,7 +1247,7 @@ def _pending_redirect_note(count: int, closed: bool, *, gap: bool) -> str: "next_action stays swarm_solidify. " "dispatch_reason stays pending_solidify. " "error is absent. severity stays soft. retryable=false. Do not tick again. " - f"{keep} {omitted}" + f"{keep} {soft_flag}" "Call swarm_solidify for this run_id. " "Pass supersede_pending=true only to replace that run. " "No cycle was started and no Parent baseline was written." @@ -1271,21 +1273,22 @@ def _pending_tick_redirect(gap: str | None) -> dict[str, Any]: means do not tick. The first call still keeps ``ok`` true. Read ``session_not_failed`` before other flags. True means the session is not failed: call ``swarm_solidify`` and keep the run. - When a soft gap is present, ``gate_soft_continue`` is omitted on the - first redirect and on the fuse. Absent is not true, so a host that - loops only while the flag is true does not take another tick. Absent - is not false, so a host that treats strict false as session death stays + When a soft gap is present, ``gate_soft_continue`` stays true on the + first redirect and on the fuse, the same bit ``apply_soft_gap`` writes + for boot and status. True is not a license to tick. This path never + writes false: a host that treats strict false as session death stays up even if it ignores ``session_not_failed``. ``tick_again`` is false. The first call keeps ``ok`` true and sets no ``error``. The gap itself stays in ``warning``. An unfinished soft-gap boot or status keeps ``gate_soft_continue`` true and sets ``tick_again`` false. - ``session_not_failed`` stays absent there. + ``session_not_failed`` stays absent there. Doctor still omits the key. ``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``. + current step is ``swarm_solidify``. ``gate_soft_continue`` stays true + and is never false. ``tick_again`` stays false. This does not execute + ``--fix``. """ meta = _pending_solidify_meta() count = _bump_pending_redirect(str(meta.get("run_id") or "")) @@ -1382,14 +1385,14 @@ def _pending_tick_redirect(gap: str | None) -> dict[str, Any]: "next_action stays swarm_solidify. " "The fuse is not an error and not a next_action. " "preserve_pending=true, abandon_run=false. Not a gate failure. " - "gate_soft_continue is omitted. tick_again=false means do not tick again. " + "gate_soft_continue=true. tick_again=false means do not tick again. " "The soft gap stays advisory." ) if loop_warn not in warnings: warnings.append(loop_warn) surfaced["warnings"] = warnings - # 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. + # Keep gate_soft_continue true. ok and error stay as set above. + # True is the soft gap, not a failed session, and not a license to tick. _prefer_solidify_steps(surfaced, hold_tick=True) _defer_doctor_fix_on_steer(surfaced) return surfaced @@ -1445,9 +1448,9 @@ def _opening_action(state: dict[str, Any]) -> tuple[str, str]: "存在待固化 run——先调用 `swarm_solidify` 完成固化,再回到步骤 1。" "此时直接 `swarm_tick` 不启新周期(`cycle_started=false`,`progress=false`)。" "两次 tick 返回都带 `session_not_failed=true`:会话没有失败。" - "有软缺口时这次 tick 的 `gate_soft_continue is omitted`" - "(`ok` 仍为 true,没有 `error`):缺这个键不是 false,不是失败的会话," - "`tick_again=false` 表示不要再 tick。" + "有软缺口时这次 tick 与保险丝的 `gate_soft_continue=true`" + "(`ok` 第一次仍为 true,没有 `error`):true 不是 false,不是失败的会话," + "也不是再 tick 的许可。`tick_again=false` 表示不要再 tick。" "同一条 run 再 tick 得到 `loop_fuse=pending_redirect_loop`" "与 `redirect_reason=pending_redirect_loop`" "(`ok=false`,没有 `error` 字段,`next_action` 仍是 `swarm_solidify`," @@ -1818,8 +1821,9 @@ async def swarm_tick( ``abandon_run`` false, and ``retryable`` false. An omitted ``retryable`` is not permission to tick again. The first call still keeps ``ok`` true. 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``. + A soft gap keeps ``gate_soft_continue`` true on the first redirect and + on the fuse, and never writes false. True is not a license to tick. + 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. diff --git a/tests/test_mcp_host_usability_pending_smoke.py b/tests/test_mcp_host_usability_pending_smoke.py index 26f02c0..805735c 100644 --- a/tests/test_mcp_host_usability_pending_smoke.py +++ b/tests/test_mcp_host_usability_pending_smoke.py @@ -304,18 +304,19 @@ def _assert_preserve_pending(payload: dict[str, Any]) -> None: def _assert_stop_is_not_failure(payload: dict[str, Any], *, closed: bool, gap: bool) -> None: """Stop-tick signals stay off the fail and abandon fields. - ``gate_soft_continue`` is omitted, so a host that dies on strict false - stays up, and a host that loops only while the flag is true does not tick. + A soft gap writes ``gate_soft_continue=true`` and never false, so a host + that dies on strict false stays up. A host that loops only while the flag + is true sees the same advisory bit as boot and status. Stop-tick stays on + ``tick_again`` and ``retryable``. Without a soft gap the key stays absent. The fuse code lives in ``loop_fuse``. ``error`` is absent, so a host that abandons on any error keeps the run. ``ok`` is still false on the fuse. """ - assert "gate_soft_continue" not in payload + assert payload.get("gate_soft_continue") is not False assert "error" not in payload assert payload.get("tick_again") is False assert _host_ticks_unless_tick_again_is_false(payload) is False assert payload.get("retryable") is False assert "retryable=false" in str(payload.get("note") or "") - assert _host_ticks_only_on_gate_soft_continue(payload) is False assert _host_ticks_unless_retryable_is_false(payload) is False if not closed: assert _host_dies_on_nonretryable_failed_ok(payload) is False @@ -345,8 +346,14 @@ def _assert_stop_is_not_failure(payload: dict[str, Any], *, closed: bool, gap: b assert _host_solidifies_only_if_error_is_the_fuse(payload) is False assert _host_solidifies_when_it_reads_the_fuse(payload) is False if gap: - assert "gate_soft_continue is omitted" in note + assert payload.get("gate_soft_continue") is True + assert _host_ticks_only_on_gate_soft_continue(payload) is True + assert "gate_soft_continue=true" in note assert "not a failed session" in note.lower() + assert "not a license to tick" in note + else: + assert "gate_soft_continue" not in payload + assert _host_ticks_only_on_gate_soft_continue(payload) is False def test_standing_instructions_name_the_fuse_not_a_bare_code() -> None: @@ -387,7 +394,8 @@ def test_prompt_mode_and_doctor_are_listed_with_descriptions(self) -> None: assert "next_action=pending_redirect_loop" not in supersede assert "preserve_pending" in supersede assert "abandon_run" in supersede - assert "omitted" in supersede + assert "gate_soft_continue=true" in supersede + assert "never writes gate_soft_continue=false" in supersede assert "session_not_failed" in supersede assert props["supersede_pending"].get("default") is False mode = props["prompt_mode"] @@ -641,7 +649,8 @@ def test_instrument_names_the_read_only_alternative_only_while_pending(self) -> assert "abandon_run=false" in pending assert "loop_fuse=pending_redirect_loop" in pending assert "redirect_reason=pending_redirect_loop" in pending - assert "gate_soft_continue is omitted" in pending + assert "gate_soft_continue=true" in pending + assert "gate_soft_continue is omitted" not in pending assert "error=pending_redirect_loop" not in pending assert "next_action=pending_redirect_loop" not in pending assert "session_not_failed=true" in pending @@ -711,10 +720,9 @@ async def boom(is_loop: bool = False) -> dict[str, Any]: assert result.get("gap") in _SOFT_GAPS assert any("cycle_started=false" in str(item) for item in result.get("warnings") or []) # Soft-gap solidify steer: doctor --fix is not the current step. - # Other catalog commands stay. The flag stays omitted, not false. + # Other catalog commands stay. The flag is true, never 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("gate_soft_continue") is True assert result.get("soft_continue") is not False assert not (isolated_swarm_env / "evolution" / "swarm_state.json").exists() @@ -810,8 +818,7 @@ async def boom(is_loop: bool = False) -> dict[str, Any]: 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.get("gate_soft_continue") is True assert first["progress"] is False assert first["tick_again"] is False assert first["tick_closed"] is False @@ -846,8 +853,7 @@ async def boom(is_loop: bool = False) -> dict[str, Any]: _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("gate_soft_continue") is True assert closed.get("soft_continue") is not False assert closed["ok"] is False note = str(closed.get("note") or "") @@ -1021,7 +1027,8 @@ def test_wire_tools_list_publishes_prompt_mode(tmp_path: Path) -> None: assert "next_action=pending_redirect_loop" not in tick["supersede_pending"]["description"] assert "preserve_pending" in tick["supersede_pending"]["description"] assert "abandon_run" in tick["supersede_pending"]["description"] - assert "omitted" in tick["supersede_pending"]["description"] + assert "gate_soft_continue=true" in tick["supersede_pending"]["description"] + assert "never writes gate_soft_continue=false" in tick["supersede_pending"]["description"] assert "session_not_failed" in tick["supersede_pending"]["description"] assert tick["supersede_pending"]["default"] is False assert tools["swarm_doctor"]["annotations"]["readOnlyHint"] is True