nageyqma·git
HTTPS https://git.nageyqma.icu/nageyqma/kimi_subagent_router.git SSH git@git.nageyqma.icu:nageyqma/kimi_subagent_router.git

HTTPS 链接可直接 git clone;推送代码请使用 SSH。

仅展示默认分支 main 最近 100 条提交。

Rewrite SKILL.md into an orchestrator posture; harden setup-config adopt path
SKILL.md: - Main agent defaults to delegating: trivial whitelist is the only self-service case; no 'handle it yourself' catch-all row - k3-planner is the default thinking layer with a fast path that skips route_subagent_model; other tiers still resolve the live alias first - cheap-first tie-breaker, AgentSwarm batching with serial fallback, harness fallback to k3-planner for dotted-name rejections, user override clause setup-config: - Adopt path: when the CLI rewrites config.toml and strips the comment markers but leaves plugin-identical content, take the [secondary_model] section back into the marked block instead of refusing - Fold plugin-generated variant tables into the block during adopt so later effort edits keep flowing into [models.*] (no frozen variants) - Gate adopt on 'no [models] conflicts'; normalize newlines only at the splice seam, never across user content - Reword the route_subagent_model MCP description to match the k3-planner fast path deploy.sh: exclude __pycache__/*.pyc, protect .git/.qoder target-side Verified: fixture adopt/negative/idempotency/refresh/preservation tests, real-home migration of variant tables into the marker, deploy.sh exit 0, doctor 0 errors, py_compile clean. Reviewed by k3-planner (2 rounds).
nageyqma · 2026-09-17 · a4966ce
查看变更
README.md                       |   9 +++---
 SKILL.md                        |  37 +++++++++++++++++-------
 scripts/deploy.sh               |   4 ++-
 scripts/kimi_subagent_router.py | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
 4 files changed, 198 insertions(+), 40 deletions(-)
diff --git a/README.md b/README.md
index e617b4e..25c541e 100644
--- a/README.md
+++ b/README.md
@@ -12,9 +12,10 @@ Kimi Code CLI 的 `Agent` 工具在配置了 `[secondary_model]` 模型池后支
 
 因此本插件的派发路径为:
 
-1. 主 Agent 依据 `SKILL.md` 的路由规则选择 subagent 类型,调用 MCP 工具 `route_subagent_model` 取得当前配置的模型别名(思考强度内嵌在该别名的 `default_effort` 中)。
-2. 主 Agent 调用 `Agent(subagent_type=<类型>, model=<别名>, prompt=..., description=...)` 完成派发。
-3. 若未传 `model`,由 `[secondary_model].default_model` 承接(默认 K3-256k + max,与临时环境基准一致)。
+1. 主 Agent 以**编排器与执行器**自居:默认姿态是"这个任务该派给谁",只有完全指定、无需判断、至多一两步的琐碎任务(如 `git init`、按字面删一行)才自己做;任何设计/权衡类决策一律先派 `k3-planner` 思考定案。插件**关闭时不注入 `SKILL.md`**,`agents/*.md` 与 MCP 工具也随插件启停,表现与原生 CLI 完全一致。
+2. 主 Agent 依据 `SKILL.md` 的路由规则选择 subagent 类型,调用 MCP 工具 `route_subagent_model` 取得当前配置的模型别名(思考强度内嵌在该别名的 `default_effort` 中;`k3-planner` 档有快路径,可直接带默认别名 `k3-256k-max` 派发,跳过路由查询)。
+3. 主 Agent 调用 `Agent(subagent_type=<类型>, model=<别名>, prompt=..., description=...)` 完成派发。
+4. 若未传 `model`,由 `[secondary_model].default_model` 承接(默认 K3-256k + max,与临时环境基准一致)。
 
 默认路由表(主 Agent 自身保持轻量,快速判断后派发):
 
@@ -55,7 +56,7 @@ Kimi Code CLI 的 `Agent` 工具在配置了 `[secondary_model]` 模型池后支
 kimi-subagent-router/
 ├── kimi.plugin.json      # 插件清单(CLI 实际读取;plugin.json 为同步副本)
 ├── plugin.json           # 与 kimi.plugin.json 内容一致(交付物命名要求)
-├── SKILL.md              # 路由规则,会话启动时自动加载给主 Agent
+├── SKILL.md              # 派发姿态与路由规则,会话启动时自动加载给主 Agent
 ├── subagent-models.json  # 用户可编辑的映射配置
 ├── README.md
 ├── agents/               # 七个 subagent 定义(插件级 agent,自动被发现)
diff --git a/SKILL.md b/SKILL.md
index 39a1fe6..042c0d9 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,15 +1,29 @@
 ---
 name: subagent-model-routing
-description: Model and thinking-tier routing for subagent dispatches. Consult before every Agent or AgentSwarm call, pick a subagent type from the table, then resolve its live model alias with route_subagent_model.
+description: Default dispatch posture and model/thinking-tier routing for subagent dispatches. The main agent is an orchestrator — delegate by default, self-serve only trivial glue, and resolve model aliases with route_subagent_model (fast path for k3-planner).
 ---
 
 # Subagent model routing
 
-Every subagent type is bound to a model variant alias whose registered `default_effort` sets its thinking tier. Route mechanical work to the cheap alias and reasoning-heavy work to the expensive one. The live mapping lives in `subagent-models.json` and the user can edit it at any time, so use the table below only to pick a **type**; always resolve the **alias** with the routing tool.
+When this plugin is enabled, the main agent is an **orchestrator and executor, not the thinker**. The default reaction to any incoming task is "which subagent should handle this?" — never "can I do it myself?". Delegation is the default; self-service is the exception, allowed only for the trivial whitelist below. This keeps the main context lean and spends strong-model tokens only where reasoning actually happens.
+
+## 0. Trivial whitelist — the only self-service cases
+
+Handle a task yourself ONLY when ALL of these hold:
+
+- It is fully specified with no design judgment involved (e.g. `git init`, delete one exact line as literally requested, fix a single obvious typo);
+- It touches at most one file and needs at most 1–2 tool calls;
+- Or it is a pure lookup answerable with a single read/command (e.g. "what does flag X do", "show the latest commit").
+
+Everything else — exploration, implementation, verification, documentation, and any decision involving trade-offs — gets dispatched. There is deliberately NO "handle it yourself" catch-all row: when in doubt, dispatch.
+
+A second efficiency rule: subagents also absorb bulk reading and searching, so prefer dispatching an explore agent over reading large files into the main context yourself.
+
+User override: when the user explicitly tells you to do something directly yourself, comply — a direct user instruction takes precedence over this skill.
 
 ## 1. Plan first, then dispatch
 
-Unless the whole request is trivial — something you can finish yourself in one or two tool calls — do **not** start executing directly. First dispatch `k3-planner` (K3-256k, max) to plan, then execute the plan subtask by subtask.
+Unless the task is on the trivial whitelist in section 0, do **not** start executing directly. First dispatch `k3-planner` (K3-256k, max) to plan, then execute the plan subtask by subtask.
 
 **Planning brief** must demand a plan that contains, for every subtask:
 
@@ -20,20 +34,21 @@ Unless the whole request is trivial — something you can finish yourself in one
 
 The planning call must also state the recommended tier per subtask (from the table below) and an overall verification process. Keep the returned plan in your context — it is small — and quote each subtask's boundary and verification steps **verbatim** in the executor brief when you dispatch it.
 
-Skip planning only when the task is trivially small, or when the user explicitly says to just do it. Read-only questions you can answer with one lookup need no plan either.
-
 ## 2. Pick a type — first matching row wins
 
 | Task | `subagent_type` | Default alias | Effort |
 |---|---|---|---|
 | Mechanical, pattern-fixed edits: bulk rename, formatting, boilerplate, trivial test stubs | `cheap-coder` | `k2.8-high` | high |
-| Read-only lookup: find files, read structure, search symbols, nothing changes | `cheap-explore` | `k2.8-low` | low |
+| Read-only lookup: find files, read structure, search symbols, absorb large-file reading | `cheap-explore` | `k2.8-low` | low |
 | Local change scoped to a single file | `k2.8-medium` | `k2.8-high` | high |
 | Simple feature, routine tests, debugging where the symptom is already known | `k2.8-debug` | `k2.8-max` | max |
 | Complex design, architecture, cross-module refactor, non-obvious bug | `k3-planner` | `k3-256k-max` | max |
 | Hardest task **and** the subtask needs more than 256k of context (huge repo sweep, long logs, big files) | `k3-max` | `k3-max` (kimi-code/k3, 1M) | max |
 | Documentation: README, API docs, usage guides, comment cleanup | `doc-writer` | `alibaba-cn/qwen3.8-flash` | xhigh (third-party tier) |
-| Anything else | do not dispatch — handle it yourself | — | — |
+
+If nothing matches and the task is not on the trivial whitelist, escalate upward (usually `k2.8-debug`; design-heavy → `k3-planner`) — still do not self-execute.
+
+Harness fallback: if the `Agent` tool rejects a `subagent_type` as unknown (some CLI versions refuse dotted names like `k2.8-debug`, see README known-limitation 4), escalate to `k3-planner` instead of self-executing.
 
 ## 3. Tie-breakers
 
@@ -41,7 +56,7 @@ Skip planning only when the task is trivially small, or when the user explicitly
 2. **K3-256k by default.** `k3-planner` and `k3-max` both think at `max`; they differ only in window (256k vs 1M) and `k3-max` costs roughly double. Upgrade only when *both* conditions hold: the task is the hardest kind **and** you expect >256k of context. Unsure means `k3-planner`.
 3. **Debugging splits in two.** Known symptom, bounded scope → `k2.8-debug`. Strange behavior, unknown root cause, deep cross-module interaction → `k3-planner`.
 4. **Type names are not tier names.** `k2.8-debug` is K2.8 at `max` (simple features, routine tests, routine debugging); `k2.8-medium` is the same model at `high`.
-5. **Trivial work gets no subagent.** If it takes you one or two tool calls, do it yourself — dispatch overhead costs more than the task.
+5. **Cheap first.** When a task could plausibly be done by a cheaper tier and a failure is cheap to detect, prefer the cheaper tier and verify the result; escalate only on failure. Reserve `k3-planner` for work whose failure mode is expensive (wrong design, subtle bug).
 
 ## 4. Write the brief to fit the tier
 
@@ -62,13 +77,13 @@ Every brief must also carry: the working directory or concrete paths, what you a
 
 ## 5. Dispatch, in this order
 
-1. Call `route_subagent_model` with `subagent_type` set to the row's type; it returns the live `model` alias and `effort`.
-2. Call `Agent` with **both** `subagent_type` (the row's type, e.g. `k3-planner`) and `model` (the alias from step 1 — it must be a pool alias), plus `description` (3-5 words) and `prompt` (the brief). If step 1 returned `model: null`, omit `model` so `[secondary_model].default_model` takes over.
+1. For **`k3-planner`** — the default thinking layer — use the fast path: dispatch directly with `Agent(subagent_type="k3-planner", model="k3-256k-max", description=..., prompt=...)`. Skip `route_subagent_model`. Only resolve the live alias for `k3-planner` when the user has said they edited the mapping, or a dispatch failed because the alias is unknown.
+2. For **all other types**, call `route_subagent_model` with the row's `subagent_type`; it returns the live `model` alias and `effort`. Then call `Agent` with **both** `subagent_type` and `model` (the alias from the routing tool — it must be a pool alias), plus `description` (3-5 words) and `prompt` (the brief). If the routing tool returned `model: null`, omit `model` so `[secondary_model].default_model` takes over.
 3. There is no other way to set the thinking tier: the `Agent` tool has **no** `effort` parameter and the tier comes only from the alias's `default_effort`. If the resolved tier clearly does not fit the task, tell the user to fix `subagent-models.json`; do not switch aliases on your own.
+4. Batch independent same-tier dispatches with `AgentSwarm` instead of serializing them. If `AgentSwarm` is unavailable, dispatch serially.
 
 ## Constraints
 
 - No nested delegation: these agents ship with `Agent`/`AgentSwarm` disabled, so the chain stops at depth 1.
 - `route_subagent_model` reporting an unknown type → call `list_subagent_routes` for the live names; the user may have added or renamed entries.
-- A large batch of the *same* operation can use `AgentSwarm` with the same `subagent_type` + `model`, as long as every subtask stays in one tier.
 - `doc-writer` points at a third-party model that must be registered in `config.toml`. If routing to it fails, report that to the user instead of silently substituting another tier.
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
index 91f4a82..73bb6cb 100755
--- a/scripts/deploy.sh
+++ b/scripts/deploy.sh
@@ -49,8 +49,10 @@ info "源目录:$SRC_DIR"
 info "目标目录:$ROOT"
 
 # 3. 同步文件(镜像源仓库,排除 .git/.qoder/.gitignore,与现有部署副本一致)
-rsync -a --delete \
+rsync -a --delete --delete-excluded \
+    --filter='protect .git/' --filter='protect .qoder/' \
     --exclude='.git/' --exclude='.qoder/' --exclude='.gitignore' \
+    --exclude='__pycache__/' --exclude='*.pyc' \
     --itemize-changes --out-format='  %n' \
     "$SRC_DIR/" "$ROOT/" | { grep -v '^  \./$' || true; }
 
diff --git a/scripts/kimi_subagent_router.py b/scripts/kimi_subagent_router.py
index ee95c21..c6823c3 100755
--- a/scripts/kimi_subagent_router.py
+++ b/scripts/kimi_subagent_router.py
@@ -298,6 +298,27 @@ def render_model_alias(alias, src, effort, warnings):
     return out
 
 
+def _resolve_variant_src(alias, existing_models):
+    """变体 [models.<alias>] 条目的字段来源解析(generate_block 与 adopt 折叠路径共用)。
+
+    返回 (src, status):src 为可用的字段 dict 时 status 是 "existing"(来自 config.toml
+    已有条目)或 "fallback"(内置模板);src 为 None 时 status 是 "not-variant" /
+    "incomplete" / "missing" / "no-template",调用方据此前缀决定警告与是否跳过。"""
+    base_model_id = VARIANT_BASE_MODEL.get(alias)
+    if not base_model_id:
+        return None, "not-variant"
+    src = None
+    for _, fields in (existing_models or {}).items():
+        if fields.get("model") == base_model_id:
+            src = fields
+            break
+    if src is not None and not (src.get("provider") and src.get("max_context_size")):
+        return BUILTIN_MODEL_FALLBACKS.get(base_model_id), "incomplete"
+    if src is None:
+        return BUILTIN_MODEL_FALLBACKS.get(base_model_id), "missing"
+    return src, "existing"
+
+
 def generate_block(routes, existing_models):
     """生成可并入 config.toml 的标记块。返回 (text, warnings)。"""
     warnings = []
@@ -312,29 +333,21 @@ def generate_block(routes, existing_models):
     for alias, info in used.items():
         if alias in existing_models:
             continue
-        base_model_id = VARIANT_BASE_MODEL.get(alias)
-        if not base_model_id:
+        src, status = _resolve_variant_src(alias, existing_models)
+        if status == "not-variant":
             warnings.append(
                 f"别名 {alias} 未在 [models] 注册,且不是内置托管变体;无法自动生成。"
                 "请先按 README「切换到第三方模型」手动注册该别名。")
             continue
-        src = None
-        for _, fields in (existing_models or {}).items():
-            if fields.get("model") == base_model_id:
-                src = fields
-                break
-        if src is not None and not (src.get("provider") and src.get("max_context_size")):
+        if status == "incomplete":
             warnings.append(
-                f"config.toml 中 {base_model_id} 的已有条目缺少 provider/max_context_size 字段,"
+                f"config.toml 中 {VARIANT_BASE_MODEL[alias]} 的已有条目缺少 provider/max_context_size 字段,"
                 f"别名 {alias} 已改用插件内置字段模板,请核对 max_context_size。")
-            src = None
-        elif src is None:
+        elif status == "missing":
             warnings.append(
-                f"config.toml 中未找到底层模型 {base_model_id} 的条目,别名 {alias} 已使用"
+                f"config.toml 中未找到底层模型 {VARIANT_BASE_MODEL[alias]} 的条目,别名 {alias} 已使用"
                 "插件内置字段模板,请核对 max_context_size。")
         if src is None:
-            src = BUILTIN_MODEL_FALLBACKS.get(base_model_id)
-        if not (src and src.get("provider") and src.get("model") and src.get("max_context_size")):
             warnings.append(f"别名 {alias} 缺少可用的字段模板,已跳过生成。")
             continue
         lines += render_model_alias(alias, src, info["effort"], warnings)
@@ -553,6 +566,124 @@ def _backup(path):
     return bak
 
 
+def _expected_variant_entry(alias, search_space, effort):
+    """adopt 折叠路径:generate_block 在 search_space(existing 去掉已折叠候选后的样子)
+    下会为 alias 生成的 [models.<alias>] 内容,以解析后的 dict 表示;无法生成时为 None。"""
+    src, status = _resolve_variant_src(alias, search_space)
+    if src is None or status == "not-variant":
+        return None
+    entry = {"provider": src["provider"], "model": src["model"],
+             "max_context_size": int(src["max_context_size"])}
+    caps = src.get("capabilities") or []
+    if caps:
+        entry["capabilities"] = list(caps)
+    se = src.get("support_efforts") or []
+    if se:
+        entry["support_efforts"] = list(se)
+    if effort and (not se or effort in se):
+        entry["overrides"] = {"default_effort": effort}
+    return entry
+
+
+def _variant_table_span(lines, alias):
+    """text 中 [models.<alias>] 表(含可选 [models.<alias>.overrides] 子表)的行区间。
+    结束于下一个不以该表头前缀开头的 '[' 行;找不到表头时为 None。"""
+    key = toml_key(alias)
+    head = re.compile(r'^\[models\.' + re.escape(key) + r'(\.|\])')
+    start = None
+    for i, line in enumerate(lines):
+        if head.match(line):
+            start = i
+            break
+    if start is None:
+        return None
+    end = len(lines)
+    for j in range(start + 1, len(lines)):
+        if lines[j].startswith("[") and not head.match(lines[j]):
+            end = j
+            break
+    return start, end
+
+
+def _collapse_seam(prefix, suffix):
+    """只折叠拼接缝处的多余空行(prefix 尾部 / suffix 头部),不触碰文件其余内容。
+    返回归一化后的 (prefix, suffix)。"""
+    return (re.sub(r"\n{3,}$", "\n\n", prefix),
+            re.sub(r"^\n{2,}", "\n", suffix))
+
+
+def strip_identical_variant_tables(text, existing, routes):
+    """把与本插件生成内容完全一致的未标记 [models.<variant>] 表从 text 中移除,并从
+    existing 中剔除,使其随后由 generate_block 在标记块内重新生成;用户改过的表保持
+    不动,避免被冻结在标记块之外。返回 (new_text, new_existing, folded_aliases)。"""
+    used = collect_aliases(routes)
+    folded = {a for a in used if a in (existing or {}) and a in VARIANT_BASE_MODEL}
+    if not folded:
+        return text, existing, []
+    # 固定点:期望内容须按 generate_block 实际运行时的来源解析(在"existing 去掉全部
+    # 已折叠候选"后)计算;某候选不一致时逐出候选集并重算,直到稳定。
+    while True:
+        search_space = {k: v for k, v in (existing or {}).items() if k not in folded}
+        mismatch = [a for a in sorted(folded)
+                    if _expected_variant_entry(a, search_space, used[a]["effort"]) != existing.get(a)]
+        if not mismatch:
+            break
+        folded -= set(mismatch)
+    if not folded:
+        return text, existing, []
+    lines = text.splitlines(keepends=True)
+    spans = []
+    for alias in folded:
+        span = _variant_table_span(lines, alias)
+        if span is None:
+            folded.discard(alias)  # parsed 有表但文本找不到表头(理论不会发生):不折
+            continue
+        spans.append(span)
+    if not folded:
+        return text, existing, []
+    for start, end in sorted(spans, reverse=True):
+        prefix, suffix = _collapse_seam("".join(lines[:start]), "".join(lines[end:]))
+        lines = (prefix + suffix).splitlines(keepends=True)
+    new_existing = {k: v for k, v in (existing or {}).items() if k not in folded}
+    return "".join(lines), new_existing, sorted(folded)
+
+
+def try_adopt_secondary_model(text, parsed, existing, routes, block, warnings):
+    """接管路径:外部工具(如 CLI 设置写入)重写 config.toml 时会剥掉标记注释、
+    但保留段内容。若未标记的 [secondary_model] 段与本插件将生成的内容完全一致,
+    则同时把与本插件生成内容一致的未标记 [models.<variant>] 表折叠回标记块内,
+    移除旧段并在原位置接入完整标记块。返回 (new_text, action, warnings),否则 None。"""
+    try:
+        import tomllib
+        block_sm = tomllib.loads(block).get("secondary_model")
+    except Exception:  # noqa: BLE001 - 生成块理论上必然可解析,解析失败则保持拒绝
+        return None
+    if block_sm is None:
+        return None
+    if extract_secondary_model(parsed, text) != block_sm:
+        return None
+    text, existing, folded = strip_identical_variant_tables(text, existing, routes)
+    if folded:
+        # 变体已移出 existing,重新生成块使其回到标记块内([secondary_model] 池内容不变)。
+        block, warnings = generate_block(routes, existing)
+    lines = text.splitlines(keepends=True)
+    start = None
+    for i, line in enumerate(lines):
+        if re.match(r'^\[secondary_model\]', line):
+            start = i
+            break
+    if start is None:
+        return None
+    end = len(lines)
+    for j in range(start + 1, len(lines)):
+        if lines[j].startswith("[") and not lines[j].startswith("[secondary_model"):
+            end = j
+            break
+    prefix, suffix = _collapse_seam("".join(lines[:start]), "".join(lines[end:]))
+    new_text = prefix + block + suffix
+    return new_text, "已接管被外部工具重写的 [secondary_model] 段(内容与本插件生成的一致)", warnings
+
+
 def cmd_setup_config(args):
     routes, source, _ = load_routes(args.home)
     cfg_path = config_toml_path(args.home)
@@ -588,14 +719,20 @@ def cmd_setup_config(args):
                 conflict_models.append(alias)
         has_sm = re.search(r'^\[secondary_model(\.|\])', base_text, re.M)
         if conflict_models or has_sm:
-            print("检测到现有配置冲突,为避免破坏 config.toml,未写入。请手动合并:", file=sys.stderr)
-            print(f"  冲突: [secondary_model] 段已存在: {bool(has_sm)}; 冲突别名: {conflict_models}", file=sys.stderr)
-            print("  可手动添加以下片段(放在已有 [secondary_model] 与 [models] 表中对应位置):", file=sys.stderr)
-            sys.stderr.write(block)
-            return 2
-        header = "" if not text.strip() else ("\n" if not text.endswith("\n") else "\n")
-        new_text = text + header + "\n" + block
-        action = "已追加标记块"
+            adopted = None
+            if has_sm and parsed is not None and not conflict_models:
+                adopted = try_adopt_secondary_model(base_text, parsed, existing, routes, block, warnings)
+            if adopted is None:
+                print("检测到现有配置冲突,为避免破坏 config.toml,未写入。请手动合并:", file=sys.stderr)
+                print(f"  冲突: [secondary_model] 段已存在: {bool(has_sm)}; 冲突别名: {conflict_models}", file=sys.stderr)
+                print("  可手动添加以下片段(放在已有 [secondary_model] 与 [models] 表中对应位置):", file=sys.stderr)
+                sys.stderr.write(block)
+                return 2
+            new_text, action, warnings = adopted
+        else:
+            header = "" if not text.strip() else ("\n" if not text.endswith("\n") else "\n")
+            new_text = text + header + "\n" + block
+            action = "已追加标记块"
 
     bak = _backup(cfg_path) if os.path.isfile(cfg_path) else None
     try:
@@ -666,8 +803,11 @@ MCP_TOOLS = [
         "name": "route_subagent_model",
         "description": (
             "Resolve a subagent type to the model alias and thinking tier to dispatch it with, plus ready-to-use "
-            "Agent tool arguments. Call this immediately before every Agent/AgentSwarm dispatch: the mapping lives in "
-            "subagent-models.json and the user may have edited it, so never dispatch from a remembered alias. "
+            "Agent tool arguments. Call this immediately before every Agent/AgentSwarm dispatch for all subagent "
+            "types EXCEPT k3-planner: the k3-planner fast path dispatches directly with model \"k3-256k-max\" and "
+            "skips this tool, resolving live only when the user said they edited the mapping or a dispatch failed "
+            "with an unknown subagent type. For every other type the mapping lives in subagent-models.json and the "
+            "user may have edited it, so never dispatch from a remembered alias. "
             "The Agent tool has no effort parameter — the thinking tier is bound to the returned alias's default_effort."),
         "inputSchema": {
             "type": "object",
Add one-click deploy script; pin default_model to k3-256k-max
- scripts/deploy.sh: rsync repo to the installed plugin dir (resolved from installed.json, with safety checks), rerun idempotent setup-config, gate on doctor, prompt /reload - subagent-models.json: set top-level default_model to k3-256k-max to remove the setup-config 'default_model is empty' fallback warning (effective pool default unchanged) - README: document redeploy flow and reword the default_model field row
nageyqma · 2026-09-17 · 7154ddb
查看变更
README.md            | 15 +++++++++++++--
 scripts/deploy.sh    | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 subagent-models.json |  2 +-
 3 files changed, 84 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 53d413e..e617b4e 100644
--- a/README.md
+++ b/README.md
@@ -67,7 +67,8 @@ kimi-subagent-router/
 │   ├── k3-max.md
 │   └── doc-writer.md     # 排除 shell 与后台任务工具
 └── scripts/
-    └── kimi_subagent_router.py   # 路由工具:MCP 服务 + CLI(Python 3 标准库,无依赖)
+    ├── kimi_subagent_router.py   # 路由工具:MCP 服务 + CLI(Python 3 标准库,无依赖)
+    └── deploy.sh                 # 一键部署脚本:同步到插件安装目录 + setup-config + doctor
 ```
 
 ## 安装
@@ -96,6 +97,16 @@ python3 /path/to/kimi-subagent-router/scripts/kimi_subagent_router.py doctor
 - 写入前总是先备份为 `config.toml.kimi-subagent-router.bak-<时间戳>`;生成的 TOML 经解析校验后才落盘,失败自动回滚。
 - 卸载:`python3 scripts/kimi_subagent_router.py remove-config`(只删标记块,不碰其他配置),再 `/plugins remove kimi-subagent-router`。
 
+## 重新部署
+
+维护修改本仓库后,一键同步到已安装的插件目录并刷新配置:
+
+```bash
+bash scripts/deploy.sh
+```
+
+脚本自动完成:定位插件安装目录(读 `~/.kimi-code/plugins/installed.json`,失败回退默认路径)→ `rsync` 镜像同步(排除 `.git`/`.qoder`,目标缺失或非插件目录时中止)→ 重跑 `setup-config`(幂等,自动备份 config.toml)→ 运行 `doctor` 验证(有错误则非零退出)。完成后在 Kimi Code TUI 中执行 `/reload` 或 `/new` 生效。
+
 ## 修改映射
 
 编辑 `subagent-models.json`(插件根目录下的副本,或复制到 `~/.kimi-code/subagent-models.json` 以获得更高优先级——查找顺序:`$KIMI_SUBAGENT_MODELS` 环境变量 > `$KIMI_CODE_HOME/subagent-models.json` > 插件目录 > 内置默认值;每次工具调用都会重新读取,改完立即生效,无需重启)。
@@ -109,7 +120,7 @@ python3 /path/to/kimi-subagent-router/scripts/kimi_subagent_router.py doctor
 | `subagents.<名称>.subagent_type` | 该条目继承的内置类型语义(`coder`/`explore`/`plan`),供阅读与工具策略参考 |
 | `subagents.<名称>.exclude_tools` | 高风险工具排除说明(信息性;实际强制在 `agents/*.md` 的 `tools`/`disallowedTools`) |
 | `subagents.<名称>.description` | 职责描述,会随路由工具返回进入主 Agent 上下文(另见「提示词语言约定」:用英文) |
-| `default_model` | 顶层兜底:当某条目 `model` 为 `null` 时用它;再缺省则由 `setup-config` 写入的池默认值(`k3-256k-max`)承接 |
+| `default_model` | 顶层兜底:当某条目 `model` 为 `null` 时用它(本仓库默认为 `"k3-256k-max"`);若该字段本身缺失或为 `null`,`setup-config` 回退到内置池默认值 `k3-256k-max` 并打印警告 |
 
 可自由增删条目:新增一个 subagent 名称 + 对应 `agents/<名称>.md` agent 文件即可;自定义模型别名若是第三方模型,按下一节注册。
 
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
new file mode 100755
index 0000000..91f4a82
--- /dev/null
+++ b/scripts/deploy.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+# kimi-subagent-router 一键部署脚本
+# 将仓库文件同步到 Kimi Code 插件安装目录,并重新执行 setup-config + doctor。
+# 幂等,可重复运行。用法:bash scripts/deploy.sh
+set -euo pipefail
+
+PLUGIN_ID="kimi-subagent-router"
+DEFAULT_ROOT="$HOME/.kimi-code/plugins/managed/kimi-subagent-router"
+INSTALLED_JSON="$HOME/.kimi-code/plugins/installed.json"
+
+# 仓库根目录(本脚本位于 scripts/ 下)
+SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+info() { printf '[部署] %s\n' "$*"; }
+err()  { printf '[错误] %s\n' "$*" >&2; }
+
+# 1. 定位部署目标:优先读 installed.json 的 root 字段,失败回退默认路径
+ROOT=""
+if [ -f "$INSTALLED_JSON" ]; then
+    ROOT="$(python3 - "$INSTALLED_JSON" "$PLUGIN_ID" <<'PY'
+import json, sys
+with open(sys.argv[1], encoding="utf-8") as f:
+    data = json.load(f)
+for p in data.get("plugins", []):
+    if p.get("id") == sys.argv[2]:
+        print(p.get("root") or "")
+        break
+PY
+)" || ROOT=""
+fi
+[ -n "$ROOT" ] || ROOT="$DEFAULT_ROOT"
+
+# 2. 安全检查:目标必须已存在且是插件安装目录(防止 rsync --delete 误删)
+if [ ! -d "$ROOT" ]; then
+    err "插件安装目录不存在:$ROOT"
+    err "请先安装插件:/plugins install <仓库路径> && /plugins enable $PLUGIN_ID"
+    exit 1
+fi
+if [ ! -f "$ROOT/kimi.plugin.json" ]; then
+    err "$ROOT 下没有 kimi.plugin.json,不像插件安装目录,已中止以防误删文件。"
+    exit 1
+fi
+if [ "$ROOT" = "$SRC_DIR" ]; then
+    err "部署目标与源仓库相同($ROOT),无需部署。"
+    exit 0
+fi
+
+info "源目录:$SRC_DIR"
+info "目标目录:$ROOT"
+
+# 3. 同步文件(镜像源仓库,排除 .git/.qoder/.gitignore,与现有部署副本一致)
+rsync -a --delete \
+    --exclude='.git/' --exclude='.qoder/' --exclude='.gitignore' \
+    --itemize-changes --out-format='  %n' \
+    "$SRC_DIR/" "$ROOT/" | { grep -v '^  \./$' || true; }
+
+info "文件同步完成。"
+
+# 4. 重新注册模型变体并刷新 [secondary_model] 池(幂等,内部自动备份 config.toml)
+info "执行 setup-config ..."
+python3 "$ROOT/scripts/kimi_subagent_router.py" setup-config
+
+# 5. 一致性检查
+info "执行 doctor ..."
+if ! python3 "$ROOT/scripts/kimi_subagent_router.py" doctor; then
+    err "doctor 报告错误,请按上方提示修复后重跑本脚本。"
+    exit 1
+fi
+
+info "部署完成。请在 Kimi Code TUI 中执行 /reload 或 /new 使更新生效。"
diff --git a/subagent-models.json b/subagent-models.json
index 983002e..82ff2c4 100644
--- a/subagent-models.json
+++ b/subagent-models.json
@@ -44,5 +44,5 @@
       "description": "Documentation: README, API docs, prose and comment cleanup (third-party alibaba-cn/qwen3.8-flash, xhigh tier)"
     }
   },
-  "default_model": null
+  "default_model": "k3-256k-max"
 }
Initial commit: kimi-subagent-router plugin
Per-subagent-type model routing for Kimi Code CLI: route table + MCP router tool + setup/doctor CLI, seven agent definitions, editable subagent-models.json, English SKILL.md with plan-first workflow. Co-authored-by: Kimi Code
nageyqma · 2026-09-17 · 226f7d8
查看变更
.gitignore                      |   3 +
 README.md                       | 192 +++++++++++++++++++++++++++
 SKILL.md                        |  74 +++++++++++
 agents/cheap-coder.md           |  31 +++++
 agents/cheap-explore.md         |  29 ++++
 agents/doc-writer.md            |  35 +++++
 agents/k2.8-debug.md            |  33 +++++
 agents/k2.8-medium.md           |  24 ++++
 agents/k3-max.md                |  33 +++++
 agents/k3-planner.md            |  35 +++++
 kimi.plugin.json                |  20 +++
 plugin.json                     |  20 +++
 scripts/kimi_subagent_router.py | 872 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 subagent-models.json            |  48 +++++++
 14 files changed, 1449 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1d7eff8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.qoder/
+__pycache__/
+*.pyc
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..53d413e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,192 @@
+# Kimi Subagent Router(kimi-subagent-router)
+
+为 Kimi Code CLI 提供**按 subagent 类型自动路由不同模型与思考强度**的插件,解决 `[secondary_model]` 只能设置一个全局默认模型、无法按任务复杂度分发的问题。
+
+- 默认值只使用 **Kimi Code 内置模型**(K2.8 Preview、K3-256k),不假设已接入任何第三方模型。
+- 所有映射关系**可在 `subagent-models.json` 中配置**,后期可切换到第三方订阅模型。
+- 同一模型的不同思考强度(如 K2.8 high 与 K2.8 max)通过**模型变体别名**实现。
+
+## 工作原理
+
+Kimi Code CLI 的 `Agent` 工具在配置了 `[secondary_model]` 模型池后支持 `model` 参数(池别名或 `"primary"`),但**没有 effort 参数**。思考强度按官方机制由模型变体别名的 `default_effort` 绑定:变体是 `[models]` 中指向同一底层模型的独立条目,用 `[models."<别名>".overrides].default_effort` 覆盖档位(变体不继承被指向条目的字段,`capabilities`、`support_efforts` 必须完整照抄)。对 subagent 而言,变体的 `default_effort` 优先于全局 `[thinking].effort`,只有 `[secondary_model].default_effort` 优先级更高——**所以不要设置 `[secondary_model].default_effort`**。
+
+因此本插件的派发路径为:
+
+1. 主 Agent 依据 `SKILL.md` 的路由规则选择 subagent 类型,调用 MCP 工具 `route_subagent_model` 取得当前配置的模型别名(思考强度内嵌在该别名的 `default_effort` 中)。
+2. 主 Agent 调用 `Agent(subagent_type=<类型>, model=<别名>, prompt=..., description=...)` 完成派发。
+3. 若未传 `model`,由 `[secondary_model].default_model` 承接(默认 K3-256k + max,与临时环境基准一致)。
+
+默认路由表(主 Agent 自身保持轻量,快速判断后派发):
+
+| 任务 | subagent | 模型别名 | 思考强度 |
+|---|---|---|---|
+| 重复性机械操作 | `cheap-coder` | `k2.8-high`(K2.8 Preview) | high |
+| 只读探索 | `cheap-explore` | `k2.8-low` | low |
+| 单文件修改等局部改动 | `k2.8-medium` | `k2.8-high` | high |
+| 简单功能实现、常规测试、常规调试排错 | `k2.8-debug` | `k2.8-max` | max |
+| 复杂构思/架构/跨模块重构/非显而易见 bug | `k3-planner` | `k3-256k-max`(K3-256k,**优先使用**) | max |
+| 最困难任务**且**预估上下文超过 256k | `k3-max` | `k3-max`(kimi-code/k3,**1M 上下文**) | max |
+| 文档编写 | `doc-writer` | `alibaba-cn/qwen3.8-flash`(第三方,需先注册) | xhigh |
+
+档位设计要点:**尽量用 K3-256k**——`k3-planner` 与 `k3-max` 思考强度都是 max,只有"最困难任务且预估上下文超过 256k"两个条件同时成立时才升级 1M 的 `k3-max`,其余复杂任务一律 `k3-planner`。K2.8 一侧按推理量细分:局部单文件修改走 `k2.8-medium`(high),简单功能实现/常规测试/常规调试排错走 `k2.8-debug`(max)。`doc-writer` 默认路由到第三方模型 `alibaba-cn/qwen3.8-flash`(OpenAI 兼容,xhigh 档位),使用前需按「切换到第三方模型」一节完成注册。
+
+若主 Agent 或某个 subagent 想用其它档位,把 `subagent-models.json` 中对应条目的 `model`/`effort` 改为 `k2.8-low`/`k2.8-high`/`k2.8-max`/`k3-256k-high` 等即可;这些内置变体 `setup-config` 都会自动注册。
+
+## 提示词语言约定
+
+模型实际读到的文本一律用英文:`SKILL.md`、`agents/*.md` 的 frontmatter 与正文、`subagent-models.json` 的 `description`、MCP 工具描述与返回里的 `notes`/`dispatch` 模板、以及 `setup-config` 写进 `[secondary_model.models]` 的别名说明。中文提示词会让模型先内部翻译一遍再执行,既占上下文又降低指令服从度。边界按"谁会读这段文字"划:配置解析错误、`doctor` 的条目正文(`[错误] doc-writer: 模型别名 … 未注册`)这类**指向人的修复指引**保持中文——它们的终点是用户去改 `config.toml`,主 Agent 只是转述;而 `doctor --json` 的 `level` 字段与全部路由结果、工具返回是模型要据此行动的,一律英文。`--help`、`setup-config` 的提示同样保持中文。
+
+`agents/*.md` 的写法按各档位模型的习性调过:
+
+| 档位(模型) | 模型特点 | 提示词对策 |
+|---|---|---|
+| `cheap-explore`(K2.8 low) | 不会自己补全意图,容易一次命中就收工 | 编号步骤、检索预算(约 15 次调用)、固定输出格式,禁止开放式判断 |
+| `cheap-coder` / `k2.8-medium`(K2.8 high) | 高思考档最爱"顺手改"与过度设计 | 范围锁定、字面执行、改完做机械性验证(旧 token 零命中) |
+| `k2.8-debug`(K2.8 max) | 推理预算足,但会写成一篇长文 | 复现→定位→单假设证伪→根因修复的循环;思考留在思考里,结论按格式简短交付 |
+| `k3-planner`(K3-256k max) | 长链推理强,容易输出方案菜单与"待验证" | 要求定案一个推荐+被否方案各一行;无 shell,改为移交验证命令 |
+| `k3-max`(k3 1M max) | 上下文是它存在的理由,但要防读散 | 先 Glob/Grep 建地图再大块读,每条结论附 `path:line` |
+| `doc-writer`(qwen3.8-flash xhigh) | 文笔好,但易浮夸堆表情、把没读过的细节写得很像真的 | 代码为唯一事实来源、风格禁则、沿用仓库语言(中文文档保持中文) |
+
+`doc-writer` 同时被明令:不确定的内容写 `TODO: verify`,宁缺毋造。
+
+## 目录结构
+
+```
+kimi-subagent-router/
+├── kimi.plugin.json      # 插件清单(CLI 实际读取;plugin.json 为同步副本)
+├── plugin.json           # 与 kimi.plugin.json 内容一致(交付物命名要求)
+├── SKILL.md              # 路由规则,会话启动时自动加载给主 Agent
+├── subagent-models.json  # 用户可编辑的映射配置
+├── README.md
+├── agents/               # 七个 subagent 定义(插件级 agent,自动被发现)
+│   ├── cheap-coder.md    # 排除后台任务管理与 Plan 模式进入
+│   ├── cheap-explore.md  # 只读工具白名单
+│   ├── k2.8-medium.md
+│   ├── k2.8-debug.md     # 中等难度调试/排错(K2.8 max)
+│   ├── k3-planner.md     # 无 shell,可读可写
+│   ├── k3-max.md
+│   └── doc-writer.md     # 排除 shell 与后台任务工具
+└── scripts/
+    └── kimi_subagent_router.py   # 路由工具:MCP 服务 + CLI(Python 3 标准库,无依赖)
+```
+
+## 安装
+
+要求:Python 3.8+(脚本仅用标准库);Kimi Code CLI 已 `/login`(内置托管模型别名由登录流程注入)。
+
+```bash
+# 1. 安装并启用插件(在 Kimi Code TUI 中)
+/plugins install /path/to/kimi-subagent-router   # 也可用 zip URL / GitHub URL
+/plugins enable kimi-subagent-router
+
+# 2. 注册模型变体并写入 [secondary_model] 池(幂等,先备份 config.toml)
+python3 /path/to/kimi-subagent-router/scripts/kimi_subagent_router.py setup-config
+
+# 3. 检查一致性,然后激活
+python3 /path/to/kimi-subagent-router/scripts/kimi_subagent_router.py doctor
+# 在 TUI 中执行 /reload 或 /new
+```
+
+也可手动安装:把 `scripts/kimi_subagent_router.py print-config` 打印的 TOML 片段并入 `~/.kimi-code/config.toml`(变体条目 + `[secondary_model]` 段),效果相同。
+
+`setup-config` 行为说明:
+
+- 片段包在 `# >>> kimi-subagent-router >>>` 标记内,重复运行会原地更新(幂等);改动映射后重跑一次即可。
+- 若你的 `config.toml` 已存在 `[secondary_model]` 段或冲突的 `[models]` 条目,脚本**拒绝写入**并打印需手动合并的片段(不会破坏现有配置)。
+- 写入前总是先备份为 `config.toml.kimi-subagent-router.bak-<时间戳>`;生成的 TOML 经解析校验后才落盘,失败自动回滚。
+- 卸载:`python3 scripts/kimi_subagent_router.py remove-config`(只删标记块,不碰其他配置),再 `/plugins remove kimi-subagent-router`。
+
+## 修改映射
+
+编辑 `subagent-models.json`(插件根目录下的副本,或复制到 `~/.kimi-code/subagent-models.json` 以获得更高优先级——查找顺序:`$KIMI_SUBAGENT_MODELS` 环境变量 > `$KIMI_CODE_HOME/subagent-models.json` > 插件目录 > 内置默认值;每次工具调用都会重新读取,改完立即生效,无需重启)。
+
+字段说明:
+
+| 字段 | 含义 |
+|---|---|
+| `subagents.<名称>.model` | 模型别名。缺失或 `null` 时回退到 `[secondary_model].default_model`。指向内置变体(`k2.8-low` 等)时无需其他操作;指向自定义别名时需先在 `config.toml` 注册该别名并加入 `[secondary_model.models]` 池 |
+| `subagents.<名称>.effort` | 思考强度声明(Kimi 档位 `low`/`high`/`max`,第三方模型可为 `xhigh` 等)。实际绑定靠别名的 `default_effort`;声明值用于核对,`doctor` 发现不一致会警告。第三方模型不支持 Kimi 档位时设为 `null` 或删除 |
+| `subagents.<名称>.subagent_type` | 该条目继承的内置类型语义(`coder`/`explore`/`plan`),供阅读与工具策略参考 |
+| `subagents.<名称>.exclude_tools` | 高风险工具排除说明(信息性;实际强制在 `agents/*.md` 的 `tools`/`disallowedTools`) |
+| `subagents.<名称>.description` | 职责描述,会随路由工具返回进入主 Agent 上下文(另见「提示词语言约定」:用英文) |
+| `default_model` | 顶层兜底:当某条目 `model` 为 `null` 时用它;再缺省则由 `setup-config` 写入的池默认值(`k3-256k-max`)承接 |
+
+可自由增删条目:新增一个 subagent 名称 + 对应 `agents/<名称>.md` agent 文件即可;自定义模型别名若是第三方模型,按下一节注册。
+
+## 切换到第三方订阅模型
+
+两步,约 5 分钟:
+
+### 第一步:在 `config.toml` 注册第三方模型
+
+以 DeepSeek(OpenAI 兼容协议)为例,在 `~/.kimi-code/config.toml` 中添加:
+
+```toml
+[providers.deepseek]
+type = "openai"
+base_url = "https://api.deepseek.com/v1"
+api_key = "你的API_KEY"
+
+[models."deepseek/deepseek-v4.1-flash"]
+provider = "deepseek"
+model = "deepseek-chat"
+max_context_size = 131072
+capabilities = [ "tool_use" ]
+```
+
+也可以直接在 TUI 运行 `/provider`(或 shell 命令 `kimi provider`)走交互式向导:选供应商、输 API Key、选模型;models.dev 已收录的供应商会自动拉取模型列表。
+
+注意事项:
+
+- **能力声明**:模型不支持 `thinking` 就不要在 `capabilities` 里声明。部分第三方模型支持 `reasoning_effort` 字段,但取值与 Kimi 的 `low`/`high`/`max` 不同,本插件不假设兼容性——不支持 Kimi 档位时把 JSON 里的 `effort` 设为 `null` 或删除。
+- **池成员**:新别名必须加入 `[secondary_model.models]` 池,`Agent` 工具的 `model` 参数只接受池别名或 `"primary"`。最简单的方式是改完 JSON 后重跑 `setup-config`(它能自动把新别名补进池中;生成不了条目时会明确提示手动注册)。
+- **凭证管理**:`api_key` 可直接写在 `config.toml`,也可用 `[providers.<name>.env]` 子表引用环境变量名(仅作回退来源,不会修改 shell 环境)。
+
+### 第二步:修改 `subagent-models.json`
+
+```json
+"cheap-coder": {
+  "model": "deepseek/deepseek-v4.1-flash",
+  "effort": null,
+  "subagent_type": "coder",
+  "description": "Mechanical, pattern-fixed coding: bulk renames, formatting, boilerplate"
+}
+```
+
+重跑 `doctor` 确认无错误,执行 `/reload`。
+
+## 验收自查
+
+```bash
+# 复杂任务路由(k3-planner = K3-256k max;k3-max = kimi-code/k3 1M max)
+python3 scripts/kimi_subagent_router.py route --type k3-planner
+python3 scripts/kimi_subagent_router.py route --type k3-max
+# 廉价任务路由(cheap-coder 应返回 k2.8-high;cheap-explore 应返回 k2.8-low)
+python3 scripts/kimi_subagent_router.py route --type cheap-coder
+python3 scripts/kimi_subagent_router.py route --type cheap-explore
+# K2.8 细分档(应返回 k2.8-high / k2.8-max)
+python3 scripts/kimi_subagent_router.py route --type k2.8-medium
+python3 scripts/kimi_subagent_router.py route --type k2.8-debug
+# 文档任务(应返回 alibaba-cn/qwen3.8-flash,effort 为 xhigh;需先注册该第三方模型)
+python3 scripts/kimi_subagent_router.py route --type doc-writer
+# 一致性检查(0 错误即通过;doc-writer 未注册第三方模型时会如实报错)
+python3 scripts/kimi_subagent_router.py doctor
+```
+
+安装后让主 Agent 执行一个明显复杂的任务(如"为这个项目设计插件架构"),它应派发 `k3-planner`/`k3-max`;执行"把 src 下所有 .js 文件重命名为 .ts"这类任务应派发 `cheap-coder`。
+
+## 已知限制
+
+1. **`Agent` 工具没有 effort 参数**:思考强度只能经由模型变体别名的 `default_effort` 绑定;插件的 `route_subagent_model` 工具返回 effort 仅供核对/展示。同理,插件无法实现"封装 `dispatch_subagent` 内部直接派发"的路径(MCP 工具无法调用 CLI 内部的 Agent 机制),采用的是路径 A:工具返回别名,主 Agent 显式传入 `Agent` 的 `model` 参数。
+2. **映射是路由建议而非强制**:最终由主 Agent 在 `Agent` 调用中传 `model` 生效;若主 Agent 省略 `model`,spawn 落到 `[secondary_model].default_model`(默认 `k3-256k-max`)。
+3. **`[secondary_model].default_effort` 会击穿路由**:该字段优先级最高且段级生效,设置后所有变体档位失效。`doctor` 会警告;请保持未设置。
+4. **`k2.8-medium` 名称含点号**:个别 CLI 版本对 agent 名的 kebab-case 校验可能不允许点号,导致该 agent 文件被跳过(派发时提示未知类型)。解决办法:把 `agents/k2.8-medium.md` 重命名为 `k28-medium.md`、frontmatter 的 `name` 改为 `k28-medium`,并同步修改 `subagent-models.json` 的键与 `SKILL.md` 路由表。
+5. **单层委派**:插件 agent 均声明 `subagents: []`,无法在 subagent 内继续派发,委派链单层终止。
+6. **配置错误 fail-loud**:池别名解析不到 `[models]` 条目时,会话启动/恢复会直接报错并列出断链别名(这是 Kimi Code 的行为,先于插件)。改配置后跑 `doctor` 可提前发现。
+
+## 参考文档
+
+- Agents and Sub-Agents:https://www.kimi.com/code/docs/en/kimi-code-cli/customization/agents.html
+- Plugins:https://www.kimi.com/code/docs/en/kimi-code-cli/customization/plugins.html
+- 配置文件(`[secondary_model]` 与模型变体机制):https://www.kimi.com/code/docs/en/kimi-code-cli/configuration/config-files.html
+- Providers and models:https://www.kimi.com/code/docs/en/kimi-code-cli/configuration/providers.html
diff --git a/SKILL.md b/SKILL.md
new file mode 100644
index 0000000..39a1fe6
--- /dev/null
+++ b/SKILL.md
@@ -0,0 +1,74 @@
+---
+name: subagent-model-routing
+description: Model and thinking-tier routing for subagent dispatches. Consult before every Agent or AgentSwarm call, pick a subagent type from the table, then resolve its live model alias with route_subagent_model.
+---
+
+# Subagent model routing
+
+Every subagent type is bound to a model variant alias whose registered `default_effort` sets its thinking tier. Route mechanical work to the cheap alias and reasoning-heavy work to the expensive one. The live mapping lives in `subagent-models.json` and the user can edit it at any time, so use the table below only to pick a **type**; always resolve the **alias** with the routing tool.
+
+## 1. Plan first, then dispatch
+
+Unless the whole request is trivial — something you can finish yourself in one or two tool calls — do **not** start executing directly. First dispatch `k3-planner` (K3-256k, max) to plan, then execute the plan subtask by subtask.
+
+**Planning brief** must demand a plan that contains, for every subtask:
+
+- **goal and scope** — what to achieve, which files/modules it covers;
+- **safety boundary** — what it must not touch (unrelated files, public APIs, user data, secrets), and which operations are off-limits (e.g. no deletions, no force-pushes);
+- **concrete details** — relevant paths, interfaces, constraints, and existing conventions to follow;
+- **verification steps** — the exact commands or checks that prove the subtask is done.
+
+The planning call must also state the recommended tier per subtask (from the table below) and an overall verification process. Keep the returned plan in your context — it is small — and quote each subtask's boundary and verification steps **verbatim** in the executor brief when you dispatch it.
+
+Skip planning only when the task is trivially small, or when the user explicitly says to just do it. Read-only questions you can answer with one lookup need no plan either.
+
+## 2. Pick a type — first matching row wins
+
+| Task | `subagent_type` | Default alias | Effort |
+|---|---|---|---|
+| Mechanical, pattern-fixed edits: bulk rename, formatting, boilerplate, trivial test stubs | `cheap-coder` | `k2.8-high` | high |
+| Read-only lookup: find files, read structure, search symbols, nothing changes | `cheap-explore` | `k2.8-low` | low |
+| Local change scoped to a single file | `k2.8-medium` | `k2.8-high` | high |
+| Simple feature, routine tests, debugging where the symptom is already known | `k2.8-debug` | `k2.8-max` | max |
+| Complex design, architecture, cross-module refactor, non-obvious bug | `k3-planner` | `k3-256k-max` | max |
+| Hardest task **and** the subtask needs more than 256k of context (huge repo sweep, long logs, big files) | `k3-max` | `k3-max` (kimi-code/k3, 1M) | max |
+| Documentation: README, API docs, usage guides, comment cleanup | `doc-writer` | `alibaba-cn/qwen3.8-flash` | xhigh (third-party tier) |
+| Anything else | do not dispatch — handle it yourself | — | — |
+
+## 3. Tie-breakers
+
+1. **Decide fast, explore elsewhere.** Picking a tier is one pass over the table. Do not read code deeply or reason at length in your own context first — that is the subagent's job, and `cheap-explore` exists so the searching happens off your context.
+2. **K3-256k by default.** `k3-planner` and `k3-max` both think at `max`; they differ only in window (256k vs 1M) and `k3-max` costs roughly double. Upgrade only when *both* conditions hold: the task is the hardest kind **and** you expect >256k of context. Unsure means `k3-planner`.
+3. **Debugging splits in two.** Known symptom, bounded scope → `k2.8-debug`. Strange behavior, unknown root cause, deep cross-module interaction → `k3-planner`.
+4. **Type names are not tier names.** `k2.8-debug` is K2.8 at `max` (simple features, routine tests, routine debugging); `k2.8-medium` is the same model at `high`.
+5. **Trivial work gets no subagent.** If it takes you one or two tool calls, do it yourself — dispatch overhead costs more than the task.
+
+## 4. Write the brief to fit the tier
+
+A subagent sees only your `prompt`; none of this conversation carries over. Match the brief to how each tier actually works:
+
+| Tier | Strong at | Brief it like this |
+|---|---|---|
+| `k2.8-low` | Fast literal lookups | One concrete question, exact scope, named output shape, a tool-call budget. No open-ended judgment and no multi-part asks. |
+| `k2.8-high` | Repetitive edits, one-file changes | The exact file list, the exact transformation, and an explicit "change nothing outside this" boundary. |
+| `k2.8-max` | Solving a problem you already scoped | Symptom + evidence (error text, failing test, repro command) + acceptance criteria. Ask for the root cause and the verification that was run. |
+| `k3-planner` | Design, cross-module reasoning, critical code | Goal, hard constraints, non-goals, and *what decision must be made*. Demand one committed recommendation plus rejected alternatives in one line each — not a menu. |
+| `k3-max` | All of the above across a wide corpus | Same as `k3-planner`, plus where to start reading (paths, line ranges) and what "covered" means, so it reads the real surface instead of guessing at it. |
+| `doc-writer` | Prose, structure, tone | Target path, audience, the repo's existing doc language and formatting to mirror, and which source files are the ground truth. |
+
+Both K3 agents and `doc-writer` have **no shell**: never ask them to run tests or builds. Ask instead for the exact commands the caller should run afterwards.
+
+Every brief must also carry: the working directory or concrete paths, what you already established (so it does not redo the work), and the deliverable format.
+
+## 5. Dispatch, in this order
+
+1. Call `route_subagent_model` with `subagent_type` set to the row's type; it returns the live `model` alias and `effort`.
+2. Call `Agent` with **both** `subagent_type` (the row's type, e.g. `k3-planner`) and `model` (the alias from step 1 — it must be a pool alias), plus `description` (3-5 words) and `prompt` (the brief). If step 1 returned `model: null`, omit `model` so `[secondary_model].default_model` takes over.
+3. There is no other way to set the thinking tier: the `Agent` tool has **no** `effort` parameter and the tier comes only from the alias's `default_effort`. If the resolved tier clearly does not fit the task, tell the user to fix `subagent-models.json`; do not switch aliases on your own.
+
+## Constraints
+
+- No nested delegation: these agents ship with `Agent`/`AgentSwarm` disabled, so the chain stops at depth 1.
+- `route_subagent_model` reporting an unknown type → call `list_subagent_routes` for the live names; the user may have added or renamed entries.
+- A large batch of the *same* operation can use `AgentSwarm` with the same `subagent_type` + `model`, as long as every subtask stays in one tier.
+- `doc-writer` points at a third-party model that must be registered in `config.toml`. If routing to it fails, report that to the user instead of silently substituting another tier.
diff --git a/agents/cheap-coder.md b/agents/cheap-coder.md
new file mode 100644
index 0000000..2b206b1
--- /dev/null
+++ b/agents/cheap-coder.md
@@ -0,0 +1,31 @@
+---
+name: cheap-coder
+description: Mechanical coder on K2.8 high effort — bulk renames, formatting, boilerplate, trivial test stubs. Applies one fixed pattern and nothing else.
+whenToUse: Repetitive, pattern-fixed coding that needs almost no reasoning. The cheapest tier that can still edit files.
+disallowedTools:
+  - TaskList
+  - TaskOutput
+  - TaskStop
+  - WaitFor
+  - EnterPlanMode
+  - ExitPlanMode
+  - Agent
+  - AgentSwarm
+subagents: []
+---
+
+You are a mechanical executor on K2.8 at high thinking effort. That effort is for applying the pattern correctly, not for redesigning the task.
+
+Working rules:
+- Do exactly what was asked, at the stated scope. No drive-by refactoring, no new abstractions, no extra renames, no formatting of files you did not otherwise touch.
+- Apply one pattern uniformly. If the first couple of cases show the pattern does not fit (name collision, file not in the assumed shape), stop and report it — do not invent a policy for the remainder.
+- Ambiguous instruction: take the simplest literal reading and record that choice in your report.
+- Real design or architecture judgment is out of scope: stop and report "beyond the mechanical tier — escalate to k2.8-debug or k3-planner", plus what you already finished.
+- You cannot delegate, enter plan mode, or manage background tasks.
+
+Verification is part of the job: after the edits, prove the mechanical property — grep the old token to show zero hits, count the changed files, or run the format/lint/test command the caller named. Fix what your change broke; improve nothing else.
+
+Output format — the caller sees only your final message:
+- **Changed**: file paths, one clause per change.
+- **Verified**: the command or check used and its result.
+- **Skipped / needs a decision**: cases outside the pattern, or "none".
diff --git a/agents/cheap-explore.md b/agents/cheap-explore.md
new file mode 100644
index 0000000..524b2c8
--- /dev/null
+++ b/agents/cheap-explore.md
@@ -0,0 +1,29 @@
+---
+name: cheap-explore
+description: Read-only code search on K2.8 low effort — locate files, map structure, find symbols, quote the exact lines. Changes nothing.
+whenToUse: Any task that is only find / read / report. Cheapest option in the routing table.
+tools:
+  - Read
+  - Grep
+  - Glob
+  - ReadMediaFile
+subagents: []
+---
+
+You are a read-only explorer running at a low thinking tier. Search, read, report. You never edit files, never advise on design, never delegate.
+
+Procedure — follow it literally:
+1. State in one line what question the caller actually needs answered.
+2. Locate before reading: `Glob` for filenames, `Grep` for symbols and strings, then `Read` only the matching regions using offsets — not whole files.
+3. Search the whole repo unless the caller named a path. Before calling anything missing, try the naming variants: camelCase, snake_case, singular/plural, and the string a caller would use rather than the definition.
+4. Stop when every question is answered, or after about 15 tool calls — then say what you did not reach.
+
+Rules:
+- Every claim maps to a line you actually read. Anything else is labelled as inference.
+- "Not found" must list the queries that came back empty.
+- No refactoring opinions, no TODO plans, no writes, no subagents or background tasks.
+
+Output format — your final message is the only thing the caller receives, so it must stand alone:
+- **Answer**: 2-4 sentences, direct.
+- **Evidence**: `path:line` per finding, one short note each.
+- **Gaps**: what you did not check, or "none".
diff --git a/agents/doc-writer.md b/agents/doc-writer.md
new file mode 100644
index 0000000..e832015
--- /dev/null
+++ b/agents/doc-writer.md
@@ -0,0 +1,35 @@
+---
+name: doc-writer
+description: Documentation writer on alibaba-cn/qwen3.8-flash at the xhigh tier — README, API docs, usage guides, comment cleanup. No shell, no changes to code logic.
+whenToUse: Pure writing work where the difficulty is accuracy and tone, not reasoning through a design.
+disallowedTools:
+  - Bash
+  - TaskList
+  - TaskOutput
+  - TaskStop
+  - WaitFor
+  - EnterPlanMode
+  - ExitPlanMode
+  - Agent
+  - AgentSwarm
+subagents: []
+---
+
+You write documentation on qwen3.8-flash at the xhigh thinking tier. You have no shell, and you do not change code logic.
+
+Ground truth:
+- Read the code and the existing docs before writing a single line. Every factual claim — signature, default value, flag name, error behavior, limit, path — must come from something you actually read. Never document from plausibility.
+- Cannot confirm a detail? Write `TODO: verify` in the doc and list it in your final message. An omitted sentence beats an invented one.
+- Mirror the repo: its terminology, its doc **language** (a Chinese README stays Chinese — do not translate it), heading levels, tone, table and code-block conventions. Read one sibling doc first and treat it as the template.
+
+Style limits:
+- No emoji, no marketing adjectives ("seamless", "powerful", "robust", "leverage"), no lead-in filler ("In this section we will..."), no bullet spam where one sentence is shorter, no new heading every three lines.
+- Put what the reader can do first. Commands, flags, signatures and paths in code formatting; exact values, never approximations.
+- Cover the requested scope and stop. Do not add installation, troubleshooting, changelog, or "future work" sections nobody asked for, and do not drift into a neighboring section because it looks under-documented.
+- When the draft feels long enough, cut rather than extend. Brevity is the deliverable here.
+
+Output format — the caller sees only your final message:
+- **Files**: new or changed doc paths, one line each on which section was written.
+- **Sources**: the code files each documented claim came from, as `path:line`.
+- **TODO: verify**: unconfirmed items, or "none".
+- **Language**: the language you wrote in — it must match the repo.
diff --git a/agents/k2.8-debug.md b/agents/k2.8-debug.md
new file mode 100644
index 0000000..324b55c
--- /dev/null
+++ b/agents/k2.8-debug.md
@@ -0,0 +1,33 @@
+---
+name: k2.8-debug
+description: K2.8 at max thinking — simple feature work, routine tests, and debugging where the symptom is already known.
+whenToUse: Needs real reasoning but not K3 depth — implement a small feature, add routine tests, or fix a bug whose symptom is already on the table. Non-obvious root causes go to k3-planner.
+disallowedTools:
+  - Agent
+  - AgentSwarm
+subagents: []
+---
+
+You are K2.8 at its highest thinking tier — the level meant for spending reasoning budget on the problem itself. Do the deep work in your thinking; keep the final message short and factual.
+
+Implementation tasks:
+- Read the surrounding code and its call sites first; extend what exists instead of adding a parallel structure.
+- Minimal sufficient change, in the project's own style, then run the relevant tests or checks and iterate until they pass.
+- Add tests where the repo has an obvious convention for them; do not build a new test harness.
+
+Debugging tasks — work the loop, never guess-and-patch:
+1. **Reproduce.** Run the failing command, or write down the exact symptom, input and full error text.
+2. **Localize.** Read the error against the code path, then follow the data through the call chain to the first point where the value is already wrong.
+3. **Hypothesize** one cause and test it with the cheapest observation that could falsify it: a narrowed input, one targeted log, a single test.
+4. **Fix the root cause**, not the symptom. A guard clause that hides a bad upstream value is a failed fix — if the correct fix is upstream and out of scope, say so instead.
+5. **Re-verify** with the original repro plus the nearby suite, and state what actually broke and why the fix prevents it.
+
+Escalate rather than grind when the problem needs architecture-level judgment or spans deep multi-module interactions: report "upgrade to k3-planner", what you established, and where your reasoning stopped.
+
+You cannot delegate to subagents.
+
+Output format — the caller sees only your final message:
+- **Result**: what was implemented, or the root cause in one paragraph.
+- **Changed**: files, one line per change.
+- **Verified**: exact commands run and their outcomes.
+- **Open**: what is unfixed or unverified, and why, or "none".
diff --git a/agents/k2.8-medium.md b/agents/k2.8-medium.md
new file mode 100644
index 0000000..2cca132
--- /dev/null
+++ b/agents/k2.8-medium.md
@@ -0,0 +1,24 @@
+---
+name: k2.8-medium
+description: Local change on K2.8 high effort — an edit confined to one file or one clearly scoped spot.
+whenToUse: Scope is bounded to a single file — fix a function, adjust a config block, extend one component. For features, routine tests or debugging use k2.8-debug.
+disallowedTools:
+  - Agent
+  - AgentSwarm
+subagents: []
+---
+
+You are a scoped-edit subagent on K2.8 at high thinking effort. One file, one coherent change.
+
+Working rules:
+- Read the target file and its direct call sites before editing. What high effort buys you here is not a better design but a change that fits what is already there.
+- Follow the file's existing conventions: naming, structure, error handling, import order, comment language. The goal is a diff that reads as if the file's own author wrote it.
+- Keep the diff minimal and sufficient. Nothing unrelated gets touched, reordered, or cleaned up — an incidental formatting change doubles review cost.
+- Then run the project's existing check for that area (test, lint, typecheck, build) and fix what your change broke.
+- If the work turns out to need multiple files, new design, or open-ended debugging, stop: report "re-route to k2.8-debug or k3-planner", what you finished, and where it crossed the line.
+- You cannot delegate to subagents.
+
+Output format — the caller sees only your final message:
+- **Changed**: files with `path:line` ranges, one line each on what and why.
+- **Verified**: which check ran and its result.
+- **Left open**: follow-ups you deliberately did not do, or "none".
diff --git a/agents/k3-max.md b/agents/k3-max.md
new file mode 100644
index 0000000..345723a
--- /dev/null
+++ b/agents/k3-max.md
@@ -0,0 +1,33 @@
+---
+name: k3-max
+description: K3 on kimi-code/k3 with a 1M window at max thinking — the hardest tasks that also require reading far more than 256k of material.
+whenToUse: Only when both conditions hold at once — the task is the hardest kind (deep architecture, stubborn bug, correctness argument) AND it needs more than 256k of context, such as a giant repo sweep, long logs or large files. Prefer k3-planner otherwise.
+tools:
+  - Read
+  - Grep
+  - Glob
+  - ReadMediaFile
+  - Edit
+  - Write
+  - TodoList
+subagents: []
+---
+
+You are kimi-code/k3 with a 1M context at maximum thinking intensity. You exist for the combination of *hardest* and *too wide for 256k*: use the window, and reason as deep as the problem needs.
+
+How to work:
+- **Map first, then read big.** One Glob/Grep pass to size the surface, then a few large contiguous Reads rather than many small peeks. Wide, coherent context is the reason you were dispatched — do not substitute guesses for reading it.
+- **Stay anchored to evidence.** Every conclusion names the files and lines it rests on, and anything not read is labelled inference.
+- **Argue, do not assert.** Why the design is correct, its failure modes and boundary conditions, the counter-example you tried, and the simpler alternative you rejected with the reason.
+- **Cross-corpus consistency is your real deliverable**: contradictions between modules, duplicated or drifting invariants, the same abstraction implemented three ways — what a narrow context cannot see.
+- **Write critical code to a usable completion level**, and state the verification plan for each claim you make about it.
+- **Mind your own budget.** As the context grows, re-derive conclusions from the cited evidence instead of from memory of earlier steps.
+
+You have **no shell** — you cannot run tests, builds, or git. Never claim something passes. Give the caller the ordered commands and what each outcome would mean.
+
+Output format — the caller sees only your final message:
+- **Conclusion**: the answer or decision, committed.
+- **Reasoning**: the decisive arguments and evidence, with `path:line` references.
+- **Code / changes**: what you wrote, where.
+- **Verify**: commands for the caller and expected outcomes.
+- **Uncertainty**: what could still be wrong, and what would settle it.
diff --git a/agents/k3-planner.md b/agents/k3-planner.md
new file mode 100644
index 0000000..fc7df5a
--- /dev/null
+++ b/agents/k3-planner.md
@@ -0,0 +1,35 @@
+---
+name: k3-planner
+description: K3-256k at max thinking — complex design, architecture trade-offs, cross-module refactors, non-obvious bugs, critical code. The preferred heavy tier.
+whenToUse: The task needs deep reasoning. Escalate to k3-max only when it also needs more than 256k of context.
+tools:
+  - Read
+  - Grep
+  - Glob
+  - ReadMediaFile
+  - Edit
+  - Write
+  - TodoList
+subagents: []
+---
+
+You are K3-256k at maximum thinking intensity: the tier reserved for problems where the reasoning, not the typing, is the hard part. You cost about half of `k3-max` and hold a quarter of its window, so spend context deliberately — read what the decision depends on, do not ingest the repository.
+
+How to work:
+- **Ground first, then decide.** Read the relevant code and the conventions it follows before designing. A plan built on assumed structure is worse than no plan.
+- **Commit to a recommendation.** State the constraints, the failure modes you considered, and the one option you choose. Rejected alternatives get one line each with the reason they lost. "It depends" is a failed deliverable — name the condition that would change the answer instead.
+- **Be explicit only where reasoning changes the outcome.** Your value is the decision and its justification, not a transcript of everything you thought about.
+- **Write critical code to a usable level**: real signatures, real data flow, real error handling — no `// TODO` placeholders on the happy path.
+- **Treat bugs as root-cause problems.** State the competing hypotheses, what evidence separates them, which you ruled out and how, then the fix and why it holds.
+- Multi-step work: keep the plan in your own reasoning and deliver the result. Use TodoList only when the caller asked for a phased plan that someone else will execute.
+
+You have **no shell** — you cannot run tests, builds, or git. Never write that something "passes" or "is verified". Hand the caller the exact commands in the order that fails fastest, and what each result proves.
+
+Escalate upward when the answer depends on evidence only a runtime can produce, or when the task is genuinely cross-cutting over a huge surface: say that a shell-capable tier or `k3-max` is needed, and what you already finished.
+
+Output format — the caller sees only your final message:
+- **Decision**: the recommendation, and the conditions that would change it.
+- **Design / root cause**: modules, interfaces, data flow, error handling — or the causal chain of the bug.
+- **Code**: what you wrote or changed, with paths.
+- **Verify**: commands for the caller and what each proves.
+- **Risks / open questions**: the specific places this could be wrong.
diff --git a/kimi.plugin.json b/kimi.plugin.json
new file mode 100644
index 0000000..4d78a57
--- /dev/null
+++ b/kimi.plugin.json
@@ -0,0 +1,20 @@
+{
+  "name": "kimi-subagent-router",
+  "version": "1.0.0",
+  "description": "Routes each subagent type to its own model alias and thinking tier: cheap-coder / cheap-explore / k2.8-medium / k2.8-debug / k3-planner / k3-max / doc-writer. All mappings are editable in subagent-models.json.",
+  "interface": {
+    "displayName": "Kimi Subagent Router",
+    "shortDescription": "Route subagent types to model aliases and thinking tiers",
+    "longDescription": "Model routing per subagent type for Kimi Code CLI: read-only and mechanical work runs on K2.8 low/high, scoped edits and known-symptom debugging on K2.8 high/max, complex design and critical code on K3-256k max, and the hardest tasks that also need a 1M window on K3. Documentation runs on a third-party model. Every mapping lives in subagent-models.json and can be re-pointed at any registered model, including third-party subscriptions."
+  },
+  "agents": "./agents/",
+  "sessionStart": {
+    "skill": "subagent-model-routing"
+  },
+  "mcpServers": {
+    "kimi-subagent-router": {
+      "command": "./scripts/kimi_subagent_router.py",
+      "args": ["mcp"]
+    }
+  }
+}
diff --git a/plugin.json b/plugin.json
new file mode 100644
index 0000000..4d78a57
--- /dev/null
+++ b/plugin.json
@@ -0,0 +1,20 @@
+{
+  "name": "kimi-subagent-router",
+  "version": "1.0.0",
+  "description": "Routes each subagent type to its own model alias and thinking tier: cheap-coder / cheap-explore / k2.8-medium / k2.8-debug / k3-planner / k3-max / doc-writer. All mappings are editable in subagent-models.json.",
+  "interface": {
+    "displayName": "Kimi Subagent Router",
+    "shortDescription": "Route subagent types to model aliases and thinking tiers",
+    "longDescription": "Model routing per subagent type for Kimi Code CLI: read-only and mechanical work runs on K2.8 low/high, scoped edits and known-symptom debugging on K2.8 high/max, complex design and critical code on K3-256k max, and the hardest tasks that also need a 1M window on K3. Documentation runs on a third-party model. Every mapping lives in subagent-models.json and can be re-pointed at any registered model, including third-party subscriptions."
+  },
+  "agents": "./agents/",
+  "sessionStart": {
+    "skill": "subagent-model-routing"
+  },
+  "mcpServers": {
+    "kimi-subagent-router": {
+      "command": "./scripts/kimi_subagent_router.py",
+      "args": ["mcp"]
+    }
+  }
+}
diff --git a/scripts/kimi_subagent_router.py b/scripts/kimi_subagent_router.py
new file mode 100755
index 0000000..ee95c21
--- /dev/null
+++ b/scripts/kimi_subagent_router.py
@@ -0,0 +1,872 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""kimi-subagent-router — 按 subagent 类型路由模型别名与思考强度。
+
+子命令:
+  mcp                     以 MCP stdio 服务运行(插件 manifest 使用此模式)
+  route --type NAME       返回某 subagent 类型应使用的模型别名与思考强度
+  list                    列出全部路由映射
+  doctor [--json]         检查 subagent-models.json 与 config.toml 一致性
+  print-config            打印需并入 config.toml 的 TOML 片段(手动安装用)
+  setup-config [--home H] 自动把片段并入 config.toml(标记块、幂等、先备份)
+  remove-config [--home H] 移除已并入的片段
+  init-json [--path P]    写出默认 subagent-models.json
+
+所有 subagent -> 模型/思考强度映射一律从外部 subagent-models.json 读取;
+该文件缺失时使用内置默认值。配置文件查找顺序:
+  1. $KIMI_SUBAGENT_MODELS 指定的路径
+  2. <home>/subagent-models.json(home 依次取 --home 参数、$KIMI_CODE_HOME、~/.kimi-code)
+  3. <插件根目录>/subagent-models.json
+  4. 内置默认值
+"""
+
+import argparse
+import copy
+import datetime
+import json
+import os
+import re
+import shutil
+import sys
+
+PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+MARKER_BEGIN = "# >>> kimi-subagent-router >>>"
+MARKER_END = "# <<< kimi-subagent-router <<<"
+
+# ---------------------------------------------------------------------------
+# 内置默认值:仅在 subagent-models.json 不存在时使用(用户文件始终优先)。
+# model 一律指向"模型变体别名";思考强度由该别名在 config.toml 里注册的
+# default_effort 绑定(Agent 工具没有 effort 参数)。
+# ---------------------------------------------------------------------------
+DEFAULT_CONFIG = {
+    "subagents": {
+        "cheap-coder": {
+            "model": "k2.8-high",
+            "effort": "high",
+            "subagent_type": "coder",
+            "description": "Mechanical, pattern-fixed coding: bulk renames, formatting, boilerplate, trivial test stubs (K2.8 high)",
+        },
+        "cheap-explore": {
+            "model": "k2.8-low",
+            "effort": "low",
+            "subagent_type": "explore",
+            "description": "Read-only exploration: locate files, read structure, search symbols (K2.8 low)",
+        },
+        "k2.8-medium": {
+            "model": "k2.8-high",
+            "effort": "high",
+            "subagent_type": "coder",
+            "description": "Local code change scoped to one file (K2.8 high)",
+        },
+        "k2.8-debug": {
+            "model": "k2.8-max",
+            "effort": "max",
+            "subagent_type": "coder",
+            "description": "Simple feature work, routine tests, known-symptom debugging (K2.8 max; non-obvious bugs belong to k3-planner)",
+        },
+        "k3-planner": {
+            "model": "k3-256k-max",
+            "effort": "max",
+            "subagent_type": "plan",
+            "description": "Complex design, architecture, cross-module refactors, hard bugs, critical code (K3-256k max; preferred heavy tier)",
+        },
+        "k3-max": {
+            "model": "k3-max",
+            "effort": "max",
+            "subagent_type": "plan",
+            "description": "Hardest tasks that ALSO need more than 256k context (kimi-code/k3, 1M window, max)",
+        },
+        "doc-writer": {
+            "model": "alibaba-cn/qwen3.8-flash",
+            "effort": "xhigh",
+            "subagent_type": "coder",
+            "exclude_tools": ["shell", "background_task"],
+            "description": "Documentation: README, API docs, prose and comment cleanup (third-party alibaba-cn/qwen3.8-flash, xhigh tier)",
+        },
+    },
+    "default_model": None,
+}
+
+# 插件可自动注册的"内置管理模型变体":变体别名 -> 底层模型 ID。
+# 仅用于 setup-config 生成 [models] 条目;用户的映射仍以 JSON 配置为准。
+VARIANT_BASE_MODEL = {
+    "k2.8-low": "kimi-for-coding",
+    "k2.8-high": "kimi-for-coding",
+    "k2.8-max": "kimi-for-coding",
+    "k3-256k-high": "k3-256k",
+    "k3-256k-max": "k3-256k",
+    "k3-max": "k3",
+}
+
+# 当 config.toml 中找不到底层模型条目时的字段模板(Kimi Code 内置托管模型)。
+BUILTIN_MODEL_FALLBACKS = {
+    "kimi-for-coding": {
+        "provider": "managed:kimi-code",
+        "model": "kimi-for-coding",
+        "max_context_size": 1048576,
+        "capabilities": ["thinking", "always_thinking", "image_in", "video_in", "tool_use"],
+        "support_efforts": ["low", "high", "max"],
+    },
+    "k3-256k": {
+        "provider": "managed:kimi-code",
+        "model": "k3-256k",
+        "max_context_size": 262144,
+        "capabilities": ["thinking", "always_thinking", "image_in", "video_in", "tool_use"],
+        "support_efforts": ["low", "high", "max"],
+    },
+    "k3": {
+        "provider": "managed:kimi-code",
+        "model": "k3",
+        "max_context_size": 1048576,
+        "capabilities": ["thinking", "always_thinking", "image_in", "video_in", "tool_use"],
+        "support_efforts": ["low", "high", "max"],
+    },
+}
+
+# [secondary_model] 池默认值的兜底:与临时环境基准一致(K3-256k + max)。
+FALLBACK_POOL_DEFAULT = "k3-256k-max"
+
+_BARE_KEY = re.compile(r"^[A-Za-z0-9_-]+$")
+
+
+# ---------------------------------------------------------------------------
+# 配置读取
+# ---------------------------------------------------------------------------
+
+def find_models_json(home_arg=None):
+    env = os.environ.get("KIMI_SUBAGENT_MODELS")
+    if env:
+        return env, "$KIMI_SUBAGENT_MODELS"
+    homes = []
+    if home_arg:
+        homes.append(home_arg)
+    if os.environ.get("KIMI_CODE_HOME"):
+        homes.append(os.environ["KIMI_CODE_HOME"])
+    homes.append(os.path.join(os.path.expanduser("~"), ".kimi-code"))
+    for home in homes:
+        p = os.path.join(home, "subagent-models.json")
+        if os.path.isfile(p):
+            return p, os.path.join(os.path.basename(home) or home, "subagent-models.json")
+    p = os.path.join(PLUGIN_ROOT, "subagent-models.json")
+    if os.path.isfile(p):
+        return p, os.path.join("<plugin>", "subagent-models.json")
+    return None, "builtin defaults"
+
+
+def load_routes(home_arg=None):
+    """返回 (routes_dict, source_label, path_or_None)。文件缺失用内置默认值。"""
+    path, source = find_models_json(home_arg)
+    if path is None:
+        return copy.deepcopy(DEFAULT_CONFIG), source, None
+    try:
+        with open(path, "r", encoding="utf-8") as f:
+            data = json.load(f)
+    except Exception as e:  # noqa: BLE001 - 报错信息要直达用户
+        raise SystemExit(f"Failed to parse config file {path}: {e}")
+    if not isinstance(data, dict) or not isinstance(data.get("subagents"), dict):
+        raise SystemExit(f"Invalid config file {path}: expected an object containing a \"subagents\" object")
+    return data, source, path
+
+
+def resolve_home(home_arg):
+    if home_arg:
+        return home_arg
+    return os.environ.get("KIMI_CODE_HOME") or os.path.join(
+        os.path.expanduser("~"), ".kimi-code")
+
+
+def config_toml_path(home_arg):
+    return os.path.join(resolve_home(home_arg), "config.toml")
+
+
+def read_config_toml(home_arg):
+    """返回 (text, parsed_or_None)。parsed 在无法解析时为 None。"""
+    path = config_toml_path(home_arg)
+    if not os.path.isfile(path):
+        return "", None
+    with open(path, "r", encoding="utf-8") as f:
+        text = f.read()
+    parsed = None
+    try:
+        import tomllib
+        with open(path, "rb") as f:
+            parsed = tomllib.load(f)
+    except Exception:  # noqa: BLE001 - 文本扫描兜底
+        pass
+    return text, parsed
+
+
+def extract_models(parsed, text):
+    """返回 {alias: entry_dict},含 overrides 子表(如有)。"""
+    if parsed is not None and isinstance(parsed.get("models"), dict):
+        return parsed["models"]
+    models = {}
+    for m in re.finditer(r'^\[models\.("([^"]+)"|([A-Za-z0-9_-]+))\]\s*$', text, re.M):
+        alias = m.group(2) or m.group(3)
+        body = text[m.end():]
+        nxt = re.search(r'^\[', body, re.M)
+        sect = body[: nxt.start()] if nxt else body
+        entry = {}
+        for line in sect.splitlines():
+            kv = re.match(r'^\s*([A-Za-z_]+)\s*=\s*(.+?)\s*$', line)
+            if kv:
+                entry[kv.group(1)] = kv.group(2).strip().strip('"')
+        models[alias] = entry
+    return models
+
+
+def extract_secondary_model(parsed, text):
+    if parsed is not None:
+        sm = parsed.get("secondary_model")
+        return sm if isinstance(sm, dict) else None
+    m = re.search(r'^\[secondary_model\]\s*$', text, re.M)
+    if not m:
+        return None
+    body = text[m.end():]
+    nxt = re.search(r'^\[(?!models\.)', body, re.M)
+    sect = body[: nxt.start()] if nxt else body
+    out = {"models": {}}
+    for line in sect.splitlines():
+        kv = re.match(r'^\s*([A-Za-z_]+|"(?:[^"\\]|\\.)*")\s*=\s*(.+?)\s*$', line)
+        if not kv:
+            continue
+        key, val = kv.group(1).strip('"'), kv.group(2).strip().strip('"')
+        if key == "models":
+            continue
+        if key in ("force",):
+            out[key] = val.lower() == "true"
+        else:
+            out[key] = val
+    mm = re.search(r'^\[secondary_model\.models\]\s*$', text, re.M)
+    if mm:
+        body = text[mm.end():]
+        nxt = re.search(r'^\[', body, re.M)
+        sect = body[: nxt.start()] if nxt else body
+        for line in sect.splitlines():
+            kv = re.match(r'^\s*("(?:[^"\\]|\\.)*"|[A-Za-z0-9_-]+)\s*=\s*(.+?)\s*$', line)
+            if kv:
+                out["models"][kv.group(1).strip('"')] = kv.group(2).strip().strip('"')
+    return out
+
+
+# ---------------------------------------------------------------------------
+# TOML 生成
+# ---------------------------------------------------------------------------
+
+def toml_str(s):
+    return '"' + str(s).replace("\\", "\\\\").replace('"', "'").replace("\n", " ") + '"'
+
+
+def toml_key(alias):
+    return alias if _BARE_KEY.match(alias) else toml_str(alias)
+
+
+def collect_aliases(routes):
+    """JSON 配置中出现的全部非空 model 别名 -> {effort, users[]}。"""
+    used = {}
+    for name, r in routes.get("subagents", {}).items():
+        alias = r.get("model")
+        if not alias:
+            continue
+        slot = used.setdefault(alias, {"effort": r.get("effort"), "users": []})
+        if r.get("effort"):
+            slot["effort"] = r.get("effort")
+        slot["users"].append(name)
+    return used
+
+
+def render_model_alias(alias, src, effort, warnings):
+    out = [f"[models.{toml_key(alias)}]"]
+    out.append(f"provider = {toml_str(src['provider'])}")
+    out.append(f"model = {toml_str(src['model'])}")
+    out.append(f"max_context_size = {int(src['max_context_size'])}")
+    caps = src.get("capabilities") or []
+    if caps:
+        out.append("capabilities = [ " + ", ".join(toml_str(c) for c in caps) + " ]")
+    se = src.get("support_efforts") or []
+    if se:
+        out.append("support_efforts = [ " + ", ".join(toml_str(e) for e in se) + " ]")
+    if effort:
+        if se and effort not in se:
+            warnings.append(
+                f"别名 {alias}: effort={effort} 不在 support_efforts {se} 中,已跳过 default_effort。")
+        else:
+            out += ["", f"[models.{toml_key(alias)}.overrides]",
+                    f"default_effort = {toml_str(effort)}"]
+    out.append("")
+    return out
+
+
+def generate_block(routes, existing_models):
+    """生成可并入 config.toml 的标记块。返回 (text, warnings)。"""
+    warnings = []
+    used = collect_aliases(routes)
+    lines = [
+        MARKER_BEGIN,
+        "# Managed by the kimi-subagent-router plugin. To change routing, edit subagent-models.json",
+        "# and re-run: python3 scripts/kimi_subagent_router.py setup-config",
+        "",
+    ]
+    generated = set()
+    for alias, info in used.items():
+        if alias in existing_models:
+            continue
+        base_model_id = VARIANT_BASE_MODEL.get(alias)
+        if not base_model_id:
+            warnings.append(
+                f"别名 {alias} 未在 [models] 注册,且不是内置托管变体;无法自动生成。"
+                "请先按 README「切换到第三方模型」手动注册该别名。")
+            continue
+        src = None
+        for _, fields in (existing_models or {}).items():
+            if fields.get("model") == base_model_id:
+                src = fields
+                break
+        if src is not None and not (src.get("provider") and src.get("max_context_size")):
+            warnings.append(
+                f"config.toml 中 {base_model_id} 的已有条目缺少 provider/max_context_size 字段,"
+                f"别名 {alias} 已改用插件内置字段模板,请核对 max_context_size。")
+            src = None
+        elif src is None:
+            warnings.append(
+                f"config.toml 中未找到底层模型 {base_model_id} 的条目,别名 {alias} 已使用"
+                "插件内置字段模板,请核对 max_context_size。")
+        if src is None:
+            src = BUILTIN_MODEL_FALLBACKS.get(base_model_id)
+        if not (src and src.get("provider") and src.get("model") and src.get("max_context_size")):
+            warnings.append(f"别名 {alias} 缺少可用的字段模板,已跳过生成。")
+            continue
+        lines += render_model_alias(alias, src, info["effort"], warnings)
+        generated.add(alias)
+
+    registered = set(existing_models or {}) | generated
+    pool_aliases = [a for a in used if a in registered]
+    if not pool_aliases:
+        warnings.append("没有可写入 [secondary_model] 池的模型别名。")
+        return "\n".join(lines).rstrip() + "\n" + MARKER_END + "\n", warnings
+
+    default_model = routes.get("default_model")
+    if not default_model:
+        default_model = FALLBACK_POOL_DEFAULT
+        warnings.append(
+            f"subagent-models.json 的 default_model 为空,池默认值回退为 {FALLBACK_POOL_DEFAULT}。")
+    if default_model not in registered:
+        fallback = pool_aliases[0]
+        warnings.append(
+            f"default_model={default_model} 未注册到 [models],已回退为 {fallback}。")
+        default_model = fallback
+
+    lines += ["[secondary_model]", f"default_model = {toml_str(default_model)}", "",
+              "[secondary_model.models]"]
+    for alias in pool_aliases:
+        info = used[alias]
+        users = ", ".join(info["users"])
+        hint = f"For subagents: {users}. Thinking tier = this alias's default_effort."
+        lines.append(f"{toml_key(alias)} = {toml_str(hint)}")
+    lines += ["", MARKER_END]
+    return "\n".join(lines) + "\n", warnings
+
+
+def find_marked_block(text):
+    begin = text.find(MARKER_BEGIN)
+    end = text.find(MARKER_END)
+    if begin == -1 or end == -1 or end < begin:
+        return None
+    end += len(MARKER_END)
+    while end < len(text) and text[end] == "\n":
+        end += 1
+    return begin, end
+
+
+# ---------------------------------------------------------------------------
+# 命令实现
+# ---------------------------------------------------------------------------
+
+def cmd_route(args):
+    routes, source, path = load_routes()
+    name = args.type
+    entry = routes.get("subagents", {}).get(name)
+    if entry is None:
+        known = sorted(routes.get("subagents", {}).keys())
+        print(json.dumps({
+            "error": f"Unknown subagent type: {name}",
+            "known_subagent_types": known,
+            "config_source": source,
+        }, ensure_ascii=False, indent=2))
+        return 1
+    model = entry.get("model")
+    effort = entry.get("effort")
+    dispatch_args = {
+        "subagent_type": name,
+        "prompt": "<self-contained task brief: context, goal, files in scope, acceptance criteria, expected output>",
+        "description": "<3-5 word summary>",
+    }
+    notes = [
+        "The Agent tool has no effort parameter: the thinking tier is bound to this alias's default_effort.",
+    ]
+    if model:
+        dispatch_args["model"] = model
+    else:
+        notes.append("model is null: omit the model argument so [secondary_model].default_model takes over.")
+        dm = routes.get("default_model")
+        if dm:
+            model = dm
+            dispatch_args["model"] = dm
+            notes.append(f"Fell back to default_model from subagent-models.json: {dm}")
+    result = {
+        "subagent_type": name,
+        "model": model,
+        "effort": effort,
+        "subagent_type_inherits": entry.get("subagent_type"),
+        "description": entry.get("description"),
+        "exclude_tools": entry.get("exclude_tools"),
+        "config_source": source,
+        "dispatch": {"tool": "Agent", "arguments": dispatch_args},
+        "notes": notes,
+    }
+    print(json.dumps(result, ensure_ascii=False, indent=2))
+    return 0
+
+
+def cmd_list(args):
+    routes, source, path = load_routes()
+    out = {
+        "config_file": path,
+        "config_source": source,
+        "default_model": routes.get("default_model"),
+        "subagents": routes.get("subagents", {}),
+    }
+    print(json.dumps(out, ensure_ascii=False, indent=2))
+    return 0
+
+
+def run_doctor(home_arg):
+    routes, source, path = load_routes(home_arg)
+    issues = []
+
+    text, parsed = read_config_toml(home_arg)
+    cfg_path = config_toml_path(home_arg)
+    if parsed is None and text.strip():
+        issues.append(("warn", f"{cfg_path} 无法解析为 TOML,已退化为文本扫描,检查结果可能不全。"))
+    if not os.path.isfile(cfg_path):
+        issues.append(("error", f"找不到 {cfg_path};请先运行 setup-config 或手动添加配置。"))
+        return issues, source, cfg_path
+
+    models = extract_models(parsed, text)
+    sm = extract_secondary_model(parsed, text)
+
+    for name, r in routes.get("subagents", {}).items():
+        alias = r.get("model")
+        eff = r.get("effort")
+        if not alias:
+            issues.append(("info", f"{name}: 未配置 model,派生时将回退到 [secondary_model].default_model。"))
+            continue
+        if alias not in models:
+            issues.append(("error",
+                           f"{name}: 模型别名 {alias} 未注册到 [models]。运行 setup-config 或按 README 手动注册。"))
+            continue
+        entry = models[alias] or {}
+        overrides = entry.get("overrides") or {}
+        actual_effort = overrides.get("default_effort") or entry.get("default_effort")
+        se = entry.get("support_efforts") or []
+        if eff:
+            if actual_effort != eff:
+                issues.append(("warn",
+                               f"{name}: 声明 effort={eff},但别名 {alias} 的实际默认档位为 "
+                               f"{actual_effort or '(未声明)'};实际派发以别名档位为准。"
+                               "请重新运行 setup-config 或修正 [models 条目的 overrides].default_effort。"))
+            if se and eff not in se:
+                issues.append(("error", f"{name}: effort={eff} 不在 {alias} 的 support_efforts {se} 中。"))
+        if sm:
+            pool = sm.get("models") or {}
+            if sm.get("default_model") != alias and alias not in pool:
+                issues.append(("error",
+                               f"{name}: 别名 {alias} 不在 [secondary_model] 池中;Agent 工具的 model 参数"
+                               "只接受池别名或 \"primary\"。运行 setup-config。"))
+
+    if sm:
+        if sm.get("default_effort"):
+            issues.append(("warn",
+                           "[secondary_model].default_effort 已设置:它会覆盖所有变体别名档位,"
+                           "导致按类型路由的思考强度失效,建议删除该字段。"))
+        if sm.get("force"):
+            issues.append(("warn",
+                           "[secondary_model].force=true 时 Agent 工具会拒绝 model 参数,路由失效;"
+                           "建议改为 false。"))
+        if not sm.get("default_model"):
+            issues.append(("error", "[secondary_model] 缺少 default_model。"))
+    else:
+        issues.append(("error",
+                       "未找到 [secondary_model] 配置:Agent 工具没有 model 参数,subagent 将继承主 Agent 模型。"
+                       "运行 setup-config。"))
+    return issues, source, cfg_path
+
+
+def cmd_doctor(args):
+    issues, source, cfg_path = run_doctor(args.home)
+    levels = {"error": 0, "warn": 0, "info": 0}
+    for level, _ in issues:
+        levels[level] = levels.get(level, 0) + 1
+    if args.json:
+        print(json.dumps({
+            "config_file": cfg_path,
+            "config_source": source,
+            "ok": levels["error"] == 0,
+            "issues": [{"level": l, "message": m} for l, m in issues],
+        }, ensure_ascii=False, indent=2))
+    else:
+        icon = {"error": "[错误]", "warn": "[警告]", "info": "[信息]"}
+        print(f"配置文件: {cfg_path}")
+        print(f"映射来源: {source}")
+        if not issues:
+            print("一切正常。")
+        for level, msg in issues:
+            print(f"{icon.get(level, level)} {msg}")
+        print(f"\n汇总: {levels['error']} 错误 / {levels['warn']} 警告 / {levels['info']} 信息")
+    return 1 if levels["error"] else 0
+
+
+def cmd_print_config(args):
+    routes, source, _ = load_routes(args.home)
+    text, _ = read_config_toml(args.home)
+    span = find_marked_block(text)
+    if span:
+        text = text[: span[0]] + text[span[1]:]
+    try:
+        import tomllib
+        parsed = tomllib.loads(text) if text.strip() else None
+    except Exception:  # noqa: BLE001
+        parsed = None
+    existing = extract_models(parsed, text)
+    block, warnings = generate_block(routes, existing)
+    for w in warnings:
+        print(f"# 警告: {w}", file=sys.stderr)
+    sys.stdout.write(block)
+    return 0
+
+
+def _backup(path):
+    stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
+    bak = f"{path}.kimi-subagent-router.bak-{stamp}"
+    shutil.copy2(path, bak)
+    return bak
+
+
+def cmd_setup_config(args):
+    routes, source, _ = load_routes(args.home)
+    cfg_path = config_toml_path(args.home)
+    home = os.path.dirname(cfg_path)
+    os.makedirs(home, exist_ok=True)
+    text = ""
+    if os.path.isfile(cfg_path):
+        with open(cfg_path, "r", encoding="utf-8") as f:
+            text = f.read()
+
+    # 剥掉旧标记块再解析:块内由本插件生成的变体不算用户已有条目,
+    # 否则更新时会把它们误判为"已注册"而从新块中遗漏。
+    span = find_marked_block(text)
+    base_text = (text[: span[0]] + text[span[1]:]) if span else text
+    try:
+        import tomllib
+        parsed = tomllib.loads(base_text) if base_text.strip() else None
+    except Exception:  # noqa: BLE001
+        parsed = None
+
+    existing = extract_models(parsed, base_text)
+    block, warnings = generate_block(routes, existing)
+
+    if span:
+        new_text = text[: span[0]] + block + text[span[1]:]
+        action = "已更新已有标记块"
+    else:
+        conflict_models = []
+        for alias in collect_aliases(routes):
+            if alias in existing:
+                continue
+            if re.search(r'^\[models\.("?' + re.escape(alias) + r'"?)\]', base_text, re.M):
+                conflict_models.append(alias)
+        has_sm = re.search(r'^\[secondary_model(\.|\])', base_text, re.M)
+        if conflict_models or has_sm:
+            print("检测到现有配置冲突,为避免破坏 config.toml,未写入。请手动合并:", file=sys.stderr)
+            print(f"  冲突: [secondary_model] 段已存在: {bool(has_sm)}; 冲突别名: {conflict_models}", file=sys.stderr)
+            print("  可手动添加以下片段(放在已有 [secondary_model] 与 [models] 表中对应位置):", file=sys.stderr)
+            sys.stderr.write(block)
+            return 2
+        header = "" if not text.strip() else ("\n" if not text.endswith("\n") else "\n")
+        new_text = text + header + "\n" + block
+        action = "已追加标记块"
+
+    bak = _backup(cfg_path) if os.path.isfile(cfg_path) else None
+    try:
+        try:
+            import tomllib
+            tomllib.loads(new_text)
+        except Exception as e:  # noqa: BLE001
+            raise SystemExit(f"生成的 TOML 校验失败: {e}")
+        with open(cfg_path, "w", encoding="utf-8") as f:
+            f.write(new_text)
+    except Exception:
+        if bak:
+            shutil.copy2(bak, cfg_path)
+        raise
+
+    print(f"{action}: {cfg_path}")
+    if bak:
+        print(f"备份: {bak}")
+    for w in warnings:
+        print(f"警告: {w}")
+    print("下一步: 重启 Kimi Code 或执行 /reload 使配置生效。")
+    return 0
+
+
+def cmd_remove_config(args):
+    cfg_path = config_toml_path(args.home)
+    if not os.path.isfile(cfg_path):
+        print(f"找不到 {cfg_path},无需移除。")
+        return 0
+    with open(cfg_path, "r", encoding="utf-8") as f:
+        text = f.read()
+    span = find_marked_block(text)
+    if not span:
+        print("未找到 kimi-subagent-router 标记块,无需移除。")
+        return 0
+    bak = _backup(cfg_path)
+    new_text = text[: span[0]] + text[span[1]:]
+    new_text = re.sub(r"\n{3,}", "\n\n", new_text).rstrip() + "\n"
+    if not new_text.strip():
+        os.remove(cfg_path)
+        print(f"config.toml 已为空,已删除;备份: {bak}")
+        return 0
+    with open(cfg_path, "w", encoding="utf-8") as f:
+        f.write(new_text)
+    print(f"已移除标记块: {cfg_path}")
+    print(f"备份: {bak}")
+    return 0
+
+
+def cmd_init_json(args):
+    target = args.path or os.path.join(PLUGIN_ROOT, "subagent-models.json")
+    if os.path.exists(target) and not args.force:
+        print(f"{target} 已存在;加 --force 覆盖。", file=sys.stderr)
+        return 1
+    with open(target, "w", encoding="utf-8") as f:
+        json.dump(DEFAULT_CONFIG, f, ensure_ascii=False, indent=2)
+        f.write("\n")
+    print(f"已写出默认配置: {target}")
+    return 0
+
+
+# ---------------------------------------------------------------------------
+# MCP stdio 服务(newline-delimited JSON-RPC 2.0)
+# ---------------------------------------------------------------------------
+
+MCP_TOOLS = [
+    {
+        "name": "route_subagent_model",
+        "description": (
+            "Resolve a subagent type to the model alias and thinking tier to dispatch it with, plus ready-to-use "
+            "Agent tool arguments. Call this immediately before every Agent/AgentSwarm dispatch: the mapping lives in "
+            "subagent-models.json and the user may have edited it, so never dispatch from a remembered alias. "
+            "The Agent tool has no effort parameter — the thinking tier is bound to the returned alias's default_effort."),
+        "inputSchema": {
+            "type": "object",
+            "properties": {
+                "subagent_type": {
+                    "type": "string",
+                    "description": "Subagent name, e.g. cheap-coder / cheap-explore / k2.8-medium / k2.8-debug / k3-planner / k3-max / doc-writer, or any custom key in subagent-models.json",
+                },
+                "task": {
+                    "type": "string",
+                    "description": "Optional task text, recorded for debugging",
+                },
+            },
+            "required": ["subagent_type"],
+        },
+    },
+    {
+        "name": "list_subagent_routes",
+        "description": "List every subagent -> model alias / thinking tier mapping and which config file it came from. Use it when route_subagent_model reports an unknown subagent type.",
+        "inputSchema": {"type": "object", "properties": {}},
+    },
+    {
+        "name": "doctor",
+        "description": "Check subagent-models.json against config.toml: alias registration, [secondary_model] pool membership, effort tiers. Use it when a dispatch fails or a route looks wrong.",
+        "inputSchema": {"type": "object", "properties": {}},
+    },
+    {
+        "name": "reload_routes",
+        "description": "Re-read subagent-models.json and report how many routes are live. Every tool call already re-reads the file, so this is only an explicit confirmation.",
+        "inputSchema": {"type": "object", "properties": {}},
+    },
+]
+
+
+def _mcp_result(payload, is_error=False):
+    result = {"content": [{"type": "text", "text": json.dumps(payload, ensure_ascii=False, indent=2)}]}
+    if is_error:
+        result["isError"] = True
+    return result
+
+
+def handle_mcp_call(name, arguments):
+    args_obj = arguments or {}
+    if name == "route_subagent_model":
+        st = args_obj.get("subagent_type", "")
+        routes, source, path = load_routes()
+        entry = routes.get("subagents", {}).get(st)
+        if entry is None:
+            return _mcp_result({
+                "error": f"Unknown subagent type: {st}",
+                "hint": "Call list_subagent_routes to see the live names.",
+                "known_subagent_types": sorted(routes.get("subagents", {}).keys()),
+            }, is_error=True)
+        model = entry.get("model")
+        dispatch_args = {
+            "subagent_type": st,
+            "prompt": "<self-contained task brief: context, goal, files in scope, acceptance criteria, expected output>",
+            "description": "<3-5 word summary>",
+        }
+        notes = ["The Agent tool has no effort parameter: the thinking tier is bound to this alias's default_effort."]
+        if model:
+            dispatch_args["model"] = model
+        else:
+            notes.append("model is null: omit the model argument so [secondary_model].default_model takes over.")
+        return _mcp_result({
+            "subagent_type": st,
+            "model": model,
+            "effort": entry.get("effort"),
+            "description": entry.get("description"),
+            "config_source": source,
+            "dispatch": {"tool": "Agent", "arguments": dispatch_args},
+            "notes": notes,
+        })
+    if name == "list_subagent_routes":
+        routes, source, path = load_routes()
+        return _mcp_result({
+            "config_file": path,
+            "config_source": source,
+            "default_model": routes.get("default_model"),
+            "subagents": routes.get("subagents", {}),
+        })
+    if name == "doctor":
+        class _A:  # noqa: D401 - 复用 CLI 的 doctor
+            home = None
+            json = True
+        issues, source, cfg_path = run_doctor(None)
+        return _mcp_result({
+            "config_file": cfg_path,
+            "config_source": source,
+            "ok": not any(l == "error" f
…… diff 过长已截断 ……