5853c3f8ff
Letting SourceNotConfiguredError through the gate wrappers opened a hole the recheck caught: _record_quietly only degrades GovernanceBackendError, so an assembly defect raised from the accounting side would now escape and destroy a response from a call that had already genuinely succeeded. That inverts the exact invariant _record_quietly exists to hold. Widening _record_quietly is the right fix rather than narrowing the wrappers, because that layer degrades by what the path is (accounting, the call is already done) rather than by which error type shows up. Narrowing would have left 4 of 9 wrapper methods as exceptions to a rule nobody can remember. No backend raises it from an accounting method today, so this is a guardrail for whoever adds source-name validation to a breaker backend. The stub that first reported this green was wrong: its record_success lacked count_attempt, so it raised TypeError and the wrapper relabeled it. Fixed signature, then the test failed as it should have. Also finishes the three-to-five leak path correction across the four remaining spots, including the wiki summary card that indexes this design.
291 lines
19 KiB
Markdown
291 lines
19 KiB
Markdown
# 实现计划: 治理后端故障归位为 scope 级不可用(Issue #7)
|
|
|
|
- **设计**: `research-wiki/designs/2026-08-06-governance-backend-error-design.md`(已批准 2026-08-06,Q1/Q2/Q3 逐条拍板)
|
|
- **分支**: `feat/issue-7-governance-backend-error`
|
|
- **目标**: 让"限流/熔断后端故障"在类型上落入 `GatewayUnavailableError`,使调用方一条 `except` 覆盖完整;同时把混在同一类里的装配缺陷拆出去,避免配置写错的任务永远重投。
|
|
- **方案概述**: `GovernanceBackendError` 改继承 `GatewayUnavailableError`(新 reason `governance_backend_down`,`retry_after_s` 默认 5.0);两处"未知源"改抛新增的 `SourceNotConfiguredError`(**不**在 scope 级家族内);`scope` 由后端层 `self._scope` 与两个 gate 包装器注入。
|
|
- **涉及技术**: Python 3.11+,pytest(含真实 Redis 的 integration),radon/ruff 门禁。
|
|
|
|
## 保真校验(适用)
|
|
|
|
本计划触及 ARCHITECTURE.md §1.4 索引的移植蓝本:错误分类(`reference/CHSAnalyzer/app/domain/errors.py`)与限流/熔断(`reference/CHSAnalyzer/app/coordination/`)。
|
|
|
|
本次**有意变更**的语义只有一条,已在设计 §3.1 声明:`GovernanceBackendError` 的类型归属(CHS 的 `LimiterError` 是独立异常,本库将其提升为 scope 级不可用的一员)。除此之外,下列承自 CHS 的语义**不得被顺带改动**,每个任务完成前逐条自查:
|
|
|
|
| 不得改动 | 出处 |
|
|
|---|---|
|
|
| `retry_after_s` 非可选、`0 = 可立即重试` | `errors.py:74-78` |
|
|
| `SCOPE_REASONS` 既有 5 值与 `SOURCE_REASONS` 既有 7 值 | `errors.py:7-20` |
|
|
| fail-closed 降级方向(限流/熔断后端挂 → 报错而非放行) | 库铁律 |
|
|
| 记账路径降级为 warning、闸门路径上抛的分工 | `middleware/retry.py:404` |
|
|
| `RedisPermit.release/settle` 的释放侧降级 | `backends/redis/limiter.py:133,151` |
|
|
|
|
## 文件结构
|
|
|
|
| 文件 | 职责 | 动作 |
|
|
|---|---|---|
|
|
| `research-wiki/ARCHITECTURE.md` | 架构单一事实源 §6.1 错误分类表 | 修改(**必须先行**,见设计 §8.1) |
|
|
| `src/polygateway/errors.py` | 错误类型树内核 | 修改: 新常量、新 reason、新类、继承变更 |
|
|
| `src/polygateway/__init__.py` | 公共 API 面 | 修改: 导出新类 |
|
|
| `src/polygateway/backends/redis/limiter.py` | Redis 限流后端 | 修改: 6 处补 scope、1 处换新类 |
|
|
| `src/polygateway/backends/redis/breaker.py` | Redis 熔断后端 | 修改: 5 处补 scope |
|
|
| `src/polygateway/backends/memory/limiter.py` | 内存限流后端 | 修改: 1 处换新类 |
|
|
| `src/polygateway/middleware/ratelimit.py` | `QuotaGate` 包装器 | 修改: 构造增 scope、4 处补 scope |
|
|
| `src/polygateway/middleware/breaker.py` | `BreakerGate` 包装器 | 修改: 构造增 scope、5 处补 scope |
|
|
| `src/polygateway/middleware/retry.py` / `ocr.py` / `embedding.py` | 三处 gate 装配 | 修改: 各 2 行传 scope |
|
|
| `tests/unit/test_errors.py` | 错误类型契约 | 修改 |
|
|
| `tests/unit/test_backpressure.py` | 后端故障传播 | 修改 |
|
|
| `tests/unit/test_redis_key_layout.py` | 未知源行为 | 修改 |
|
|
| `tests/integration/test_redis_cross_connection.py` | 真实 Redis 掉线 | 修改 |
|
|
| `README.md` / `research-wiki/migrations/chsanalyzer.md` / `CHANGELOG.md` / `pyproject.toml` | 文档与版本 | 修改 |
|
|
|
|
## 关键接口(跨任务消费,此处写死)
|
|
|
|
`errors.py` 新增与变更部分:
|
|
|
|
```python
|
|
GOVERNANCE_BACKEND_RETRY_AFTER_S = 5.0
|
|
"""治理后端故障的建议重投间隔(秒)。
|
|
|
|
**不是环境配置项**——后端恢复时间物理上不可知(不同于熔断冷却有确定到期
|
|
时刻),故取一个保守固定值;下游有自己的退避策略时可忽略本字段。取 0 会让
|
|
积压任务零延迟同时冲击已挂掉的后端(issue #7 §3.2)。
|
|
"""
|
|
|
|
|
|
class SourceNotConfiguredError(PolyGatewayError):
|
|
"""源名不在限流后端的配置字典中: 装配缺陷,正常不可达。
|
|
|
|
**有意不在** `GatewayUnavailableError` 之下: 它不是"暂时不可用"而是
|
|
"配置写错了",必须消耗失败预算进死信让人看见;归入可重投家族会让配置
|
|
错误的任务永远重投、永不告警(issue #7 §3.4)。
|
|
"""
|
|
|
|
|
|
class GovernanceBackendError(GatewayUnavailableError):
|
|
"""限流/熔断状态后端自身故障: 必须报错而非放行(防击穿网关,降级方向铁律)。
|
|
|
|
继承 `GatewayUnavailableError`: fail-closed 时一个请求都发不出去,语义
|
|
上即 scope 级不可用,调用方一条 except 即可覆盖(issue #7)。
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
*,
|
|
scope: str,
|
|
retry_after_s: float = GOVERNANCE_BACKEND_RETRY_AFTER_S,
|
|
source_name: str | None = None,
|
|
) -> None:
|
|
super().__init__(
|
|
scope=scope,
|
|
reason="governance_backend_down",
|
|
retry_after_s=retry_after_s,
|
|
source_name=source_name,
|
|
)
|
|
# 父类会把 message 覆写为 "{scope} 网关暂时不可用: {reason}",而各构造点
|
|
# 携带的诊断串是排障主线索,必须保住(设计 §3.5,机制已实跑验证)
|
|
self.args = (message,)
|
|
```
|
|
|
|
两个 gate 包装器的构造签名(`scope` 为 keyword-only 必填):
|
|
|
|
```python
|
|
class QuotaGate:
|
|
def __init__(self, limiter: RateLimiter, *, scope: str) -> None:
|
|
self._limiter = limiter
|
|
self._scope = scope
|
|
|
|
|
|
class BreakerGate:
|
|
def __init__(self, gate: ProviderGate, *, scope: str) -> None:
|
|
self._gate = gate
|
|
self._scope = scope
|
|
```
|
|
|
|
---
|
|
|
|
## 任务清单
|
|
|
|
### - [x] T1: ARCHITECTURE §6.1 回补(必须先行)
|
|
|
|
**文件**: `research-wiki/ARCHITECTURE.md`(§6.1,约 372-380 行)
|
|
|
|
**行为**: 在错误分类表补两行——`GovernanceBackendError`(scope 级不可用,reason 恒为 `governance_backend_down`)与 `SourceNotConfiguredError`(装配缺陷,不重试不换源,消耗失败预算);scope 级 `reason` 值域由 5 值扩为 6 值,增 `governance_backend_down`。同时记录本次归位的理由与日期,并说明根因(该类是 M2 引入分布式后端时新增,当时未回补本表)。
|
|
|
|
**为什么先行**: `ARCHITECTURE.md` 是单一事实源,新 reason 值域与其现状冲突;先改代码后补文档等于让实现与事实源脱节(设计 §8.1)。
|
|
|
|
**验收**: §6.1 表格含上述两行;reason 值域文字与 `errors.py` 将要写入的 `SCOPE_REASONS` 逐字一致。
|
|
|
|
**测试要求**: 纯文档,无测试证据要求。
|
|
|
|
**验证**: `grep -n "governance_backend_down\|SourceNotConfiguredError" research-wiki/ARCHITECTURE.md` → 至少各 1 处命中。
|
|
|
|
**提交**: `docs: admit governance backend failures into the scope-level error model`
|
|
|
|
---
|
|
|
|
### - [x] T2: errors.py 纯增量(新常量、新 reason、新类)+ 导出
|
|
|
|
**文件**: 改 `src/polygateway/errors.py`、`src/polygateway/__init__.py`;改 `tests/unit/test_errors.py`
|
|
|
|
**行为**:
|
|
1. 加模块级常量 `GOVERNANCE_BACKEND_RETRY_AFTER_S = 5.0`(docstring 逐字见上文"关键接口");
|
|
2. `SCOPE_REASONS` 增 `"governance_backend_down"`;
|
|
3. 新增 `SourceNotConfiguredError(PolyGatewayError)`(定义逐字见上文);
|
|
4. `__init__.py` 的 import 块与 `__all__` 各增 `SourceNotConfiguredError`(`__all__` 保持字母序: `SourceDeadError` → **`SourceNotConfiguredError`** → `TransientError`,即插在 `SourceDeadError` **之后**)。
|
|
|
|
**本任务不动 `GovernanceBackendError`**——它是纯增量,不破坏任何既有调用点,可独立提交且全套件保持通过。
|
|
|
|
**测试要求(先失败后通过)**:
|
|
- 新增用例断言 `SourceNotConfiguredError` **不是** `GatewayUnavailableError` 的子类,且是 `PolyGatewayError` 的子类。改前该类不存在 → `ImportError`;改后 PASS。
|
|
- 新增用例断言 `"governance_backend_down" in SCOPE_REASONS`,且 `GatewayUnavailableError(scope="llm", reason="governance_backend_down", retry_after_s=0.0)` 可构造。改前 `reason` 校验抛 `ValueError` → 用例失败;改后 PASS。
|
|
- 新增用例断言 `from polygateway import SourceNotConfiguredError` 可用。
|
|
|
|
**验证**: `conda run -n PolyGateway pytest tests/unit/test_errors.py -v` → 全 PASS;`conda run -n PolyGateway pytest tests/ -q` → 与改动前同样全绿(纯增量不应影响任何既有用例)。
|
|
|
|
**提交**: `feat: add SourceNotConfiguredError and the governance backend reason`
|
|
|
|
---
|
|
|
|
### - [x] T3: `GovernanceBackendError` 归位 + 22 处构造点 + scope 注入(原子)
|
|
|
|
**文件**: 改 `src/polygateway/errors.py`、`backends/redis/limiter.py`、`backends/redis/breaker.py`、`backends/memory/limiter.py`、`middleware/ratelimit.py`、`middleware/breaker.py`、`middleware/retry.py`、`ocr.py`、`embedding.py`;改 `tests/unit/test_errors.py`、`tests/unit/test_backpressure.py`、`tests/unit/test_redis_key_layout.py`、`tests/integration/test_redis_cross_connection.py`
|
|
|
|
**为什么必须原子**: `scope` 是必填 keyword,继承变更与全部构造点若分批提交,中间状态会 `TypeError`,门禁跑不过。
|
|
|
|
**行为**:
|
|
|
|
1. `errors.py`: `GovernanceBackendError` 改继承 `GatewayUnavailableError` 并覆写 `__init__`(逐字见上文"关键接口")。
|
|
|
|
2. **两处未知源改抛新类**(设计 §3.4,Q1 已拍板):
|
|
|
|
| 位置 | 改为 |
|
|
|---|---|
|
|
| `backends/redis/limiter.py:198` | `raise SourceNotConfiguredError(f"未知源 {source_key!r}(scope={self._scope})")` |
|
|
| `backends/memory/limiter.py:92` | 同上 |
|
|
|
|
3. **后端层 11 处补 `scope=self._scope`**(该属性已存在: redis limiter `:170`、redis breaker `:291`、memory limiter 同名字段):
|
|
- `backends/redis/limiter.py` 的 `:250 / :268 / :275 / :286 / :298 / :305`(6 处)
|
|
- `backends/redis/breaker.py` 的 `:370 / :388 / :410 / :422 / :432`(5 处)
|
|
|
|
4. **两个 gate 包装器**: 构造函数改为上文"关键接口"的签名;`QuotaGate` 4 处(`ratelimit.py:30/38/46/54`)与 `BreakerGate` 5 处(`breaker.py:26/36/46/54/62`)的 `raise` 补 `scope=self._scope`。
|
|
- ~~各方法开头的 `except GovernanceBackendError: raise` **保持不变**~~ **← 这条是错的,2026-08-06 独立验证时炸出(见 §T6)**。正确做法: 该放行必须扩为 `except (GovernanceBackendError, SourceNotConfiguredError): raise`,否则新增的兄弟类型会落进下一行的 `except Exception` 被**重新包成** `GovernanceBackendError`,使 Q1 的拆分在唯一的生产路径上完全失效。
|
|
|
|
5. **三处装配各传 scope**(三处的 `self._scope` 均已在装配前赋值,无需调整顺序):
|
|
|
|
| 文件 | 行 | 改为 |
|
|
|---|---|---|
|
|
| `middleware/retry.py` | 186-187 | `QuotaGate(limiter, scope=self._scope)` / `BreakerGate(gate, scope=self._scope)` |
|
|
| `ocr.py` | 122-123 | 同款 |
|
|
| `embedding.py` | 123-124 | 同款 |
|
|
|
|
**测试要求(先失败后通过,逐条对应)**:
|
|
|
|
| 用例 | 文件 | 改前为何失败 |
|
|
|---|---|---|
|
|
| `GovernanceBackendError` 可被 `except GatewayUnavailableError` 接住,且 `reason == "governance_backend_down"`、`retry_after_s == 5.0` | `tests/unit/test_errors.py` | 改前非其子类,`pytest.raises(GatewayUnavailableError)` 不匹配 |
|
|
| `str(exc)` 仍为构造时的诊断串(防 §3.5 回归) | `tests/unit/test_errors.py` | 改前无该风险但改后若漏写 `self.args` 即失败,是回归护栏 |
|
|
| 闸门泄漏路径(共五条,见设计 §1.1)抛出的异常带正确 `scope`、且可被 `except GatewayUnavailableError` 接住;钉住 `try_acquire` / `try_enter` / `progress_age_s` 三条代表路径 | `tests/unit/test_backpressure.py` — **三条都要新增桩**。现状: `progress_age_s` 只有 `TestQuotaGateProgressAge`(`:243-257`)覆盖包装行为、不验 scope;`try_acquire`(`QuotaGate`)与 `try_enter`(`BreakerGate`)**完全无桩** | 改前异常无 `scope` 属性 → `AttributeError`;两条新路径改前无覆盖 |
|
|
| 未知源抛 `SourceNotConfiguredError`,且断言它**不是** `GatewayUnavailableError` | 改 `tests/unit/test_redis_key_layout.py:70-74`(`test_unknown_source_rejected`,现断言 `GovernanceBackendError`);内存版**当前无对应用例,需新增**一条同款(`backends/memory/limiter.py:92` 的 `_cfg("nope")`) | 改前 redis 版类型断言失败;内存版改前无覆盖(该分支从未被测过) |
|
|
| Redis 真实掉线时准入侧抛 scope 级异常且 `reason == "governance_backend_down"` | `tests/integration/test_redis_cross_connection.py:228-245`(真实 Redis,不 mock) | 改前无 `reason` 属性 |
|
|
|
|
**必须同批更新的既有测试构造点**(新签名为 keyword-only 必填,漏改即 `TypeError: missing required keyword-only argument`,门禁直接红):
|
|
|
|
| 位置 | 现状 | 改为 |
|
|
|---|---|---|
|
|
| `tests/unit/test_backpressure.py:176 / :181 / :186` | `raise GovernanceBackendError("redis 抖动")` | 补 `scope=`(任意测试 scope,如 `"llm"`) |
|
|
| `tests/unit/test_errors.py:89` | `exc = GovernanceBackendError("redis down")` | 同上;该用例现断言它**不属于**可重试分类,须一并改为断言它**是** `GatewayUnavailableError` |
|
|
| `tests/unit/test_backpressure.py:255 / :257` | `QuotaGate(_L())` / `QuotaGate(_Broken())` | `QuotaGate(_L(), scope="llm")` 等 |
|
|
|
|
**保真校验检查点**: 提交前对照上文"保真校验"五条逐条自查,确认无一被顺带改动。特别核对 `RedisPermit.release/settle`(`redis/limiter.py:133,151`)的 `except GovernanceBackendError` 仍能接住释放侧失败——该处是设计 §4 否决"让原始异常穿透"路线的直接原因。
|
|
|
|
**验证**:
|
|
- `conda run -n PolyGateway pytest tests/unit tests/contracts -v` → 全 PASS
|
|
- `conda run -n PolyGateway pytest tests/integration -v` → 全 PASS(需真实 Redis)
|
|
- `conda run -n PolyGateway pytest tests/ -q` → `0 failed`
|
|
- `conda run -n PolyGateway radon cc src -n C -s` → 无输出
|
|
- `make lint` → import-linter 契约全绿(本次不新增跨层依赖,应无变化)
|
|
|
|
**提交**: `fix: reparent governance backend failures under GatewayUnavailableError (issue #7)`
|
|
|
|
---
|
|
|
|
### - [x] T4: 公开错误面文档(issue #7 第二诉求)
|
|
|
|
**文件**: 改 `README.md`(§"错误模型(四分类)",约 114-125 行)、`research-wiki/migrations/chsanalyzer.md`
|
|
|
|
**行为**:
|
|
1. README 增一张两列表,明确区分**会到达调用方**与**库内吸收**:
|
|
|
|
| 会到达调用方 | 库内吸收 |
|
|
|---|---|
|
|
| `GatewayUnavailableError` 族(`CircuitOpenError` / `AllSourcesExhausted` / `GovernanceBackendError`) | `TransientError` |
|
|
| `RequestRejectedError` | `SourceDeadError` |
|
|
| `ResultInvalidError` | |
|
|
| `SourceNotConfiguredError` | |
|
|
|
|
2. 在该表下补一句说明: `TransientError` / `SourceDeadError` 的 docstring 描述的是**库内治理行为**,它们被 `middleware/retry.py:365` 接住并在预算耗尽时包成 `AllSourcesExhausted`,**不会**到达调用方——issue #7 记载下游曾据此写错整段设计文档。
|
|
3. `migrations/chsanalyzer.md` 的 G1 条目补注:后端故障现已并入 `GatewayUnavailableError`,项目侧 `except GatewayUnavailableError` 一条即覆盖完整,无需为 `GovernanceBackendError` 单列分支。
|
|
|
|
**验收**: 调用方仅读 README 即可判断该 catch 什么,无需读 `middleware/retry.py`。
|
|
|
|
**测试要求**: 纯文档,无测试证据要求。
|
|
|
|
**验证**: `grep -n "库内吸收" README.md` → 命中。
|
|
|
|
**提交**: `docs: publish which errors reach callers and which the library absorbs`
|
|
|
|
---
|
|
|
|
### - [x] T5: 版本 1.1.0 + CHANGELOG + Wiki 同步
|
|
|
|
**文件**: 改 `pyproject.toml`(version)、`src/polygateway/__init__.py`(`__version__`)、`CHANGELOG.md`;按 `research-wiki/docs-convention.md` §2 同步 Gitea Wiki
|
|
|
|
**行为**: 版本 `1.0.6` → `1.1.0`(有行为变更但无 API 破坏:加父类是扩大)。CHANGELOG 需写明:
|
|
|
|
- **行为变更**: 后端故障从"落入调用方兜底分支"变为"被 `except GatewayUnavailableError` 捕获";下游据此把它按"延期重投、不消耗失败预算"处置,这正是修复目标,但**处置路线确实变了**,升级前须确认下游的兜底分支没有依赖它。
|
|
- **新增**: `SourceNotConfiguredError`(公共导出)、`GOVERNANCE_BACKEND_RETRY_AFTER_S`、scope 级 reason `governance_backend_down`。
|
|
- **下游请读**: `GovernanceBackendError` 现携带 `scope` / `reason` / `retry_after_s`(默认 5.0)/`per_source_reasons`;`str(exc)` 仍是原诊断串,结构化字段并存。配置写错(源名不匹配)现在抛 `SourceNotConfiguredError` 而非 `GovernanceBackendError`,它**不**属于可重投家族——这是有意的,目的是让装配缺陷进死信而不是永远重投。
|
|
|
|
**验收**: 版本三处一致(`pyproject.toml` / `__init__.py` / CHANGELOG 标题);CLAUDE.md §6 要求"版本 bump 提交不得裸发",故本任务必须与 wiki 同步同批。
|
|
|
|
**测试要求**: 无行为变更,`pytest tests/ -q` 保持全绿即可。
|
|
|
|
**验证**: `grep -n "1.1.0" pyproject.toml src/polygateway/__init__.py CHANGELOG.md` → 三处命中。
|
|
|
|
**提交**: `chore: release 1.1.0`
|
|
|
|
---
|
|
|
|
### - [x] T6: 修复独立验证炸出的阻塞缺陷(计划外,2026-08-06)
|
|
|
|
T1–T5 全绿、全部门禁通过之后,全新上下文的 verifier 用一个**走 `QuotaGate` 的**端到端用例炸出:装配缺陷在唯一的生产路径上根本没有拆出去。
|
|
|
|
**缺陷**: `QuotaGate`/`BreakerGate` 的 `except GovernanceBackendError: raise` 只放行了旧类型,新增的 `SourceNotConfiguredError` 落进下一行 `except Exception` 被重新包成 `GovernanceBackendError`(`reason=governance_backend_down`、`retry_after_s=5.0`)。实证:
|
|
|
|
```
|
|
RAISED: GovernanceBackendError | isGatewayUnavailable=True | isSourceNotConfigured=False
|
|
| 限流后端故障(source_stats): 未知源 's1'(scope=llm)
|
|
```
|
|
|
|
即配置写错的任务照样落进"可延期重投"家族,**永远重投、永不进死信、无人告警**——正是 Q1 要防的镜像 bug,G2 等于没做。
|
|
|
|
**为什么原有测试测不出来**: T3 写的两条用例(`test_backpressure.py`、`test_redis_key_layout.py`)都直接打私有 `_cfg()`,绕过了包装器;而治理循环只经包装器访问后端。**盲区在于测试打的层次比生产路径低一层。**
|
|
|
|
**修复**(三处):
|
|
|
|
| 文件 | 改动 |
|
|
|---|---|
|
|
| `middleware/ratelimit.py` | 4 个方法的放行扩为 `except (GovernanceBackendError, SourceNotConfiguredError): raise` |
|
|
| `middleware/breaker.py` | 同上,5 个方法 |
|
|
| `middleware/telemetry.py:254` | 终态捕获元组加 `SourceNotConfiguredError`。**连带坑**: 放行生效后该异常不再是 `GovernanceBackendError`,而它在任何 attempt 之前抛出,若不显式捕获则 `emit_terminal_failure` 不触发、该路径**遥测归零**,违反"遥测必录"铁律 |
|
|
|
|
**回归测试**: `test_backpressure.py::TestUnknownSourceIsAssemblyDefect::test_survives_the_quota_gate_wrapper`(参数化覆盖 `try_acquire` / `stats`),**走包装器而非私有方法**。修前 2 failed,修后 PASS。
|
|
|
|
**同批文档订正**: 泄漏路径由"三条"改为**五条**(遗漏了 `QuotaGate.stats` 与 `BreakerGate.retry_after_s`,判据是该调用点是否被 `_record_quietly` 包裹);CHANGELOG 的 `per_source_reasons` 表述改为"属性存在但恒为 `{}`"。
|
|
|
|
## 完成后
|
|
|
|
按 CLAUDE.md §3 Phase 2,合并前须派**全新上下文**的 verifier subagent 做独立验证(`verification-before-completion`),并按新规则**前台运行**。随后走 `finishing-a-development-branch` 决定合并方式,并在 Gitea 关闭 issue #7。
|