# Expert Council

[English](README.md) | [简体中文](README.zh-CN.md)

Expert Council 是一个面向 Pi 与 Codex 等 MCP 宿主的本地、多模型、成本感知专家编排系统。它会发现 Pi 当前注册的 LLM API 与 Coding Plan 中连接的模型，将运行时元数据与用户定义的计费策略、能力画像和本地可靠性数据结合，动态组建一个精简的语义专家团队，并通过 Pi 执行有明确边界的任务，最后向主代理返回紧凑的结构化结果。

主要优势：

| 优势 | 说明 |
|---|---|
| 省钱 | 灵活运用所订阅的 Plan 和 LLM API，根据任务难度自动调配最合适的模型 |
| 快速 | 可同时并发多个最合适的模型进行工作，快速完成仓库探索与上下文压缩 |
| 更安全 | 为不同专家分配不同的只读/可写权限，可写专家在 Git worktree 写入后经主代理审查后并入主分支 |
| 上下文节省 | 主代理不再需要包含过多工具调用产生的冗长上下文，只接收专家返回的摘要化处理结果 |

Expert Council在首次运行时会调用网络聚合搜索模型能力评价刻画当前可用的模型能力画像； 
在实践中发现，理论上最强的模型不一定是最合适的执行者。一个工具调用稳定、Shell 行为可靠、边际成本较低的模型，可能比更强但执行不稳定的模型拥有更高的实际任务价值。推荐配置执行能力强、成本较低的模型作为主代理，当遇到复杂问题时 Expert Council 可派遣强思考能力模型进行审查或 Plan。

## 当前状态

当前版本（0.8.6）已包含：

- **交互式专家**：运行中专家可在重大、难回退或方向含糊处暂停，通过 `request_decision` 向主代理给出 2-4 个推荐选项（含可选自由文本）；宿主用 `expert_respond` 回答，专家在**同一会话**继续。非终止——与 `report_and_stop` 区分。
- **动态工具权限**：预设角色工具是**种子而非上限**。专家用 `request_tool` 申请缺失工具；宿主授予 `once`（用一次后自动撤销）/`persistent`（本会话）/`reject`。`security.toolGrants` 提供运维侧持久按角色授予。只读执行永不升级为变更/shell 工具（隔离保证）。
- **交互经正确性通道发现**：未决交互以 `pendingInteraction` 出现在 `expert_status(view:"running")` 与 `expert_result(includeProgress)`；原生 Pi 包还会在交互一打开时用 `expert-council-interaction` 通知唤醒宿主；无法接收推送的无头宿主（Codex/MCP）继续轮询。每执行有界（默认 3 轮 + 等待超时）。
- **跨语言环境**：`provisionWorkspace` 不再只支持 Node。驱动注册表从各工具链已有的全局下载缓存物化 node/pnpm/yarn/bun、python(uv/poetry/宿主 venv)、rust、go、jvm/maven、dotnet、ruby、php、elixir。环境即代码后端（`flake.nix`、`.devcontainer/`）被检测并交还委托而非重造。新增 `security.workspaceProvisioning.strategy`（auto/drivers/as-code/in-place）与 `runtimeEnv`（isolated/host-env）；并发 worktree 绝不共享重编译 target 目录。
- **进度可观测开关**：`security.observability.expertWindow` 决定能看到专家实时工作的多少——`off`（默认，不环境播报）、`events`（running 视图浮现轻量进度），或 `interactive`：此外写入一份**可让运维者在另一个终端跟随的实时事件流**，用 `expert-council watch --exec ID --follow` 查看，**完全不占用主代理上下文**。`security.observability.streamToHost` 可关流，`redactToolArgs`（默认 `true`）决定工具调用是否只按名称记录。三者都不约束 `pendingInteraction` 正确性通道，也不约束宿主显式索取的 `expert_result(includeProgress)` 快照。
- **挣扎检测**：`security.guardrails` 统计运行时**实际观测**到的工具失败与预算消耗，以 `attention` 呈现在运行视图并为主代理发一条原生通知，每次执行最多 steer 专家两次，且**从不中止**。`maxTotalWallMs` 为一次委派的全部尝试设总时长上限；`executionMetadata.attemptHistory` 说明每次尝试做了什么。
- 与宿主无关的 Core：配置校验、模型归一化、按模型/供应商计费倍率、画像分层、角色评分、任务分类、动态团队规模、重试/升级和遥测聚合。
- 基于 Pi 当前 `ModelRuntime` 与 `createAgentSession` API 的执行运行时。
- 每个专家会话的硬工具白名单和已安装 Skill 过滤。
- 写入型专家的独立 Git worktree 隔离，及按仓库锁文件的**自动依赖供给**（npm/pnpm/bun/yarn，另有 uv/poetry/cargo 等驱动）：在工具链支持的场合抑制安装期脚本执行、白名单化子进程环境、按执行复用工作树；无法抑制的驱动会明确声明并把残余暴露作为 limitation 交给主 Agent；Python 生态自动暴露宿主 `.venv` 解释器绝对路径。
- **运行时验证门**：供给完成后自动运行仓库 typecheck 与测试，失败把成功结果降级为 `partial` 并触发修正重试，全程零主代理开销。
- **专家主动停止工具 `report_and_stop`**：专家判定任务无法完成（缺工具/缺环境/权限拒绝）时立即提交结构化报告（阻塞原因、发现、风险、建议下一步）并以 `partial` 结果终止，委派循环不重试不升级，变更工作树保留待检。
- **专家与主会话同生命周期**（`security.expertLifetime`，默认 `host-bound`）：主会话退出或被替换时自动中止所有运行中的专家，孤儿专家不再烧额度；`detached` 可恢复旧行为。
- **失败结果保留产物**：可写执行下，超时、会话错误、抛错失败的结果都附带 `filesChanged` 与最后助手输出，长任务超时不再返回空结果。只读执行则正确地不上报任何 `filesChanged`。 来自**只读**角色的 `partial` 结果现在是直接交付、而不是重跑一遍（缺陷 #32）：要不要重试本该由主 Agent 判断，Council 不再替它把整份预算花第二次，而是带着一条 risk 说明交付这份不完整的答复。可写角色的 partial 照旧升级。调用 `report_and_stop` 主动停止的专家不受此规则影响：它照旧直接终止、不再重试，因为那是“任务被阻塞”而不是“任务不完整”。
- **持久化写侧钳制**：超长数组与文本在唯一持久化入口截断，啰嗦专家再也不能写出不可重载的 state 文件。
- **`expert_availability_reset`**：按 `*`/供应商/`provider/id` 即时清除误标或已恢复的可用性标记，无需改文件或重启。
- **`expert_verify`**：插件侧在保留 worktree 或受校验工作区运行有界命令并返回真实退出码与输出尾部；`tests[]` 现携带退出码、计数、耗时与输出尾部。
- **`expert_status` 视图**：默认的有界 `summary`（运行中含剩余预算、近期完成、供应商并发槽）、`running`、以及旧全量 `full`，大幅降低观测上下文开销。
- **完成通知可靠性**：Pi 扩展在 idle/streaming 竞态时短暂重试完成消息，不再静默丢弃。
- `timeoutMs` 与 `reasoningLevel` 均为必填派遣参数：主代理必须按任务与模型显式选择；批量派遣时每一项自带这一对参数，仅单次委派从顶层读取；组合名单可按角色/模型钉定思考档位并覆盖派遣参数。
- 专家 fail-fast 纪律：发现工具或环境不可能完成任务时立即以结构化 `missing_context`/`permission_error` 终止并实时回传，委派循环不做无谓重试。
- 持久化理事会组合：`council-compositions.json` 命名名单（角色可配多模型与思考档位）、首问组合菜单、会话绑定、`model` 钉定与同角色并发派遣。
- 供应商限额与并发：`route-policy.json` 按供应商设置日/周加权 token 上限（`usage-ledger.json` 按 `costMultiplier` 记账）与并发上限，超限候选自动排除并给出原因。
- 运行时可用性标记分级：模型失效（24 小时）、配额周期耗尽（6 小时、全 plan 传播）、**瞬时限流 TPM/RPM（2 分钟）** 三档 TTL，自动过期重试，不再把限流误当配额耗尽拉黑。
- `council-config.json` 运营者配置默认在数据目录发现（无需环境变量），`expert_inspect` 返回路径、生效的供给模式与进度可观测设置，供主代理代为编辑。
- 支持 JSON 输出的 CLI。
- 包含 13 个异步语义工具、事件驱动完成等待、验收反馈闭环及显式 worktree 清理能力的 MCP Server。
- 原生 Pi Package。
- 提供商会话错误透传：`403 AccessDenied` 等上游拒绝不再被吞掉，会以真实诊断和正确失败类型返回主代理。
- 跨进程共享模型评估：多实例并行时，可用性标记无需重启即可互相可见。
- 自包含的 Codex 插件，内置共享 Skill、stdio MCP Server 与经过验证的 Pi SDK 运行时，通过 Codex 宿主自有的 `codex/sandbox-state-meta` 能力发现工作区（无需 hook 或全局 SDK 解析）。
- 不会消耗模型额度的确定性自动化测试。

## 架构

```text
Codex 或 Pi 主代理
        |
        | 语义工具 / 共享 Skill
        v
 Expert Council Core
 - 资源与模型归一化
 - 计费与能力画像
 - 确定性路由
 - 角色与团队规模
 - 重试与升级
 - 遥测聚合
        |
        v
     Pi Runtime
 - 可调用模型发现
 - 工具硬白名单
 - 已安装 Skill 过滤
 - 有边界的专家会话
 - 工作区隔离
     /     |      \
   CLI  Pi Package  MCP Server
                        |
                   Codex 插件
```

TypeScript project references 保证依赖只能按以下方向流动：

```text
core <- pi-runtime <- cli
                   <- mcp-server <- codex-integration
                   <- pi-package
```

Core 不导入 Pi、Codex、MCP transport、文件系统、Shell 或进程 API。CLI、MCP Server 和宿主分发层使用的是同一套服务与路由逻辑。

## 快速开始

环境要求：

- Node.js 22.19 或更高版本。
- npm 11 或兼容版本。
- 已安装并配置至少一个可用模型的 Pi。
- 当写入型专家需要 worktree 隔离时，Git 仓库必须至少有一个提交。

### 安装 Pi Package

从 npm 安装（推荐）：

