Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions doc/architecture/SPEC/cli.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# cli/ —— 规范

<!-- verified-against: 2026-08-26 -->
<!-- verified-against: 2026-08-28 -->

`LOC ~1240(6 个文件) · 接口 + 编排门面 · refactor-status: ok`
`LOC ~1420(6 个文件) · 接口 + 编排门面 · refactor-status: ok`

## 职责
flag CLI 与 `Copilot` 门面:解析 → 过门 → 执行;并持有 run 目录、RunTrace、notifier
Expand Down Expand Up @@ -54,6 +54,19 @@ flag CLI 与 `Copilot` 门面:解析 → 过门 → 执行;并持有 run 目
**抬高档位永远不会扩大权限**(`tier` 仍然由 `kind` 推导)。
- CLI 主路径在**创建 run 目录之前**过门,所以被放弃的计划不留下任何东西。
MCP 的预约形状(先建、后规划)是**刻意不同**的 —— 见 `mcp_server.md`。
- **`reserve_run` 返回 `(run_id, created)`,签名变更是契约的一部分**:
调用方只在 `created` 为真时入队(idempotency:命中既有键返回既有 run)。
预约时即经 `mcp_policy.authorize_repo_path` 授权/固化 `spec.repo_path`;
带键的预约走 `_reserve_keyed`/`_reserve_new`(重拉-冲突-已决三分)。
- **执行的 at-most-once 是 claim,不是锁**:`_execute_reserved_locked`
先 `rs.claim_for_execution(run_dir, child_pid)` —— 输掉 claim 的子进程
不规划、不执行、不写状态,打印后 0 码退出(关掉"server 死后重试 +
旧子进程醒来"的双评审竞态)。
- **冻结的 `spec.repo_path` 压过环境解析**:`_repo_path_for(spec)` 先看
冻结绑定、再落回 alias 解析(`_resolve_repo_path` 现在只管 alias);
`resolve()` 与 `_execute` 的 state seed 都走它。
- 阻塞原因随终局落盘:`last_blocked_reason` 进入 `run_status.json` 的
note —— 以前只打在子进程 console 上就被丢掉。

## 边界 —— 不属于这里
不含 step 逻辑、不含仓库知识字面量、不含 LLM prompt。**只做编排接线。**
Expand All @@ -68,7 +81,9 @@ flag CLI 与 `Copilot` 门面:解析 → 过门 → 执行;并持有 run 目
新的纯格式化器 → utils.py。

## 测试
`test_cli.py`、`test_phase_b.py`、`test_chat.py`、`test_ui.py`。
`test_cli.py`、`test_phase_b.py`、`test_chat.py`、`test_ui.py`;
预约/claim/repo_path 冻结:`test_mcp.py`、`test_idempotency.py`、
`test_contract.py`。

## 重构备注
拆分**已完成**(它曾是内聚拆分候选)。`Copilot` 类完整留在 `copilot.py`,
Expand Down
14 changes: 10 additions & 4 deletions doc/architecture/SPEC/config.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# config.py —— 规范

<!-- verified-against: 2026-08-26 -->
<!-- verified-against: 2026-08-28 -->

`LOC ~630 · 配置 · refactor-status: oversized`
`LOC ~658 · 配置 · refactor-status: oversized`

## 职责
从 env / `.env` 加载的 `Settings`(pydantic-settings),以及把本次 run 的档位与后端
Expand All @@ -13,12 +13,18 @@
(reviewer 模型、远端 CI 轮次/预算、`github_token` 推送凭据)、agent 运行时、
ensemble、MoA、评审深度与按 pass 路由、Strict 后端选择、profile、patch 触发器、
metrics 与升级,提供带类型字段和安全默认值;外加 PR4d 知识运行时 cutover
(`imx_knowledge_runtime`)与 manifest 展开回退(`expansion_env()`)。
(`imx_knowledge_runtime`)、manifest 展开回退(`expansion_env()`),以及
MCP `repo_path` 授权面与 idempotency 保留期:
`mcp_allowed_repo_roots`(`MCP_ALLOWED_REPO_ROOTS`,空 = 只有已配置的
checkout —— 最小权限;放宽是**运维**决定,不是调用方的)与
`idem_retention_days`(默认 30,圈住 `.idem/` 索引)。

## 公开契约
带全部可调项的 `Settings`;`reviewer` / `intent`(回退到 `agent_model`);
`repo_path(name)`;`model_for(mode)`;`tier_target(role)` → `ResolvedTarget`;
`expansion_env()`;`knowledge_runtime_repos`;以及 `strict_backend` 校验器。
`expansion_env()`;`knowledge_runtime_repos`;`allowed_repo_roots`
(配置列表,未设时 = 已配置 checkout 本身 —— `mcp_policy.authorize_repo_path`
的圈禁判据即此属性);以及 `strict_backend` 校验器。

## 不变量(**A5**、**C2**、**B1**)
- 密钥只经 env / `.env`(被 git 忽略,**绝不提交**)。
Expand Down
62 changes: 62 additions & 0 deletions doc/architecture/SPEC/contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# contract.py —— 规范

<!-- verified-against: 2026-08-28 -->

`LOC ~181 · 跨仓库公开契约面 · refactor-status: ok`

## 职责
本 copilot 的**对外消费契约**:一个 review bot(或任何宿主)被允许 import 的
一切都住在这里,此外的任何 import 都不受支持。它的存在源于一次真实事故:
下游仓库曾经用 `importlib` 伸进 `thin_mcp_server` 拿四个 `_direct_*` 私有名,
一次重命名就让另一个仓库在运行期断裂、且没有任何构建期信号。

## 公开契约(`__all__`)
`DIRECT_API_VERSION` / `STRICT_API_VERSION`(`"1.0.0"`,形状变化到消费方
必须察觉时递增);`capabilities(max_strict_workers=1,
supports_file_locking=True) -> dict`(版本/能力握手:`supports_expected_head`、
`supports_structured_result`、`supports_post_false`、`supports_file_locking`、
`max_strict_workers`);`build_review_result(run_dir) -> dict`(结构化评审
结果:`contract_version`、`run_id`、`state`、`reviewed_head_sha`、`verdict`、
`summary_markdown`、`comments`、`stale`/`expected_head_sha`/
`actual_head_sha`、`diagnostics`);`unknown_run_result(run_id)`(显式
`state: unknown`,绝不抛错 —— 丢响应和还在跑必须可区分);
`sanitize_comments` 与 `COMMENT_FIELDS`;以及从 `direct_routing` 再导出的
`direct_knowledge_routes` / `direct_execution_budget` /
`direct_completion_result` / `direct_mandatory_review_guides`。

## 不变量
- **依赖方向单向、由测试钉住**:本模块 import 数据层(`run_status`、
`run_trace`),**绝不** import 任何 MCP server 模块;server 向下 import
这里。`test_contract.py::test_contract_imports_no_server_module` 钉住。
- `build_review_result` **只读 run 已持久化的东西**:对已完成、进行中、
评审步之前就死掉的 run 都成立;JSON 读取 fail-soft(早死的 run 合法地
缺文件)。
- verdict 是**字段**不是 Markdown 刮取;stale 是**数据**
(`stale`/`expected`/`actual`),不是从散文推断。
- `sanitize_comments` 按 `COMMENT_FIELDS` 白名单
(file/line/severity/comment/evidence/suggestion)——内部记账键
(`_verified`、`corroborated_by` 等)绝不泄进消费方输出。
- 本模块自身保持仓库中立;仓库专属的 Direct 路由表住在
`direct_routing.py`(见其页)。

## 边界 —— 不属于这里
不调模型、不执行 run、不做策略强制(`mcp_policy`)、不实现 Direct 路由
(委托 `direct_routing`)。

## 依赖(允许)
`run_status`、`run_trace`、`direct_routing` —— 只向下。消费方:
`mcp_server.py`(capabilities / build_review_result / unknown_run_result)
与仓库外的 reviewbot(`direct_*` 四件套)。

## 扩展点
消费方需要的新字段 → 加进 `build_review_result` 并递增 API 版本;
新的 Direct helper → 在 `direct_routing` 实现、在这里再导出。

## 测试
`test_contract.py`(23 例:verdict 字段化、评论白名单、stale 即终局事实、
早死 run 的降级结果、unknown-run 显式化、能力上报、import 方向、
本模块仓库中立)。

## 重构备注
新模块(PR2 mixed-mode contract 拆分)。保持它薄:任何"顺手在这里实现"
的诱惑都在重造 thin_mcp_server 的巨石。
61 changes: 61 additions & 0 deletions doc/architecture/SPEC/direct_routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# direct_routing.py —— 规范

<!-- verified-against: 2026-08-28 -->

`LOC ~768 · Direct 模式知识路由(表 + 机制) · refactor-status: known-debt`

## 职责
Direct 模式的知识路由:owner/model 路由表和选路机制。从
`thin_mcp_server.py` **逐字迁出**(下游曾经 `importlib` 拿它的私有名),
公开面由 `contract.py` 再导出。它显式携带仓库专属知识 —— 这正是
`contract.py` 必须保持中立而这里不必的原因;把表外置到
`adapters/<repo>/` 是**未被这次搬迁改变的既有欠账**。

## 公开契约
经 `contract.py` 再导出的四个名字:`direct_knowledge_routes`、
`direct_execution_budget`、`direct_completion_result`、
`direct_mandatory_review_guides`。其余全部下划线私有 —— 仅供
`thin_mcp_server` 既有调用点/测试使用(它继续直接 import 下划线名)。

## 不变量
- **repo 守卫最先跑**:不支持的仓库在任何路由计算之前被拒 —— 修的是一个
真实历史 bug(守卫曾排在空 intent 提前返回之后,向不支持的仓库泄漏
owner 知识)。
- **quick map fail-closed**:`_direct_quick_map` 返回内嵌代码地图与状态
`{ok, truncated, unavailable}`,`truncated` 不是装饰 —— 把残图当全图
与缺图同罪、且更难察觉;`_direct_route` 据此置
`read_required = status != "ok"`("自己去打开"是真回退,"什么都不给
又不许看"不是)。
Comment on lines +24 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route adapter quick maps through the fail-closed path

For supported adapter-backed repositories such as afd-plugin, _adapter_changed_file_routes bypasses _direct_route, stores _direct_quick_map(...) as a tuple, and hardcodes read_required=False. For afd_plugin/config.py, the public router therefore returns quick_map: ('', 'unavailable') while telling the host not to read the page, so the asserted fail-closed invariant and the resulting knowledge-read budget are false for an entire supported review path.

Useful? React with 👍 / 👎.

- **changed files 校验选择、绝不静默替换选择**:title/body 选 owner,
diff 只报告支持或矛盾;scope-fallback 是最后手段且永远显式
(`status="scope_fallback"`)。
- `_direct_execution_budget` 是**硬顶**预算字典(`hard_ceiling=True`、
一次有界扩展);docs-only PR 走更便宜的 profile。
- `_direct_completion_result` 是**机械结构门**,不校验证据真假:
单条最终评论、`subtraction_signal ∈ {none, triggered}`、
`evidence_head_sha` 7–40 位十六进制、`existing_feedback_status` 枚举、
`finding_dispositions` 的 anchor/disposition/existing_thread/
head_recheck 约束。
- 叶子模块:**绝不** import 任何 server 模块
(`test_contract.py::test_direct_routing_does_not_import_a_server_module`)。

## 边界 —— 不属于这里
不执行、不调模型;只有路由表 + 机制。多 adapter 桥接经
`_normalize_repo`/`_adapter_for_repo` 走 `adapters/`。

## 依赖(允许)
stdlib + `.adapters`(AdapterError / AdapterRegistry / RepoAdapter)。
位于 `contract.py` 和 `thin_mcp_server.py` 之下。

## 扩展点
新 owner 路由/模型规则 → 表数据;跨仓库通用化 → 外置进
`adapters/<repo>/routing`(既有欠账的正解,不是在这里再长表)。

## 测试
`test_contract.py`(公开家、import 方向、中立性豁免);
`test_thin_mcp_server.py` / `test_thin_mcp.py`(经新家继续锻炼全部
下划线函数:路由、预算、完成门)。

## 重构备注
768 行里约 500 行是表数据。`known-debt` 指的就是表:机制是稳定的,
表的归宿在 adapter 数据面(见结构重组计划 Stage 8 的 routing.yaml 方案)。
15 changes: 10 additions & 5 deletions doc/architecture/SPEC/engine/agent_runtime.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# engine/agent_runtime/ —— 规范

<!-- verified-against: 2026-08-26 -->
<!-- verified-against: 2026-08-28 -->

`LOC ~1690(7 个文件) · 引擎(受治理的 agent 运行时) · refactor-status: ok`

Expand Down Expand Up @@ -61,10 +61,15 @@
- **压根没跑起来**(proven dead)→ 改跑档位模型,并记 `moa_member_fallback`
(带 `phase`/`member`/`effective`/`reason`),绝不再问一次已死的成员。
死亡有**两种形态**,两种都必须接住:裸 API 成员**抛异常**,而 harness transport
会把一次死掉的会话转成**类型化的非 OK 结果**("a dead harness is an outcome"),
由 `outcome_blocked` 判定。首轮与**零产出重问**都要各自守住这两种形态;重问那一处
曾经完全没有守卫,于是一个 403 的成员直接穿过 `asyncio.gather` 把整个 step 打成
BLOCKED。回退每处至多一次,档位模型自己再失败就按类型化失败返回,**不递归**。
会把一次死掉的会话转成**类型化的非 OK 结果**("a dead harness is an outcome")。
**路由判据是 `member_unreachable`(seat 压根没跑,传输级死亡),不是
`outcome_blocked`("有没有产出"—— 对死掉的和被截断但活着的 seat 都答 True)**:
用后者做路由决定,正是 2026-08-16 把 18/28 个 holdout seat 静默改道的缺陷。
首轮与**零产出重问**统一走 `_attempt`/`_member_died` 守卫路径(重问那一处
曾经完全没有守卫,于是一个 403 的成员直接穿过 `asyncio.gather` 把整个 step
打成 BLOCKED);`_member_died` 还比对 `member == tier_model`(trace 的
`same_model` 字段)—— 解析成同一个模型的回退在 trace 里可见为 no-op。
回退每处至多一次,档位模型自己再失败就按类型化失败返回,**不递归**。
- MoA 成员本身也可以骑上 provider 注册表里的某个 harness(`transport_for_id`),
与本次 run 自己的 `STRICT_BACKEND` 无关。

Expand Down
20 changes: 15 additions & 5 deletions doc/architecture/SPEC/engine/lifecycle.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# engine/lifecycle.py —— 规范

<!-- verified-against: 2026-08-25 -->
<!-- verified-against: 2026-08-28 -->

`LOC ~121 · 引擎底座(run 生命周期原语) · refactor-status: ok`
`LOC ~141 · 引擎底座(run 生命周期原语) · refactor-status: ok`

## 职责
executor 自己给不了的两条**进程级**保证:同一 run 的互斥锁,与 run 离开事件
Expand All @@ -19,16 +19,26 @@ executor 自己给不了的两条**进程级**保证:同一 run 的互斥锁
`RunLock(run_dir).acquire()/release()`(context manager);`RunLockHeld`;
`register_finalizer(run_dir, fn)`(`fn(outcome)`,outcome 是 RunOutcome,或
executor 在产出之前就抛时为 None);`finalize(run_dir, outcome)`;
`run_guarded(run, run_dir) -> outcome`。调用方:`cli/copilot.py` 在
run/resume 全程持锁并用 `run_guarded` 包 `executor.run`;目前唯一的注册者是
rebase 流水线(flock 释放、scratch 清理、终局报告、CI abort 清理)。
`run_guarded(run, run_dir) -> outcome`;`require_file_locking()` 与
`FileLockingUnavailable`(POSIX advisory 锁不可用时抛出的 fail-closed 门)。
调用方:`cli/copilot.py` 在 run/resume 全程持锁并用 `run_guarded` 包
`executor.run`;finalizer 注册者是 rebase 流水线(flock 释放、scratch 清理、
终局报告、CI abort 清理)与 `engine/worktrees.py` 的共享持有释放;
`require_file_locking` 由 `engine/worktrees.py::materialize` 和
`idempotency.py`(`key_lock`/`reap_stale`)消费。

## 不变量
- `flock` 争用按 open file description 计:同进程内对同一路径的第二次
`acquire` 也失败 —— 抛 `RunLockHeld`,绝不静默共存。
- 锁文件释放后**留在原地**:它的存在不携带语义,只有 flock 本身算数。
- 非 POSIX(无 fcntl,与 `run_status.py` 同款守卫):无 advisory 锁,
`acquire` 直接成功 —— 注释声明的刻意降级(保住 run 可用;无 trace 事件)。
- **降级与 fail-closed 是两条刻意分开的路径**:`RunLock` 的静默降级只对
"单进程 CLI run 可用性"成立;对**正确性就是锁本身**的路径(worktree
materialize、idempotency 索引与 reaper),`require_file_locking()` 直接抛
`FileLockingUnavailable` —— 宁可拒绝,也不在无保护下运行并谎报保证
(不变量 7)。`contract.capabilities()` 会如实上报
`supports_file_locking=False`。
- finalizer **恰好一次**:`finalize` 先 pop 再跑,第二次调用是 no-op;
按注册顺序执行。
- 一个抛异常的 finalizer(**含 `CancelledError`** —— BaseException,不点名
Expand Down
12 changes: 10 additions & 2 deletions doc/architecture/SPEC/engine/steps/_common.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# engine/steps/_common.py —— 规范

<!-- verified-against: 2026-08-25 -->
<!-- verified-against: 2026-08-28 -->

`LOC ~264 · step 库基础设施 · refactor-status: ok`
`LOC ~300 · step 库基础设施 · refactor-status: ok`

## 职责
step 的自注册面,以及各 step 文件共享的跨模块 helper。
Expand All @@ -21,6 +21,14 @@ helper:`repo_path`、`require_repo`、`task_spec`、`from_state`、`published`
- 这是 step 共享 helper 的**唯一**归处 —— step 模块从这里 import,
**绝不互相 import**(**A2**)。
- helper 保持轻薄、对副作用诚实、且仓库中立。
- **`repo_path(ctx)` 不再是纯访问器** —— 两处刻意的变化都要知道:
(1) 三级优先序 `ctx.params → ctx.state → state["task_spec"]["repo_path"]`
(最后一级是预约时冻结、`authorize_repo_path` 授过权的绑定 —— executor
state seed 之外触达的 step 也与规划看到的 checkout 一致);
(2) 解析出的路径落在 `worktrees.worktree_root()` 之下时经
`_hold_if_worktree` 取共享 worktree 持有 —— 挂在**使用**而非创建上
(`--resume` 跳过已完成 step 的函数体,见 `engine/worktrees.md`);
取不到持有**绝不抛**(那是被回收的风险,不是评审错误)。

## 边界 —— 不属于这里
不含 step handler;不含领域逻辑。**只有基础设施 + 共享 IO。**
Expand Down
Loading
Loading