diff --git a/.agents/docs/2026-08-06-grpcgen-layered-control-design.md b/.agents/docs/2026-08-06-grpcgen-layered-control-design.md
new file mode 100644
index 0000000..93f8725
--- /dev/null
+++ b/.agents/docs/2026-08-06-grpcgen-layered-control-design.md
@@ -0,0 +1,171 @@
+# grpcgen 的分层控制:让「方便」与「可控」不是两条路
+
+> 状态:**设计待 review**
+> 涉及:`rules/src/grpcgen.cppm`、`templates/greeter/`、`mcpp.toml` 的 `[feature-deps.codegen]`
+> 前置:mcpp 2026.8.6.2(`reexport` / `rerun_if_changed_glob`)已发布
+
+---
+
+## 0. 现状与问题
+
+`generate_all()` 一行就能跑通,底子是对的:工作被**声明**成构建图的边(`mcpp::action`)而不是当场执行,所以增量、并行、失败归属到具体那条边;错误信息直接告诉用户该往 mcpp.toml 里加哪一行。
+
+问题不在「方便」,在**可控的层次只有两级**:
+
+```cpp
+struct options {
+ std::string_view proto_dir = "proto";
+ bool grpc = true;
+};
+```
+
+两个旋钮之外是断崖。需求一超出,用户只能像 `examples/helloworld` 那样手写六十行 build.mcpp,**绕开整个规则**——而那六十行要重新实现规则里已经解决过的东西:well-known types 目录怎么定位、嵌套 `.proto` 的输出子目录怎么建、输入集合怎么算。它们会与规则悄悄漂移,且没有任何机制会报出漂移。
+
+**断崖本身就是设计缺陷**:它把「优雅」和「可控」变成了二选一。
+
+## 1. 原则:每一层是下一层的默认值,不是另一条路
+
+只要下一层是「同一条代码路径 + 默认参数」,就不会出现「用了高级功能就失去便利」的分叉,也不会出现两套实现漂移。
+
+这条原则决定了后面每一层的形状:L1 不是新函数,是 `options` 多几个字段;L2 不是新入口,是把 `bool grpc` 降级成语法糖;L3 不是旁路,是把现有函数拆成 `plan` + `submit` 两半,`generate_all()` 变成它俩的组合。
+
+## 2. L0 — 默认(保持不变)
+
+```cpp
+import mcpp; import grpcgen;
+int main() { return grpcgen::generate_all() ? 0 : 1; }
+```
+
+模板与文档的主线形态。本设计不改变它的任何行为。
+
+## 3. L1 — 声明式旋钮
+
+补上真实项目**必然**撞到的三个缺口。按撞到的频率排序:
+
+| 缺口 | 什么时候遇到 | 证据 |
+|---|---|---|
+| 额外 import 路径 | 共享 proto 仓库;`google/api/annotations.proto`(gRPC-Gateway、Google API 风格接口) | 官方 `.proto` 之间互相 import 是常态,protoc 靠 `-I` 找 |
+| mock 生成 | 一写单测就要 | `grpc_cpp_plugin` 自带 `generate_mock_code=true`,现在**完全没有办法开** |
+| 任意 protoc 参数 | 兜住没想到的 | 例如 `--experimental_allow_proto3_optional` |
+
+```cpp
+grpcgen::generate_all({
+ .imports = {"../shared/proto"},
+ .mock = true,
+ .protoc_args = {"--experimental_allow_proto3_optional"},
+});
+```
+
+三条都是**加法**:不写等于今天的行为,逐字节相同。
+
+`mock` 单独成字段而不是让用户自己往 `protoc_args` 塞,是因为它的拼写是插件参数(`--grpc_out=generate_mock_code=true:
`)而不是 protoc 顶层参数——那正是「库该承担的知识」。
+
+## 4. L2 — 插件列表:把 `bool grpc` 升成一等公民
+
+```cpp
+grpcgen::generate_all({
+ .plugins = { grpcgen::cpp(), gateway_plugin, validate_plugin },
+});
+```
+
+`grpc = true` 变成「列表里有 `cpp()`」的语法糖,旧写法一字不改。
+
+**为什么必须做这一步**:`.proto` 的插件生态不止 gRPC——`protoc-gen-validate`、grpc-gateway、文档生成都是同一条 protoc 调用上的 `--_out`。如果不做,每来一个插件就要往 `options` 上挂一个 `bool`,而那正是 `bool grpc` 已经示范过的坏形状。
+
+一个插件的完整描述是:名字、可执行文件路径、输出目录、插件参数、以及它产出哪些文件后缀(决定 `output()` 声明)。后缀不能省——mcpp 需要知道产物才能把 `.cc` 纳入编译集、把 `.h` 排除在外。
+
+## 5. L3 — `plan` / `submit` 分离:终极逃生舱
+
+```cpp
+auto edges = grpcgen::plan_all(); // 只构造边,不提交
+for (auto& e : edges) e.arg("--whatever"); // 完全接管
+grpcgen::submit(edges);
+```
+
+价值不在「能改 flag」——L1 的 `protoc_args` 已经覆盖大半——而在**再离谱的需求也不必绕开规则**:well-known types 定位、子目录创建、输入集合计算这些规则已经解决的部分继续复用,用户只接管自己关心的那一段。
+
+这一层直接消灭第 0 节说的漂移风险。
+
+## 6. 可观测性:分工已经由引擎定死
+
+**核实过,不是推断**:`mcpp:action=` 声明的边,其完整命令行会原样落进 `build.ninja`:
+
+```
+rule mcpp_action_0
+ command = .../bin/protoc -I.../proto -I.../protobuf/src --cpp_out=... --grpc_out=...
+ description = GENERATE protoc:echo
+```
+
+因此:
+
+- **规则不该实现 `GRPCGEN_EXPLAIN` 之类的命令行 dump**。「这条边到底跑了什么」是引擎已经答完的问题,`ninja -t commands