```bash
pi install npm:@expert-council/pi-package
pi list
pi --verbose
```

`pi list` 应显示 `npm:@expert-council/pi-package` 及其解析后的目录；新启动的 verbose Pi 会话应加载 `dist/extension.js`、`expert-council` Skill 和 12 个语义工具。后续升级：

```bash
pi update npm:@expert-council/pi-package
```

### 安装 Codex 插件（可选）

若要由 Codex 担任主代理，可直接从 Git Marketplace 安装固定版本的预构建插件，无需克隆仓库或在本地构建：

```bash
codex plugin marketplace add Labiey/expert-council-router --ref v0.8.6 --json
codex plugin add expert-council@expert-council-router --json
```

安装后请完全重启 Codex Desktop。插件自带经过测试的 Pi SDK 运行时，运行时不依赖全局安装的 `pi` 包；但仍需至少安装并配置过一次 Pi（或手动把提供商凭据放入 `~/.pi`），账号与模型目录才可用。Windows CLI 定位、验证、升级和卸载步骤见 [Codex 插件](#codex-插件)。

### 从源码构建（开发）

```bash
npm install
npm run build
npm test
```

只发现模型，不调用任何模型：

```bash
node packages/cli/dist/bin.js models --json
node packages/cli/dist/bin.js inspect --json
```

只构建专家团队，不执行专家：

```bash
node packages/cli/dist/bin.js build "修复设备热插拔竞态问题" --max-experts 4 --json
```

只有在你确定需要实际调用 Pi 模型时才执行委派：

```bash
node packages/cli/dist/bin.js delegate architecture-oracle "分析并发调用路径" \
  --workspace /path/to/repo --timeout-ms 600000 --reasoning-level high --json
```

零配置模式会使用保守的能力默认值，将无法确认的计费类型标记为 `unknown`，并拒绝未隔离的写入操作。它不会猜测某个 API 是免费的，也不会根据模型名称臆测其能力强弱。

## 模型发现

`PiExpertRuntime` 调用 Pi 的 `ModelRuntime.getAvailable()`，而不是使用硬编码模型列表。仅仅出现在模型注册表或用户画像中的模型不会自动进入路由；只有 Pi 报告当前可调用的模型才会被使用。

运行时会归一化以下信息：

- Provider 与模型 ID；
- 显示名称；
- 推理支持以及 Pi 暴露的推理等级映射；
- 上下文窗口和最大输出；
- 输入模态；
- 已发布的 API 价格字段；
- 安全的兼容性元数据。

运行时会依次尝试解析本地兼容 Pi SDK、`PI_CODING_AGENT_MODULE` 指定目录，以及全局 npm Pi 安装。若全部失败，会返回可操作的诊断信息，而不是伪造模型列表。

Pi 在每个会话内只构建一次这份清单，且提供商目录可能保留失效的模型名称，因此 `listAvailableModels()` 可能包含一个上游已无法服务的模型。在真实调用失败之前，路由会把它视为可调用；这正是带失效模型证据的失败会在持久化模型评估中标记该模型不可用的原因（见下文路由一节）。标记会在 24 小时后过期，恢复的模型会自动重新被尝试。

## 配置

通过 `EXPERT_COUNCIL_CONFIG` 环境变量，或 CLI 的 `--config PATH` 参数指定配置文件。可以从 [`config/examples/balanced.example.json`](https://github.com/Labiey/expert-council-router/blob/main/config/examples/balanced.example.json) 开始。

画像优先级为：

```text
内置保守默认值
  < 用户配置或可选预设
  < 当前任务运行时覆盖
```

客观运行时元数据单独合并。本地结果数据只有在积累至少 3 个样本后才会影响路由，而且调整幅度受 `routing.localLearningMaxAdjustment` 限制。显式用户配置始终拥有更高权威。

### 计费策略

支持以下计费类型：

```text
subscription  metered  quota  free  unknown
```

边际成本以单一数值 `costMultiplier` 表示，因为公开 Token 单价无法表达订阅计划、固定额度、本地推理和促销额度。Pi Runtime 适配器会把运行时明确报告的订阅或具名 Token Plan 目录识别为 `subscription`；否则，Pi 模型目录存在非零单价的 Provider 识别为 `metered`，没有可靠证据的保持 `unknown`。`expert_inspect` 会返回推断来源，显式用户配置始终具有最高优先级。同一按量 Provider 内的模型仍会通过 `routing.apiPriceWeight`（默认 `0.35`）比较具体单价；全零价格表按“未提供”处理，不会猜测为免费。

**按模型的计费条目。** 订阅 token plan 带有周期配额（常见为周限额）且各模型消耗倍率不同，单一 provider 级权重无法表达真实边际成本。为特定模型增加 `provider/id` 键即可覆盖 provider 默认值——路由先查显式 `billingProfile`，再查模型级条目，最后才是 provider 级。同样的键也可用于 `model-assessment.json` 的 billing 段与用户配置：

```json
{
  "billing": {
    "providers": {
      "subscription-provider": {
        "billingType": "subscription",
        "costMultiplier": 0.1
      },
      "subscription-provider/qwen3.8-max": {
        "billingType": "subscription",
        "costMultiplier": 2.0
      },
      "scarce-provider": {
        "billingType": "quota",
        "costMultiplier": 5.0
      }
    }
  }
}
```

### 计费倍率

`costMultiplier` 是用于成本评分与 Provider 额度计账的相对 Token 消耗权重，省略时默认为 `1.0`，取值范围为 `0.01`–`100`。数值越低，在成本权重较高的角色中越经济；成本效率得分为 `10 / (1 + costMultiplier)`（在计费类型加成之前）。参考值：按量 flash 级 ≈`1.0`，按量旗舰全尺寸 ≈`5.0`，token plan flash 级 ≈`0.1`，token plan 旗舰 ≈`2.0`。

已移除的档位词汇仍会被接受并确定性映射，旧配置继续可用：

| 旧 `marginalCostClass` | `costMultiplier` |
|---|---|
| `very-low` | `0.1` |
| `low` | `0.5` |
| `normal` | `1.0` |
| `high` | `3.0` |
| `scarce` | `5.0` |

旧的 `usagePreference` 字段会被忽略并丢弃，不再影响路由。

### Provider 限额与并发

`route-policy.json` 可在模型 allow/deny 策略旁携带可选的 `providers` 映射——allow/deny 语法见[路由策略文件](#路由策略文件)，本节不再重复：

```json
{
  "version": 1,
  "providers": {
    "qwen-token-plan-cn": { "maxConcurrency": 2, "dailyTokenCap": 5000000, "weeklyTokenCap": 40000000 },
    "zai": { "maxConcurrency": 0 }
  }
}
```

- `maxConcurrency`（整数 ≥ 0）限制该 Provider 同时运行的执行数；`0` 或省略表示不限制。
- `dailyTokenCap` 与 `weeklyTokenCap`（整数 > 0）限制每个 UTC 自然日与每个 ISO 周（周一为起点，UTC）的加权 Token 消耗。默认值为每日 `20,000,000`、每周 `150,000,000`。
- 计账是加权的：每次尝试消耗该模型 `(inputTokens + outputTokens) × costMultiplier`；不计算缓存读写 Token。
- 触顶会在持久化评估中标记该 Provider 的所有模型，直到下一个 UTC 重置边界——每日触顶为下一个 UTC 零点，每周触顶为下周一 `00:00` UTC——路由因此停止在已耗尽的 Provider 上浪费尝试，并在重置后自动重试。
- 在途计数使用分配给该 Provider 的运行中执行；达到 `maxConcurrency` 的 Provider 在新候选中被排除，直到其中一个完成。
- 未接入 usage ledger 时，额度与并发限制均禁用，Core 不进行任何 I/O。

`expert_inspect` 会为每个 Provider 返回 `providerLimits`，包含 `maxConcurrency`、两项额度、加权 `usedToday`/`usedWeek`、`remainingDaily`/`remainingWeekly` 与 `inFlight`。

`config/examples/` 提供以下示例：

- `balanced.example.json`：适用于零配置的保守策略。
- `subscription-heavy.example.json`：优先消耗订阅资源，保护稀缺额度。
- `metered-quality.example.json`：区分经济型与高质量按量 API。
- `qwen-glm.example.json`：明确标记为假设性用户偏好的示例，不代表客观评测结论。

### 运营者配置（council-config.json）

`council-config.json` 是运营者配置文件。它可选且默认在共享数据目录（与 `model-assessment.json`、`route-policy.json` 同层）自动发现——不需要任何环境变量。解析顺序：

1. `configPath` 选项或 `EXPERT_COUNCIL_CONFIG` 环境变量——最高优先，且文件**必须**存在；
2. `<数据目录>/council-config.json`——存在则读取，不存在则静默跳过；
3. 否则全部走内置默认（`workspaceProvisioning.mode = "none"`）。

文件接受完整的运营者 schema：`security`、`billing`、`profiles`、`routing`。典型示例：

完整的标准写法如下，每个键都停在它的内置默认值上。**所有键都可以省略**——文件里可以只写一行，没写的部分就按下面的值兜底：

<!-- council-config:complete -->
```json
{
  "billing": {
    "providers": {}
  },
  "profiles": {
    "models": {}
  },
  "routing": {
    "maxExperts": 4,
    "minimumWorkerToolReliability": 4,
    "localLearningMaxAdjustment": 1,
    "apiPriceWeight": 0.35,
    "roleWeights": {},
    "diversity": {
      "repeatedModelPenalty": 0.35,
      "reviewerSameProviderPenalty": 0.25,
      "reviewerSameFamilyPenalty": 0.5
    },
    "taskClassification": {
      "tinyMaxWords": 8,
      "tinyMaxCjkChars": 18,
      "complexMinWords": 35,
      "complexMinCjkChars": 60,
      "complexSignalThreshold": 2
    }
  },
  "retry": {
    "maxAttempts": 3,
    "maxEscalations": 2,
    "correctedRetriesPerModel": 1
  },
  "security": {
    "workspaceStrategy": "auto",
    "allowInPlaceMutations": false,
    "allowedWorkspaceRoots": [],
    "trustedSkills": [],
    "worktreeRetentionMs": 86400000,
    "toolGrants": {},
    "expertLifetime": "host-bound",
    "observability": {
      "expertWindow": "off",
      "streamToHost": true,
      "redactToolArgs": true,
      "autoOpenWindow": false,
      "recordReasoning": false,
      "contentStream": "none",
      "contentByRole": {},
      "contentWindowLines": 10,
      "contentWindowChars": 120,
      "contentEventBytes": 65536,
      "contentFileBytes": 10485760,
      "contentTotalBytes": 209715200
    },
    "guardrails": {
      "warnHost": true,
      "nudgeExpert": true,
      "consecutiveToolFailures": 3,
      "minCallsForRatio": 8,
      "failureRatio": 0.5,
      "budgetFractions": [0.6, 0.85]
    },
    "workspaceProvisioning": {
      "mode": "none",
      "strategy": "auto",
      "runtimeEnv": "isolated",
      "timeoutMs": 600000,
      "maxConcurrent": 1,
      "scrubEnv": true,
      "removalTimeoutMs": 300000
    }
  }
}
```

provider 与 model 的键名必须和 `expert_inspect` 报告的完全一致，即 `"provider"` 与 `"provider/id"`。下面是大多数人真正会改的两段记录（`acme-*` 只是虚构示例，不是推荐值）：

<!-- council-config:variant -->
```json
{
  "billing": {
    "providers": {
      "acme-plan": { "billingType": "subscription", "costMultiplier": 0.1 },
      "acme-metered": { "billingType": "metered", "costMultiplier": 1 },
      "acme-host": { "billingType": "quota", "costMultiplier": 5, "disabled": false }
    }
  },
  "profiles": {
    "models": {
      "acme-plan/acme-flash": {
        "reasoning": 7,
        "planning": 7,
        "architecture": 6.5,
        "coding": 7,
        "debugging": 7,
        "review": 6.5,
        "longContext": 7.5,
        "toolReliability": 7,
        "bashReliability": 7,
        "autonomousExecution": 7,
        "speed": 8.5,
        "preferredReasoningByRole": { "scout": "low", "architecture-oracle": "high" },
        "incompatibleRoles": [],
        "billingProfile": "acme-plan",
        "disabled": false
      },
      "acme-metered/acme-pro": {
        "coding": 9,
        "debugging": 9,
        "toolReliability": 9,
        "bashReliability": 9,
        "autonomousExecution": 9
      }
    }
  }
}
```

字段说明：

- `security.workspaceProvisioning.mode`——`"none"`（默认，不安装）、`"auto"`（按锁文件探测生态自动安装）或 `"custom"`（原样运行 `command`）。
- `security.workspaceProvisioning.verifyCommand`——单条命令的扁平 argv 数组，供给后的变更型专家完工时以它替代默认的「先 typecheck 后 test」。
- `security.workspaceProvisioning.scrubEnv`——为 `true`（默认）时，供给与验证子进程只收到白名单环境；`~/.npmrc` 的 registry 令牌仍可能到达子进程（已记录的残余风险）。
- `security.worktreeRetentionMs`——供给后的工作树保留多久供审查（默认 24h）。
- `billing` / `profiles` / `routing`——与 `model-assessment.json` 计费条目、模型能力画像、角色权重相同的 schema。

**主代理编辑契约**：`expert_inspect` 在 `operatorConfig` 下返回文件位置与生效的供给模式。当用户要求调整供给行为时，主代理直接编辑该文件，并告知用户需要重启宿主会话才能生效。JSON 格式错误或非法枚举会在启动时硬报错（运营者笔误绝不会静默关闭安全设置）。

### 工作树供给与验证门

变更型工作树从已提交的 `HEAD` 创建，天然不含未追踪的本地产物。运行时可在专家开工前用仓库自身提交的锁文件为其安装依赖；该功能默认关闭：

```json
{
  "security": {
    "workspaceProvisioning": {
      "mode": "auto",
      "timeoutMs": 600000,
      "maxConcurrent": 1,
      "scrubEnv": true,
      "removalTimeoutMs": 300000
    }
  }
}
```

该配置保存在共享数据目录（与 `model-assessment.json` 同一层）的 `council-config.json` 中。文件可选且默认自动发现——不需要任何环境变量：存在则读取，不存在则全部走默认。`expert_inspect` 会在 `operatorConfig` 下返回其路径，主代理可按用户需求代为编辑（修改在宿主会话重启后生效）。显式的 `configPath` 选项或 `EXPERT_COUNCIL_CONFIG` 环境变量仍优先，且必须指向已存在的文件。

- `mode` 取 `none`（默认，永不预置）、`auto`（检测仓库提交的锁文件并安装）或 `custom`（把 `command` 原样作为 argv 数组运行）。
- `auto` 运行 `pnpm install --frozen-lockfile --prefer-offline`、`npm ci --prefer-offline --no-audit --no-fund` 或 `bun install --frozen-lockfile`，三者都附加 `--ignore-scripts`。yarn 是刻意的例外：Berry 已把该 flag 从 CLI 移除，加上它只会让安装报错而非加固，因此改由子进程环境提供 `YARN_ENABLE_SCRIPTS=false` 与 `YARN_IGNORE_SCRIPTS=true`，一次覆盖两代 yarn。注册表还会用各工具链自己的全局下载缓存物化 python（`uv sync`、`poetry install`，或宿主 `.venv` 解释器）、rust、go、jvm/maven、dotnet、ruby、php、elixir；这些驱动并不都有等价开关，所以每个驱动各自声明能否抑制构建脚本——用了抑制不了的驱动时，向主 Agent 上报一条 limitation，而不是假装没有这回事。声明了 environment-as-code 后端（`flake.nix`、`.devcontainer/`）的仓库会被检测并交还给它，从不另起炉灶重新实现。
- 只有变更型工作树会被预置；只读角色在主工作区运行，永不预置。
- 子进程环境经过白名单 scrub（`PATH`、`HOME`、`USERPROFILE`、`APPDATA`、`LOCALAPPDATA`、`TEMP`、`TMP`、`SYSTEMROOT`、`SYSTEMDRIVE`、`COMSPEC`、`PROGRAMFILES`、`PROGRAMDATA`、`GIT_*`、`npm_config_registry`、`npm_config_cache`）；API token 与云凭据不会被转发。驱动自带的抑制设置项在 scrub **之后**合并，因此既不会被白名单擦掉，父进程里的同名值也无法反向覆盖议会的意图。
- 预置为 `ready` 时，若配置了 `verifyCommand` 就运行它，否则先 `npm run typecheck` 再 `npm test`。验证门失败会把本已成功的专家结果降级为 `partial`（`failureType: "test_failure"`）。
供给完成后验证门自动运行仓库的 typecheck 与测试命令；门失败会把本已成功的结果降级为 `partial`（`failureType: "test_failure"`），从而触发既有修正重试路径。

### 可观测性与专家窗口

`security.observability` 决定能看到专家实时工作的多少。它与 `pendingInteraction` 通道有意分开：后者是正确性面，永远上报。

```json
{
  "security": {
    "observability": {
      "expertWindow": "interactive",
      "streamToHost": true,
      "redactToolArgs": true
    }
  }
}
```

| `expertWindow` | 得到什么 |
| --- | --- |
| `off`（默认） | 无环境播报：running 视图只有已用/剩余预算。 |
| `events` | 在此基础上，`expert_status` running 视图多出有界的实时进度块（`messageCount` 与最后一条助手输出），需 `streamToHost` 为 true。 |
| `interactive` | 在此之上再写入一份事件流，运维者可在**另一个终端**跟随，且不花费主代理任何上下文。 |

事件流位于 `<数据目录>/observability/<executionId>.jsonl`，一行一个 JSON 对象：`started`、`tool_started`、`tool_finished`、`assistant_text`、`interaction_opened`、`interaction_answered`，最后恰好一个 `stopped` / `completed` / `failed`。跟随方式：

```bash
expert-council watch --exec exec_abc123 --follow
```

- `--follow` **一定会退出**：遇到终止事件、超过 `--timeout-ms`（默认 300000）、或文件消失。它按字节偏移量追读，绝不输出未写完的半行。`--json` 输出原始对象。
- 专家叙述被压成每事件一行的有界文本；磁盘上不会有工具输出，也不会有思考链。
- `redactToolArgs`（默认 `true`）只记录工具名；设为 `false` 会额外写入有界的入参摘要，内容是专家传了什么——包括文件路径与完整命令行。仅在本地文件里留下这些值也可接受的场合才关闭。
- 事件流文件 7 天后自动清理。
- 事件流由**运行专家的那个进程**写入。同一数据目录下的 `watch` 才能看到它；换一个无关仓库去跟是看不到的。
- `expert_inspect` 会报 `runtimeCapabilities.eventStream`，主代理由此能判断所请求的档位是否真可用；写入能力缺失时 `interactive` 降级为 `events`，并在 `warnings` 里说明。
- `watch` 在 Council 写出委派级终止标记（`delegation_final`）时关闭，并把收尾报成 `delegation finished`，而不是拿第一次尝试的终止事件当结局。之所以要区分，是因为重试或升级后的委派**每次尝试都会写一个终止事件**：见到第一个就停的观察者会在失败那一刻关窗，永远看不到真正完成任务的那次尝试。旧运行时的流、或进程已死的流，改为在足够长的静默之后才关闭（默认 15 秒，可用 `--quiet-ms` 调整）——沉默不等于死亡：专家在工具调用之间会思考数秒，一次构建更能安静几分钟。这条兜底只在**已经看到终止事件**之后生效；从未出现过终止事件的流改由 `--timeout-ms` 兜底，因为刚启动的专家在模型思考时本来就可以什么都不写。`--timeout-ms` 始终给等待设上限。
- 每个事件都带上产生它的那次尝试序号，重试后的尝试渲染成 `[role model #2]`，于是升级本身在窗口里就看得见。事件渲染被强制压成一行：一条事件吐出第二行会让运维者的 tail 与流失步。

### 自动打开观察窗口

默认什么都不弹。`security.observability.autoOpenWindow` 是一个显式的 opt-in：开启后，每次委派会拥有
一个自己的终端，跑的正是上面那条命令——和你亲手开一个窗口看到的完全一样，因此主 Agent 依然不为此花任何上下文。

```json
{
  "security": {
    "observability": {
      "expertWindow": "interactive",
      "autoOpenWindow": true
    }
  }
}
```

- **一次委派一个窗口，而不是一次尝试一个。** 重试或升级沿用同一个 execution id 与同一个流文件，否则会闪出
  第二个窗口只给你看半截工作。委派写到最终标记时，该窗口记录被释放。
- **不限制同时开启的窗口数。** 三个专家在跑就是三个窗口——数量是操作员的事，不是议会的事。
- **专家结束不会关掉窗口。** 跟随器在 `delegation_final` 停止滚动，然后等你按键，你可以慢慢往回翻看整段
  过程，再由你决定它何时消失。
- **窗口自身的超时跟随专家预算**（1.5 倍，下限 15 分钟，上限为 CLI 自身的 60 分钟），所以观察者不可能比
  它所观察的工作先过期。
- **窗口只读。** 决策请求与工具授权仍然回到宿主，因此能回答专家的地点始终只有一个。
- **只有这个配置项能开窗。** 任何工具参数、专家输出或任务文本都触发不了它，任务内容也不会进入命令行：
  参数只有 execution id、角色名与若干数值选项。

开窗是运行时唯一一次**代替操作员启动本机进程**：它启动的是操作系统的终端宿主（装有 Windows Terminal 时用
它，否则退回经典控制台），运行的是本项目自带的 CLI 跟随器。若找不到终端宿主或 CLI，或平台不是 Windows，
那就什么都不弹、委派完全不受影响，并且 `expert_inspect` / `expert_status` 会给出**一条**说明回退原因的
limitation。要把观察者指向特定的 CLI 构建，设 `EXPERT_COUNCIL_CLI`；要指定终端宿主，设 `EXPERT_COUNCIL_TERMINAL`。

手动路径对所有人继续有效：在另一个终端跑 `expert-council watch --exec <execution-id> --follow`；这也是
回看一次已完成委派的方式——流文件在 7 天清理前一直在盘上。
### 观察者记录什么（内容挡位）

默认情况下，事件流只记录**名字与计数**：跑了哪个工具、成功没有、用了几次；它不记录工具返回了
什么。这是一次刻意的暴露面选择，而 `security.observability.contentStream` 是你主动放宽它的地方
——五个递增挡位，让你自己挑风险，而不是被一个复选框替你决定：

| 挡位 | 落盘内容 | 为什么选它 |
| --- | --- | --- |
| `none`（默认） | 名字、计数、结局标记 | 除了进程内信息，什么都不出去 |
| `assistant` | assistant 文本，随生成随记 | 想看叙述思路，但不想把文件内容也存下来 |
| `assistant+tool-tail` | 上面这些，外加每个工具结果的**尾部** | 只看得到失败那一行，不必存整份文件 |
| `transcript` | 上面这些，外加**完整**工具结果（带可见接缝） | 事后复现专家当时看到的东西 |
| `transcript+args` | 上面这些，外加完整工具**入参** | 连 shell 命令与路径都有——调试价值最高，也是唯一可能记下密钥的一挡 |

```json
{
  "security": {
    "observability": {
      "expertWindow": "interactive",
      "autoOpenWindow": true,
      "contentStream": "transcript",
      "contentByRole": { "debugger": "transcript", "reviewer": "assistant" },
      "contentWindowLines": 10,
      "contentWindowChars": 120,
      "contentEventBytes": 65536,
      "contentFileBytes": 10485760,
      "contentTotalBytes": 209715200
    }
  }
}
```

- **挡位解析顺序**：内置 `none` < `contentStream` < `contentByRole[角色]` < `EXPERT_COUNCIL_CONTENT`。
  只有配置与环境变量能开启：任何工具参数、专家输出、任务文本都开不了它——这是隐私开关，不是性能开关。
  `contentByRole` 里的角色名会拿真实角色表校验，所以拼错会报出一条**列出合法值**的错误，而不是得到一个
  永远不生效的哑挡。
- **除非你明确要求，否则不记录**：`security.observability.recordReasoning: true`（或单进程用
  `EXPERT_COUNCIL_REASONING=1`），并且**还要**一个会记 assistant 文本的挡位。开启后推理走同样的游标与
  上限，每条都带 `reasoning: true`，呈现时是 `thinks` 而不是 `says`，运行时还会在能力限制里点名这个开关。
  默认关，因为推理是一条消息里体量最大、也最私有的内容，而且很多提供商给的是摘要而不是真实轨迹。
- **默认情况下事件流里没有任何推理。** 只有 `type: "text"` 的内容才可能进入事件流——这是项目红线，不是
  一个会漂移的默认值：开关没开而思维链落了盘，那条专门的测试就变红。
- **窗口显示的内容比存下来的少。** 工具块流式阶段一次最多 3 行；结束记录显示载荷的最后
  `contentWindowLines` 行（默认 10）。叙述在落盘前先合并：真实模型吐出的碎片小到不值得单独记一条，
  所以要等它结束一行或一句、且已攒下约 80 字节才记；一段不间断的长文本到 600 字节上限时释放，
  切点**回退到最近的空白**而不是切进单词；消息结束时把还攥着的余量冲刷出去。真机实测：合并前 2.2 KB
  文本要 129 条记录，合并后 4.2 KB 只要 28 条（平均每条 150 字符），且一个字符都不丢。被藏起的行数与
  被省略的字节数都会**明确说出来**而不是悄悄丢掉，而且标题会给出这条记录在事件流文件里的**行号**，
  所以看全文只差一次跳转：

```bash
sed -n '137p' ~/AppData/Local/ExpertCouncil/observability/exec_abc123.jsonl | jq -r .text
```

- **三道上限，且只管内容**：`contentEventBytes` 限单个载荷（更大的存成"头 + 尾"，中间缺口以字节数写明）、
  `contentFileBytes` 限单次委派的流（默认 10 MB）、`contentTotalBytes` 限整个目录（默认 200 MB，
  按最旧优先淘汰）。撞上上限时丢的是*内容*，并写一条 `stream_truncated` 通知；它**不会**丢掉委派如何结束，
  也**永远不会**让委派失败。正在写入的流永远拥有最新的修改时间，所以目录淘汰不可能删掉你窗口正跟着的文件。
- 文件仍然 7 天后清理；开启记录也不会给主 Agent 增加任何东西：专家返回的仍是那份紧凑的结构化结果。
### 观察窗口的版式

观察窗口按编码 Agent 自己那种记录读法渲染：叙述就是**正常段落文本**（每行不加署名），
每次工具调用是一个**带底色的块**，标题写明哪个工具、被要求跑什么。

```bash
expert-council watch --exec exec_abc123                        # 终端里是面板，被管道接走就是单行
expert-council watch --exec exec_abc123 --style plain          # 每个事件一行，与从前一致
expert-council watch --exec exec_abc123 --style panel --columns 100 --no-color
```

- **`--style auto`（默认）只在终端里选 panel，其他一律 plain**，所以把流重定向到文件时，
  字节与面板出现之前完全一致。`--json` 既不渲染也不上色。
- **块与块之间也要隔开，不只是块与散文之间。** 两条灰带相接会被读成同一个块，所以现在相邻块之间
  会留一行空行，而且标题比自己的正文浅一档底色，即使行相接也看得到接缝。正在运行的工具重绘时
  故意**不拆**——那本来就是一个块在长。
- **观察窗口用的是显式版式。** 启动器在命令行里直接传 `--style panel --color`，不让跟随器去猜有没有
  终端：它开出来的窗口继承的是被忽略的 stdio 句柄，在那里"猜"会报成管道，否则操作员会在真终端里看到
  plain 版式。`--style auto` 仍是给"人坐在自己终端前"用的正确默认。
- **要有块，挡位得真的记工具输出。** `none` 与 `assistant` 没有可放进块的内容，于是每次调用仍是一行
  暗淡的工具名——这正是那两挡的用途：看见活动，但不记内容。从 `assistant+tool-tail` 起，块里才有结果。
- **`$ <命令>` 这个标题需要入参可见**：`redactToolArgs: false` 给一条有界的单行摘要，只有
  `transcript+args` 存完整文本。开着脱敏时标题就只有 `bash`——这是隐私选择，不是渲染能力不足。
- **颜色属于视图，不属于数据。** ANSI 只在终端里发，且 `NO_COLOR` 或 `TERM=dumb` 时一律不发；
  `--color` / `--no-color` 可强制。两种情况下流文件都仍是纯 JSON。把块补到右边缘时，东亚文字按
  每字符两格计算，所以中文叙述行不会让底色短半格。

### 挣扎检测（护栏）

`security.guardrails` 决定 Council 是否会察觉「专家卡住」而不是「只是忙」。检测在设计上**不阻塞、不中止**：误报只浪费一次查看，自动中止会毁掉好成果。

| 选项 | 默认 | 作用 |
| --- | --- | --- |
| `warnHost` | `true` | 统计挣扎信号，并以 `attention` 出现在 `expert_status({ view: "running" })`；每条警告另发一次原生 Pi 通知（`expert-council-guardrail`）。关闭后不再有警告，计数仍会记录。 |
| `nudgeExpert` | `true` | 用一条有界指令 steer 专家本身：不要重复同样失败的调用；要么说明换什么做法，要么 `report_and_stop`，要么 `request_decision`。每次执行**最多 2 次**，且仅当运行时会话支持 steer。 |
| `consecutiveToolFailures` | `3` | 连续失败多少次触发 `consecutive_tool_failures`。 |
| `minCallsForRatio` / `failureRatio` | `8` / `0.5` | 观测到至少 8 次调用后，失败比例达到 50% 触发 `failure_ratio_high`。 |
| `budgetFractions` | `[0.6, 0.85]` | 在本次尝试 `timeoutMs` 的这些分位发 `budget_fraction` 警告；只有最高一档才同时 nudge。 |
| `maxTotalWallMs` | 未设 | **一次委派全部尝试**的总时长上限。未设即保持旧行为；设置后重试循环会提前停止并给出原因，而不会无声地花掉 `retry.maxAttempts × timeoutMs`。 |

每条警告也会以 `attention` 事件写入事件流，运维者在 `expert-council watch` 里能看到。

这些数字来自运行时自己观测到的工具事件，不是专家自述。委派结果的 `executionMetadata` 会带 `toolCalls`、`toolErrors`、`attention`，以及 `attemptHistory`（每次尝试一条有界记录：模型、状态、失败类型、耗时、摘要前 300 字符），因此主代理无需翻状态文件就能看懂「三次尝试为什么花了一小时」。

供应商故障不能当作模型证据。传输类失败（`Connection error.`、`fetch failed`、`502/503/504`、`gateway timeout`、`overloaded`）现在归为 `provider_error` 而非 `unknown`，并且带该失败类型的结果会被排除在喂给路由的可靠性聚合之外——一次断流不会让一个能干的模型看起来不可靠。

### 能力画像

模型可以在以下维度获得 0 到 10 分的用户评分：

```text
reasoning planning architecture coding debugging review longContext
toolReliability bashReliability autonomousExecution speed
```

模型键必须使用 `models --json` 返回的精确 `provider/model`。新发现或没有本地画像的模型会获得保守默认值；不可用模型的残留配置只会产生警告，不会导致路由崩溃。

### 主代理能力审查与首次组建偏好

用户不需要逐个模型维护使用优先级。主代理决定当前任务值得组建委员会后，如果这是新对话中的第一次组建且既未指定 `composition` 也未指定 `costPolicy`，`expert_build` 会返回一份组合菜单，主代理应原样呈现给用户选择：

```text
1. 已保存的理事会组合（最多 3 个，按 council-compositions.json 中的顺序）
2. 自动组建（auto）——再选择一次成本偏好：价格优先（economy）／综合平衡（balanced）／速度优先（speed）
```

选择已保存组合即绑定到当前会话（池内路由，`route-policy` deny 恒胜）；选择 auto 则进入成本偏好流程并解除旧绑定。Pi Package 把成本偏好记录在当前 Pi Session 的隐藏扩展状态中；本对话后续委员会自动复用，除非用户主动改变。`economy` 会增强成本权重，`speed` 会增强经过审查的速度维度，`balanced` 使用正常的角色权重。旧的 `quality` API 值保留兼容，但不会作为默认提问选项。组合的创建与修改直接编辑 `council-compositions.json`（路径见「委员会编成」一节），主代理可代为操作。

模型能力由主代理审查，而不是要求用户手工排序。`expert_inspect` 会返回强制评估门禁：若尚无审查、审查已超过 30 天、可调用模型发生变化，或用户明确要求重新审查，主代理必须使用宿主已经具备的联网工具研究门禁列出的每个可调用模型；完成前 `expert_build` 不会组建委员会。主代理提交一份完整 `modelAssessment`，其中 ISO 时间必须读取宿主真实时钟，来源应合并为 1–12 个 URL，并包含 0–10 能力维度及可验证 Provider 访问/计费方式。未来时间戳会单独报告，修正时间时无需重新联网研究。若检查结果表明已保存评估仍为 `current`，宿主调用 `expert_build` 时应省略 `modelAssessment`；不完整、过期或未来时间的替代表不能覆盖当前有效快照。评估在当前用户数据目录只保存一份，只要可调用模型清单仍兼容，新对话和其他工作区都直接复用；普通计划和执行状态保存只保留全局评估，不会让持有旧内存快照的服务实例覆盖它，只有显式提交并成功通过门禁的新评估才会全局替换旧评分。显式用户计费配置始终高于主代理判断，不能确认的计费方式保持 `unknown`。

推荐交叉验证而不是信任单榜：[Artificial Analysis Data API](https://artificialanalysis.ai/data-api/docs) 可提供 Coding、Agentic、价格、吞吐与延迟数据，[LiveBench](https://livebench.ai/) 提供 Coding 与 Agentic Coding，[Arena](https://arena.ai/leaderboard/text) 反映人类偏好，Provider 官方资料用于核对版本、上下文、工具与访问方式。[OpenRouter Rankings](https://openrouter.ai/rankings?category=programming) 主要反映实际使用量，只作为采用度信号，不能单独证明模型质量。门禁要求审查而宿主没有联网工具时，主代理必须说明限制并停止组建，不能静默使用未经审查的默认值；Expert Council 不会自动安装插件、Skill 或第三方可执行包。

### 角色权重

每个语义角色都有归一化默认权重。Implementation Worker 更重视工具可靠性、编码、自治执行与 Shell 可靠性；Architecture Oracle 更重视架构、规划、长上下文与审查。

```json
{
  "routing": {
    "roleWeights": {
      "implementation-worker": {
        "toolReliability": 0.4,
        "coding": 0.3,
        "costEfficiency": 0.1
      }
    }
  }
}
```

权重会自动重新归一化。`costPolicy: economy` 会提高成本因素的影响，`speed` 会提高速度并降低成本因素的影响，旧的 `quality` 会降低成本因素；它们都不会绕过安全或兼容性硬约束。

## 语义角色与团队规模

角色由任务语义定义，不与任何模型名称绑定：

| 角色 | 默认权限 | 目的 |
|---|---|---|
| Planner | 只读 | 任务拆解、依赖与风险 |
| Scout | 只读 | 仓库探索与上下文压缩 |
| Architecture Oracle | 只读 | 困难跨文件推理与第二意见 |
| Implementation Worker | 可写 | 有边界的代码修改和聚焦测试 |
| Debugger | 可写 | 复现、定位、修复和验证 |
| Reviewer | 只读 | 回归、边界条件和设计审查 |
| Verifier | 只读 | 检查已报告的测试、Diff 与验收标准 |

微小任务只使用一个 Worker；普通任务使用 Worker 与 Verifier；复杂功能使用 Planner、Worker、Reviewer 与 Verifier；复杂调试使用 Scout、Debugger、Oracle 与 Verifier。`maxExperts` 会限制团队规模，主代理永远不会再被复制成一个多余的 `lead` 专家。

只读专家在主工作区运行，因此**从不报告 `filesChanged`**：它们没得变更工具，所以那里的 git 脏文件属于主代理；把它们归给专家等于伪造作者身份。对这类运行调用 `expert_cleanup` 会答 `not-required`，因为并不存在 worktree。确实需要改代码时，请改派 implementation-worker 或 debugger，并审阅它独立的 worktree。 可写执行若**读不出** diff 则是另一种情况，并且会被如实标出：`executionMetadata.filesChangedError` 外加一条 risk。因为 `git status` 失败得来的空列表会被读成“专家什么都没改”，照这个列表做集成的宿主就会把真实成果丢成没有成果（缺陷 #28）。

任务分类和全部评分运算都是确定性的。宿主可以在委派前检查已选模型、备选模型、分数和简明理由。组建 Council 时还会应用可配置的多样性惩罚；Reviewer 会在经济合理时优先选择与先前成员不同的 Provider 和推断模型家族，但角色适配度与硬约束仍然优先。

## 路由过程

```text
发现当前可调用候选模型
  -> 应用硬约束
  -> 合并能力与画像层
  -> 计算角色适配度和有效成本
  -> 保守应用本地结果调整
  -> 使用稳定规则排序
  -> 返回选择、备选项和理由
```

硬约束会排除不可用或已禁用模型、不兼容角色、工具可靠性不足、上下文不足、运行时不支持写入、日常任务中的 `escalation-only` 资源，以及带有活跃运行时可用性标记的模型。

Pi 会在会话内缓存模型清单，提供商目录也可能保留失效的模型名称，否则 Council 可能围绕一个上游已无法服务的模型组建。当一次委派尝试以 `provider_error` 失败且带失效模型证据（例如 `model_not_found`、未知或已停产的模型、以及运行时自身的预检可用性检查）时，服务会通过一次原子 read-modify-write 把 `modelAvailability` 标记写入持久化的共享模型评估（`EXPERT_COUNCIL_DATA_DIR`，Windows 上即 `%LOCALAPPDATA%/ExpertCouncil/model-assessment.json`），且不会回退其他正在运行的 Pi/Codex 实例写入的更新快照。受影响的 `expert_result` 会在 `executionMetadata.unavailableModels` 和 `risks` 中点名该模型，`expert_inspect` 会警告活跃标记，后续 `expert_build`、委派和升级会以硬约束拒绝被标记的模型。标记是保守的本地证据：24 小时后自动过期，在提交全新审计时保留，并且显式的 `modelOverrides["provider/model"].overrideUnavailableMarker: true` 可以重新启用某个模型。标记并不只属于"模型已死"：瞬态 TPM/RPM 限流会记录 `rate-limited` 标记（`MODEL_RATE_LIMIT_MARKER_TTL_MS`，2 分钟），并连带同提供商的兄弟模型——限流是账户级条件；提供商传输不可达记录 `transport-unstable`（5 分钟），且只标记失败的那一条路由；套餐或余额耗尽记录 `quota-exhausted`（6 小时）；而套餐级访问被拒（例如 403 `AccessDenied.Unpurchased`）属于失效模型证据，记录 `unavailable`（24 小时）。这些瞬态种类一律不计入模型自身的可靠性记录——那是模型的数据，上述是供应商的数据。若尚无已保存的评估，标记无法持久化，但失败仍会报告给主代理并记入本地遥测。

推理等级是可选且与模型相关的。只有当 Pi 明确暴露选定模型支持某个等级时，角色偏好才会生效；否则 Pi 会保留或钳制到模型支持的默认值。

## Skill 与最小权限

平台无关的主代理指导唯一源文件是 [`shared/skills/expert-council/SKILL.md`](https://github.com/Labiey/expert-council-router/blob/main/shared/skills/expert-council/SKILL.md)。构建过程会把它与 `shared/skills/expert-council/hosts/` 下的小型宿主适配层合成，生成不同的 Pi 与 Codex `SKILL.md`，而不复制公共工作流。Pi 产物只说明完成后的 `steer`/`followUp` 行为，完全不暴露 `expert_wait`；Codex 产物才说明有限时的 `expert_wait` 流程。共享角色提示位于 `packages/core/src/roles/prompts/`，作为包资源复制，而不是为不同宿主重复编写。

只读角色永远不会获得 `edit`、`write`、`bash` 或 `powershell`，即使调用者试图把它们加入工具列表。Pi 会话使用真实的 `tools` allowlist，因此它比仅靠 Prompt 约束更强。在提供专用的无副作用命令运行器之前，需要 Shell 执行测试的任务应交给隔离 worktree 中的写入型角色。

系统只会激活已经安装并启用、且当前角色需要的 Pi Skill。用户级 Skill 默认可信；项目级和临时 Skill 默认排除，只有名称精确列入 `security.trustedSkills` 才能启用。每个专家资源加载器都会禁用扩展、Prompt 模板、主题和项目上下文文件；无法强制这些策略的 Pi SDK 版本会被拒绝。Expert Council 不会下载或安装任何 Skill 或可执行扩展。

专家提示要求：修改前先阅读、验证路径、优先局部编辑、失败后诊断再换方法、使用有限时非交互命令、检查执行结果、禁止递归委派，并返回紧凑 JSON，而不是私人思维过程。

## 重试与升级

失败类型会被归一化为：

```text
tool_call_error reasoning_failure test_failure timeout provider_error
missing_context permission_error unknown
```

默认情况下，第一次可纠正的工具、上下文或测试失败最多获得一次改变方法后的重试。重复的相关失败或 Provider 错误会切换到下一个符合条件且尚未尝试的模型。尝试次数与升级次数分别设有上限；没有候选或预算耗尽时，未解决状态会返回主代理。

系统不存在无限循环，也不会按策略盲目重复同一种失败操作。

## 结构化结果与上下文效率

专家结果包含状态、角色、模型、摘要、修改文件、测试、发现、风险、下一步建议、失败类型、Pi 可提供的近似用量，以及有限的执行元数据。系统优先使用专家返回的结构化失败类型，并确定性识别测试、Provider、工具和上下文失败；无法解析的非 JSON 输出会标记为 `reasoning_failure`。系统不会请求或保存私有思维过程，也不会把整份源码复制回主代理上下文。

## 工作区安全

系统不会因为 Codex 自身处于沙箱就假设外部 Pi 进程同样安全。Pi Runtime 使用独立边界：

1. 规范化请求工作区路径。
2. 要求路径位于允许的根目录内。
3. 从仓库当前 `HEAD` 在系统临时目录内当前用户专属的私有目录中创建 detached worktree。
4. 在该 worktree 中为 Worker 提供写入工具。
5. 返回 worktree 路径和修改文件列表。
6. 由 Codex 或 Pi 主代理检查、整合并最终验收。
7. 整合或拒绝结果后调用 `expert_cleanup`。一次调用会删除该 execution ID 因重试或升级创建的全部 worktree，并返回所有已删除路径。无人认领的 worktree 会在 `security.worktreeRetentionMs` 后自动清理，默认保留 24 小时，并同步 prune Git 元数据。

非 Git 工作区默认拒绝写入。若确实需要原地修改，必须显式配置：

```json
{
  "security": {
    "workspaceStrategy": "bounded-in-place",
    "allowInPlaceMutations": true,
    "allowedWorkspaceRoots": ["/absolute/path/to/project"]
  }
}
```

启用前请阅读 [`SECURITY.md`](https://github.com/Labiey/expert-council-router/blob/main/SECURITY.md)。

## 遥测与本地学习

默认用户数据根目录为：Windows `%LOCALAPPDATA%\ExpertCouncil`，Linux `$XDG_STATE_HOME/expert-council` 或 `~/.local/state/expert-council`，macOS `~/Library/Application Support/ExpertCouncil`。共享的 `telemetry.jsonl` 保存不透明执行结果，使实际可靠性能够跨对话和工作区复用；同一 execution 的反馈会覆盖早期样本，不会重复计数。共享的 `model-assessment.json` 保存最新的显式能力与计费评分，以及运行时学习到的模型可用性标记。可用 `EXPERT_COUNCIL_DATA_DIR`、`EXPERT_COUNCIL_TELEMETRY`、`EXPERT_COUNCIL_MODEL_ASSESSMENT` 和 `EXPERT_COUNCIL_STATE` 覆盖位置。

它不会记录 Prompt、源码内容、凭据、API Key、Secret 或思维过程。聚合指标包括按角色成功率、首轮成功率、工具错误率、重试率、验证通过率和平均尝试次数。Expert Council 没有远程分析端点。

计划、执行状态和已完成结构化结果仍按工作区隔离，保存在 `workspaces/<工作区哈希>/state.json`。进程重启后仍可查询；重启时仍在运行的任务会被关闭为明确的中断失败。旧版项目内 `.expert-council` 和 `%USERPROFILE%\.expert-council` 目录不会被自动删除。

## CLI

CLI 与 MCP、Pi Package 使用完全相同的 Core 和 Pi Runtime：

```text
expert-council models
expert-council inspect
expert-council compositions
expert-council build <task>
expert-council delegate <role> <task>
expert-council feedback <execution-id> --verification passed|failed
expert-council status
expert-council abort <execution-id>
expert-council reset <scope>
expert-council verify (--exec <id> | --workspace <path>) --command JSON_ARRAY
expert-council respond <execution-id> --kind decision|tool_approval
expert-council cleanup <execution-id>
expert-council watch --exec <execution-id> [--follow]
expert-council --version
```

常用参数：

- `--json`：机器可读输出。
- `--cwd`：项目工作区。
- `--config`：用户策略文件。
- `--telemetry`：自定义本地遥测路径。
- `--state`：自定义持久化计划、执行和结果状态路径。
- `--timeout-ms`：专家执行超时。
- `watch` 额外接受 `--dir`（事件流目录）、`--interval-ms`（轮询间隔，默认 1000）与 `--timeout-ms`（最长跟随时间，默认 300000）、`--quiet-ms`（没有终止标记的流需静默多久才放弃跟随，默认 15000）；它需要 `security.observability.expertWindow: "interactive"` 正在产生事件流。
- `watch` 还接受 `--max-lines N`（每条正文块显示几行，1-50，默认 10）与 `--max-chars N`（每行截断宽度，20-400，默认 120）；自动打开的窗口会从配置继承 `contentWindowLines` 与 `contentWindowChars`，所以你把数字调大不会被跟随器自己的默认值悄悄覆盖。

## MCP Server

MCP 表面刻意保持为 13 个语义工具：

- `expert_inspect`
- `expert_build`
- `expert_delegate`
- `expert_wait`
- `expert_result`
- `expert_abort`
- `expert_feedback`
- `expert_cleanup`
- `expert_escalate`
- `expert_status`
- `expert_availability_reset`
- `expert_verify`
- `expert_respond`

`expert_respond` 用于回答运行中专家的未决 `pendingInteraction`（`request_decision` 的抉择或 `request_tool` 的授权），使其会话继续；无法接收推送的无头宿主通过 `expert_status`（`view: "running"`）或带 `includeProgress` 的 `expert_result` 轮询发现未决交互。

`expert_inspect` 和 `expert_build` 默认返回面向宿主的紧凑视图。只有确实需要准确模型元数据、备选项、评分、工具或 Skill 时才传入 `detail: "full"`。

### 委员会编成（Council compositions）

已保存的委员会名单保存在共享状态目录中与 `route-policy.json` 同级的 `council-compositions.json`——不新增任何工具。默认位置：Windows `%LOCALAPPDATA%\ExpertCouncil\council-compositions.json`，macOS `~/Library/Application Support/ExpertCouncil/council-compositions.json`，Linux `$XDG_STATE_HOME/expert-council/council-compositions.json`（或 `~/.local/state/expert-council/council-compositions.json`）；可用 `EXPERT_COUNCIL_COMPOSITIONS` 覆盖。

```json
{
  "compositions": [
    {
      "name": "daily-cheap",
      "roles": {
        "scout": ["qwen-token-plan-cn/deepseek-v4-flash"],
        "implementation-worker": ["zai/glm-5.3-flash", "deepseek/deepseek-v4-flash"]
      }
    }
  ],
  "sessions": { "01a066b2-a166-7e67-983d-2bbec848c223": "daily-cheap" }
}
```

- `compositions` 是有序列表（即菜单优先级），最多 32 个唯一名称（≤80 字符）。`roles` 以七个语义角色为键；每个值是 `provider/id` 模型键列表（每角色 ≤16 个）。角色缺失或列表为空时该角色自动路由。
- `sessions` 把会话键（宿主对话 ID）映射到编成名称。条目 30 天后过期；工具写入的绑定带 `updatedAt` 时间戳，手写的字符串条目则保留。
- 首次构建既未传 `composition` 也未传 `costPolicy` 时，`expert_build` 返回 `compositionMenu`：最多三个已保存名单加一个 `auto` 选项。把选中的名称作为 `composition` 传回；`auto` 选项即成本策略流程（economy/balanced/speed）。
- 显式 `composition` 构建成功后会把该名称绑定到会话；成功传入 `costPolicy` 会解除绑定。菜单响应不绑定任何内容。
- 某角色的候选池是编成列表与路由策略过滤、Provider 限额/并发排除的交集。路由策略的 `deny` 恒胜于候选池，池被完全排除的角色会报告为无法编成。
- `expert_delegate` 的单项参数与 `assignments[]` 均接受可选 `model`（`provider/id`）。对同一角色的多个任务分别固定不同的池内模型即可并发派遣多个专家；固定模型不在该角色池、已发现清单或路由策略内时会返回结构化错误。

### 路由策略文件

模型黑白名单保存在共享状态目录中与 `model-assessment.json` 同级的 `route-policy.json`——不新增任何工具。文件包含所有会话共同遵守的 `system` 条目，以及按宿主会话键组织的 `sessions` 条目（Pi 会话 ID 在 resume 后保持不变；MCP stdio 会话使用稳定的 `"default"` 键）。会话只能收紧系统策略：deny 取并集、allow 取交集、deny 恒胜。条目为 `provider/id` 或裸 `provider`（整个供应商）。`expert_inspect` 会返回本会话的 `sessionKey`、当前 `effective` 策略与文件 `sourcePath`，宿主（或你）可以直接编辑该文件；改动在下次专家调用即生效，超过 30 天的会话条目自动清理，损坏文件会带警告忽略。

<!-- route-policy:complete -->
```json
{
  "version": 1,
  "system": {
    "allow": [],
    "deny": ["acme-host", "acme-metered/acme-pro"],
    "updatedAt": "2026-09-18T12:00:00+08:00",
    "note": "free text, never sent to an expert"
  },
  "sessions": {
    "4f0c…": {
      "allow": ["acme-plan/acme-flash"],
      "deny": ["acme-plan/acme-heavy"],
      "updatedAt": "2026-09-18T12:00:00+08:00",
      "note": "narrows the system policy for this conversation only"
    },
    "default": { "deny": ["acme-metered"] }
  },
  "providers": {
    "acme-plan": { "maxConcurrency": 1, "dailyTokenCap": 12000000, "weeklyTokenCap": 60000000 },
    "acme-metered": { "maxConcurrency": 2 }
  }
}
```

- `version` 必须是字面量 `1`；其他值会让整份文档被拒绝。
- `system` 对所有会话生效。`sessions` 以宿主会话 id 为键——Pi 的 session id 在 resume 后仍然有效，MCP stdio 会话共用稳定的 `"default"` 键。
- 会话只能**收窄**系统策略：deny 取并集，allow 取交集，deny 永远优先。`allow` 是排他清单——只要写了它，未列出的模型就不可路由。
- 条目形如 `provider/id`，或只写 `provider` 表示整个 provider。每个清单最多 32 条、每条最多 200 字符；控制字符会被剥掉，不符合该形状的内容会被丢弃而不是被信任。
- `providers` 放的是花费与并发上限，**不是**黑白名单：`maxConcurrency`（每个 provider 0-8，`0` 表示停放）、`dailyTokenCap`、`weeklyTokenCap`，都是正整数。上限依据用量账本执行；撞顶的 provider 在本窗口内被跳过，而不是让整次委派失败。
- `note`、`updatedAt`、`workspace` 只是留存的元数据。特别是 `workspace`：它会被保存并原样往返，但目前没有任何路由代码读它——不要依赖它来限定策略范围。
- 删除这个文件不是无害操作：没有 deny 清单后，此前被排除的 provider 会重新可路由，这可能把工作从包月计划悄悄挪到按量计费的 API 上。
- `expert_inspect` 会报告本会话的 `sessionKey`、生效后的 `effective` 策略以及文件的 `sourcePath`，所以宿主（或你）可以直接编辑它；改动在下一次调用生效，过期的会话条目 30 天后清理，文件损坏时会被忽略并给出警告。

### 一次委派多个专家

`expert_delegate` 会启动后台任务并立即返回 execution ID，原有单任务参数保持兼容。`timeoutMs` 是每项任务的必填参数（1000–3600000 ms）——缺省即报错；请按任务难度设置。超时的尝试会自动把重试预算放大 1.5×，且当专家发现以现有工具无法完成任务时会以结构化 `missing_context`/`permission_error` 提前终止。存在两个以上相互独立的任务时，应在继续其他主代理工作前一次发配整个批次：

```json
{
  "assignments": [
    { "role": "scout", "task": "定位相关文件", "taskDescription": "仓库映射", "timeoutMs": 300000 },
    { "role": "reviewer", "task": "审查边界设计", "taskDescription": "边界审查", "timeoutMs": 600000 }
  ]
}
```

`assignments` 必须是实际 JSON 数组，不能是包含 JSON 文本的字符串。原生 Pi 适配器对部分模型偶发的字符串化数组提供有界兼容解析，但正常调用仍应直接生成数组。

可选的 `taskDescription` 是供宿主识别任务的简短标签，不属于专家实际任务内容。派发后主代理应继续所有可独立完成的工作；无其他有用工作时，调用一次 `expert_wait`，传入最多 8 个 execution ID、通常使用 `mode: "all"`（任一早期结果即可推进时使用 `"any"`），并按预计剩余难度设置 `timeoutMs`。等待由执行 Promise 的完成事件驱动而不是轮询；阻断当前 MCP 调用属于预期行为，等待期间不会继续消耗主模型 Token。

### 等待后台任务

```json
{
  "executionIds": ["exec_a", "exec_b"],
  "mode": "all",
  "timeoutMs": 900000
}
```

`expert_wait` 只返回完成状态和任务 ID，随后使用 `expert_result` 获取正式反馈，并在主代理验收后调用 `expert_feedback`。`expert_wait.timeoutMs` 只限制本次等待，不会延长各专家自己的执行期限。所有可能阻断的 Expert Council、Bash、PowerShell 或其他 MCP 调用仍必须按操作难度附带显式的有限超时；其余同步 Expert Council 操作受独立的 30 秒 Server 内部上限保护，只有一处刻意的例外：`expert_verify` 的上限取该默认值与 60 秒中的较大者，因为把专家的声称变成观察到的退出码，通常意味着要跑一次构建或测试。`expert_status` 会返回有界的逐次尝试历史。原生 Pi Package 使用主动完成通知，因此不暴露 `expert_wait`。

### 直接运行 stdio 服务

直接启动 stdio Server：

```bash
node packages/mcp-server/dist/bin.js
```

### 环境变量

支持以下环境变量：

- `EXPERT_COUNCIL_WORKSPACE`：默认允许工作区。
- `EXPERT_COUNCIL_CONFIG`：用户 JSON 配置。
- `EXPERT_COUNCIL_TELEMETRY`：本地遥测 JSONL 路径。
- `EXPERT_COUNCIL_STATE`：持久化计划、执行和结果状态路径。
- `EXPERT_COUNCIL_WORKTREES`：专家变更 worktree 的父目录（仍保留按用户隔离的私有子目录）；适用于短路径盘或更快的磁盘。
- `EXPERT_COUNCIL_COMPOSITIONS`：已保存委员会编成的路径。
- `EXPERT_COUNCIL_DATA_DIR`：全部每用户数据的根目录——评估、路由策略、账本、遥测、观察流。
- `EXPERT_COUNCIL_CONTENT`：仅对本进程生效的内容挡位，覆盖 `contentStream` 与 `contentByRole`。
- `EXPERT_COUNCIL_MODEL_ASSESSMENT`：持久化共享模型评估的路径。
- `EXPERT_COUNCIL_ROUTE_POLICY`：持久化每会话黑白名单策略的路径。
- `EXPERT_COUNCIL_USAGE_LEDGER`：provider 上限背后持久化的用量账本。
- `EXPERT_COUNCIL_MCP_TIMEOUT_MS`：同步 MCP 操作的有限超时，默认 30000 毫秒。
- `PI_CODING_AGENT_MODULE`：自动解析失败时显式指定 Pi 包目录。

环境变量覆盖和 CLI 路径参数属于“受信任的操作者输入”。其中 `PI_CODING_AGENT_MODULE` 会加载可执行代码，配置、工作区、遥测和状态路径会选择本地文件；不要从不受信任仓库、任务文本或模型输出中接受这些值。

## 原生 Pi Package

日常使用推荐通过 npm 安装（见[快速开始](#安装-pi-package)）；本节面向源码开发与本地候选版验证。

在仓库根目录构建并安装本地候选版。即使在 Windows 上，只要命令可能经过 Pi 的 Bash 兼容 Shell，也应使用正斜杠；未正确引用的 `.\packages\pi-package` 会在到达 Pi 前丢失反斜杠。

```bash
npm run build
pi install "./packages/pi-package"
pi list
pi --verbose
```

`pi list` 应显示配置中的 source 及解析后的绝对 Package 目录。新启动的 verbose Pi 会话应显示 `dist/extension.js`、`expert-council` Skill，以及不含 `expert_wait` 的 12 个语义工具。已经运行的 Pi 进程不会热加载重新构建或已移除的 Package。

或仅在当前运行中临时加载：

```bash
pi --verbose -e "./packages/pi-package"
```

无费用装载检查只需让 Pi 调用 `expert_inspect`。真实编排检查应在新对话中组建一个只读委员会，一次性批量派发两个相互独立的只读任务，确认 `expert_delegate` 立即返回 execution ID，再用 `expert_result` 和 `expert_feedback` 验收每个完成结果。若测试写入型专家，还应确认一次 `expert_cleanup` 会在 `workspaces` 中报告该 execution 的全部重试 worktree，且随后 `git worktree list` 只剩主工作区。

移除持久安装前，先退出所有已经加载该 Package 的 Pi 进程，然后仍在仓库根目录执行：

```bash
pi remove "./packages/pi-package"
pi list
```

如果当前目录已经变化，请改用解析后的绝对路径。PowerShell 示例：

```powershell
$ecPiPackage = (Resolve-Path "./packages/pi-package").Path
pi remove "$ecPiPackage"
pi list
```

如果通过 Pi 的 Bash 兼容 Shell 执行移除，请使用 `pi list` 第二行显示的正斜杠绝对路径，例如 `pi remove "C:/path/to/ExpertCouncil/packages/pi-package"`。不要直接复制 `pi list` 中缩进显示的相对 source，除非命令也从相同的 settings 目录上下文解析。

Pi 会通过当前包清单中的 `pi.extensions` 与 `pi.skills` 加载 `dist/extension.js` 和同步后的 `expert-council` Skill。扩展注册 12 个语义工具；由于原生 Pi 已提供完成 `steer`/`followUp`，因此省略 MCP 专用的 `expert_wait`。它不包含另一套路由实现。

Pi 委派是非阻断式的；一个调用最多可在返回前启动 8 个相互独立的后台任务。专家完成后，扩展发送精简 JSON：必含已完成的 `executionId`，仅在调用时提供过 `taskDescription` 才包含该描述，绝不直接携带 feedback。主 Agent 工作中时通知使用 `steer`；主 Agent 空闲时使用带 `triggerTurn` 的 `followUp` 立即唤醒。随后由主 Agent 调用 `expert_result` 获取结构化反馈。主 Agent 应先发完当前已准备好的整个批次再结束回合，之后不要轮询或静默等待消耗 token。

## Codex 插件

Codex 插件让 Codex 成为主代理：它内置共享的 `expert-council` Skill 与 stdio MCP Server，专家通过 Pi 执行。插件不包含任何 hook——服务器优先使用 MCP roots，其次从 Codex 宿主自有的 `codex/sandbox-state-meta` 能力解析当前任务工作区，最后回退到 `EXPERT_COUNCIL_WORKSPACE` 覆盖项，并且拒绝把插件安装目录当作工作区。

构建产物根目录：

```text
packages/codex-integration/plugin/expert-council/
  .codex-plugin/plugin.json
  .mcp.json
  skills/expert-council/SKILL.md
  dist/server.mjs
  dist/roles/*.md
  THIRD_PARTY_NOTICES.md
```

### 安装

当前版本已包含预构建 MCP Server 及经过验证的 Pi SDK 运行时，Codex 可以直接把本仓库作为固定版本的 Git Marketplace 安装。运行时需要 Node.js 22.19 或更高版本，以及已经配置好的 Pi 账户/模型目录；无需克隆仓库、执行 `npm install`，也不再依赖从全局 npm 目录解析 `@earendil-works/pi-coding-agent`。

```bash
codex plugin marketplace add Labiey/expert-council-router --ref v0.8.6 --json
codex plugin marketplace list --json
codex plugin list --marketplace expert-council-router --available --json
codex plugin add expert-council@expert-council-router --json
codex plugin list --json
```

每个发布版本只需执行一次 `marketplace add`。如果已经用旧版本或本地路径注册了同名 Marketplace，请先移除旧来源，或按下方升级流程操作。`plugin list --json` 应显示 `expert-council` 已从 `expert-council-router` 安装。

Windows 版 Codex Desktop 内置 CLI，但它可能不在 `PATH` 中。可以在 PowerShell 定位正在运行的 Desktop CLI，再执行同样的远程安装命令：

```powershell
$ecCodex = (Get-Command codex.exe -ErrorAction SilentlyContinue).Source
if (-not $ecCodex) {
    $ecCodex = Get-Process codex -ErrorAction SilentlyContinue |
        Where-Object Path |
        Select-Object -First 1 -ExpandProperty Path
}
if (-not $ecCodex) {
    $ecCodex = Get-ChildItem (Join-Path $env:LOCALAPPDATA "OpenAI/Codex/bin") `
        -Filter codex.exe -File -Recurse -ErrorAction SilentlyContinue |
        Sort-Object LastWriteTime -Descending |
        Select-Object -First 1 -ExpandProperty FullName
}
if (-not $ecCodex) { throw "未找到 Codex Desktop CLI。" }

& $ecCodex plugin marketplace add Labiey/expert-council-router --ref v0.8.6 --json
& $ecCodex plugin marketplace list --json
& $ecCodex plugin list --marketplace expert-council-router --available --json
& $ecCodex plugin add "expert-council@expert-council-router" --json
& $ecCodex plugin list --json
```

完全退出 Codex Desktop，等待其后端进程结束，再重新打开并新建任务。部分 Desktop 版本仅新建任务并不能可靠触发 MCP 重载。

若要开发插件，可克隆仓库、执行 `npm ci && npm run build`，再把仓库根目录的绝对路径传给 `codex plugin marketplace add`。普通使用建议安装固定版本的远程 Release。

加载成功时会同时出现 `expert-council` Skill 和全部 13 个 `expert_*` MCP 工具；`expert_inspect` 必须返回真实资源清单，而不是 “No compatible Pi SDK is installed” 诊断。如果只有 Skill 而没有工具，或检查仍出现该诊断，请先确认 Marketplace 固定到 `v0.8.6` 或更高版本，再重启或重装插件；不要手动启动 `dist/server.mjs` 或手写 JSON-RPC。

在新的 Codex 任务中输入以下提示以验证安装：

```text
使用 Expert Council 检查当前可用的 Pi 模型、Provider、计费分类和模型评估状态。只返回紧凑摘要，不组建委员会，也不派遣专家。
```

任务应调用 `expert_inspect`，不应要求信任 hook、手工启动 MCP Server，也不应把插件缓存目录当作项目工作区。

### 升级

使用 `--ref` 固定的 Marketplace 会有意停留在该发布版本。升级时请移除已安装插件与旧 Marketplace，然后添加新标签并重新安装：

```bash
codex plugin remove expert-council@expert-council-router --json
codex plugin marketplace remove expert-council-router --json
codex plugin marketplace add Labiey/expert-council-router --ref vX.Y.Z --json
codex plugin add expert-council@expert-council-router --json
```

请把 `vX.Y.Z` 替换为目标版本。如果明确希望跟随默认分支，可以在首次添加时省略 `--ref`，以后执行 `codex plugin marketplace upgrade expert-council-router --json`；普通使用仍建议固定标签。重装后请完全重启 Codex Desktop，并在新任务中测试。不要同时安装多个都声明 `expert_council` MCP Server 的副本。

### 行为要点

- 内置 `.mcp.json` 将宿主工具调用上限提升到 3660 秒，让单次有边界的 `expert_wait` 可以阻塞到完成；Skill 仍要求为每个操作设定明确时限，而不是把该上限当作默认预算。
- 对话中第一次组建委员会时，主代理会与你确定唯一的成本策略（economy、balanced 或 speed）；在此之前 `expert_build` 与 `expert_delegate` 的响应都会携带询问提醒。
- 可写专家在受信任工作区下的独立 Git worktree 内修改；变更返回给主代理审查，永不自动合并。

### 卸载

移除插件及其 Marketplace 注册：

```bash
codex plugin remove expert-council@expert-council-router --json
codex plugin marketplace remove expert-council-router --json
codex plugin list --json
codex plugin marketplace list --json
```

如果 PowerShell 找不到 `codex`，先定位 Codex Desktop 自带的 CLI（Codex Desktop 运行时可从进程取路径，否则回退到安装目录）：

```powershell
$ecCodex = Get-Process codex -ErrorAction SilentlyContinue |
    Where-Object Path |
    Select-Object -First 1 -ExpandProperty Path

if (-not $ecCodex) {
    $ecCodex = Get-ChildItem (Join-Path $env:LOCALAPPDATA "OpenAI\Codex") `
        -Filter codex.exe -File -Recurse -ErrorAction SilentlyContinue |
        Sort-Object LastWriteTime -Descending |
        Select-Object -First 1 -ExpandProperty FullName
}

if (-not $ecCodex) {
    throw "未找到 Codex Desktop 自带的 codex.exe"
}
```

然后通过定位到的 CLI 卸载：

```powershell
& $ecCodex plugin remove "expert-council@expert-council-router" --json
& $ecCodex plugin marketplace remove "expert-council-router" --json

& $ecCodex plugin list --json
& $ecCodex plugin marketplace list --json
```

如果重新打开后仍显示，可在 Codex 关闭状态下安全清理旧 Expert Council 缓存（仅删除 `%USERPROFILE%\.codex\plugins\cache\expert-council-*`）：

```powershell
$ecCacheRoot = [IO.Path]::GetFullPath(
    (Join-Path $env:USERPROFILE ".codex\plugins\cache")
)
$ecCachePrefix = $ecCacheRoot.TrimEnd("\") + "\"

$ecTargets = Get-ChildItem -LiteralPath $ecCacheRoot `
    -Directory -ErrorAction SilentlyContinue |
    Where-Object Name -Like "expert-council-*"

foreach ($ecTarget in $ecTargets) {
    $ecResolved = [IO.Path]::GetFullPath($ecTarget.FullName)

    if (
        $ecResolved.StartsWith(
            $ecCachePrefix,
            [StringComparison]::OrdinalIgnoreCase
        ) -and
        (Split-Path $ecResolved -Leaf) -like "expert-council-*"
    ) {
        Write-Host "删除缓存: $ecResolved"
        Remove-Item -LiteralPath $ecResolved -Recurse -Force
    }
}
```

之后请完全退出 Codex Desktop，再开始新任务。

## 测试

```bash
npm test
npm run typecheck
npm run build
npm run pack:check
npm run validate
```

`npm run validate` 会先构建，确保全新克隆在测试前已经生成 workspace 包入口。测试覆盖模型归一化、公开价格与真实策略计费、Worker 可靠性、Oracle 评分、Reviewer 多样性、硬约束、未知和缺失模型、团队规模、重试和逐次诊断、结构化失败分类、升级、重试上限、角色权限、紧凑宿主输出、配置校验、遥测隐私/反馈/用量聚合、Core 宿主独立性、模拟 Pi 发现与执行、CLI JSON、MCP Schema、真实 Pi 0.85.1 扩展加载/包装及异步批量通知、Pi 扩展注册和真实 Git worktree 隔离。

普通测试只使用 Mock Runtime，绝不会调用付费模型。真实只读 Pi 执行必须同时指定模型并明确确认成本：

```powershell
$env:EXPERT_COUNCIL_LIVE_MODEL = "provider/model"
$env:EXPERT_COUNCIL_LIVE_CONFIRM = "YES"
npm run smoke:live:pi
```

普通验证流程永远不会执行该脚本。

## 发布

先运行 `npm run validate`，检查每个 `npm pack --dry-run` 文件列表，再按依赖顺序发布：

```text
@expert-council/core
@expert-council/pi-runtime
@expert-council/cli
@expert-council/mcp-server
@expert-council/pi-package
```

## 已知限制

- 护栏计时依赖宿主进程确实被调度：进程被挂起或长期抢不到 CPU 时，`budget_fraction` 警告可能晚于它本应预警的超时点。
- nudge 需要运行时会话支持 steer。Pi 会话支持；对不支持的运行时，Council 只通知宿主而不去干扰专家。
- 工具失败计数只记录运行时能观测到的部分，它不是审计面；工具「卡住」而非报错时，在该次尝试超时之前不会计入失败。

- Pi API 变化较快。当前版本已在本机 0.85.1 SDK（package.json 所钉版本）上验证；运行时会检查 SDK、模型运行时、资源加载器和 Session 必需方法，并在不兼容时明确列出缺失合约。
- Pi 没有统一的真实计费类型 API。运行时订阅信号和具名 Token Plan 优先；否则非零目录价格按按量计费处理，没有可靠证据的 Provider 保持 `unknown`，直到评估或显式用户配置确认。
- Expert Council 不会根据模型名称推断主观编码质量，也不会自动下载基准预设。
- Detached worktree 从已提交的 `HEAD` 开始，不会复制主工作区未提交改动。这是刻意的隔离设计；运行时会检测脏源工作区，并在委派前通过运行时限制和 mutation 委员会警告提示该偏差。
- Worktree 修改只返回给主代理审查，不会自动合并或应用；验收或拒绝后应调用 `expert_cleanup`，否则将在保留期结束后自动清理。
- 非 Git 工作区的写入需要显式原地修改授权。
- 正在进行的模型调用不会在 Server 重启后续跑；持久化状态会把它关闭为明确的中断失败，同时保留计划和已完成结果。
- 交互式专家窗口是尽力而为的可观测性，不是审计日志：事件文件有上限、异步写入、I/O 故障时可能丢事件，且 7 天后会被清理。它们只能从“运行专家那个宿主进程”所在的数据目录里跟随，换一个检出目录或不同的 `EXPERT_COUNCIL_DATA_DIR` 什么都看不到。
- Codex 自身的沙箱不会自动包含外部 Pi Runtime，因此 Expert Council 使用单独的允许根目录和 worktree 边界。
- Expert Council 不包含任意第三方包自动安装、递归专家树、图形界面、远程控制平面或远程遥测。

## 许可证

MIT，详见 [`LICENSE`](LICENSE)。
