From 9ccbfe3924d32858bbdb106c1974171762a4984f Mon Sep 17 00:00:00 2001 From: Zhang24y <177240371+Zhang24y@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:22:36 +0800 Subject: [PATCH 1/4] feat(expert-panel): add expert-panel consensus review plugin --- .../expert-panel/.zcode-plugin/plugin.json | 13 + external_plugins/expert-panel/LICENSE | 21 + external_plugins/expert-panel/README.md | 100 +++++ external_plugins/expert-panel/README_CN.md | 105 +++++ .../expert-panel/commands/expert-panel.md | 8 + .../expert-panel/skills/expert-panel/SKILL.md | 63 +++ .../workflows/expert-panel.dwf.ts | 406 ++++++++++++++++++ marketplace.json | 19 + 8 files changed, 735 insertions(+) create mode 100644 external_plugins/expert-panel/.zcode-plugin/plugin.json create mode 100644 external_plugins/expert-panel/LICENSE create mode 100644 external_plugins/expert-panel/README.md create mode 100644 external_plugins/expert-panel/README_CN.md create mode 100644 external_plugins/expert-panel/commands/expert-panel.md create mode 100644 external_plugins/expert-panel/skills/expert-panel/SKILL.md create mode 100644 external_plugins/expert-panel/workflows/expert-panel.dwf.ts diff --git a/external_plugins/expert-panel/.zcode-plugin/plugin.json b/external_plugins/expert-panel/.zcode-plugin/plugin.json new file mode 100644 index 0000000..ab674fd --- /dev/null +++ b/external_plugins/expert-panel/.zcode-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "expert-panel", + "version": "0.1.1", + "description": "专家团共识评审:多位不同视角专家独立作答→真匿名互评→仲裁归纳(共识率脚本计算、不足自动加辩)→主席综合→事实复核;quick/standard/deep 三档,零配置,结论按一致/分歧分离绝不硬合成。", + "description_i18n": { + "en": "Expert-panel consensus review: independent answers from multiple expert lenses → truly anonymous cross-critique → arbitration (consensus rate computed by script, extra debate round below threshold) → chair synthesis → fact verification; quick/standard/deep profiles, zero config, conclusions separate agreed items from explicit disagreements.", + "zh-CN": "专家团共识评审:多位不同视角专家独立作答→真匿名互评→仲裁归纳(共识率脚本计算、不足自动加辩)→主席综合→事实复核;quick/standard/deep 三档,零配置,结论按一致/分歧分离绝不硬合成。" + }, + "author": { "name": "Zhang24y" }, + "license": "MIT", + "skills": "./skills", + "commands": "./commands" +} diff --git a/external_plugins/expert-panel/LICENSE b/external_plugins/expert-panel/LICENSE new file mode 100644 index 0000000..e883e6f --- /dev/null +++ b/external_plugins/expert-panel/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Zhang24y + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/external_plugins/expert-panel/README.md b/external_plugins/expert-panel/README.md new file mode 100644 index 0000000..df78418 --- /dev/null +++ b/external_plugins/expert-panel/README.md @@ -0,0 +1,100 @@ +# expert-panel — Expert Panel Consensus Review (ZCode Plugin) + +Put an important decision in front of a panel of experts with distinct lenses: **independent answering → truly anonymous cross-critique → arbitration (consensus rate computed deterministically by script, extra debate round when below threshold) → chair synthesis → fact verification**. The output separates **stable agreed conclusions from explicitly stated disagreements**. Zero configuration — every role runs on your current session model. + +## Why + +The failure modes of a single model answer: conformity (anchoring on the first framing), authority bias (deferring to whoever sounds senior), and forced synthesis (mushing disagreement into "generally feasible"). This workflow counters each one: + +| Problem | Countermeasure | +|---|---| +| Conformity | Every expert **answers independently first**, before seeing anyone else's position | +| Authority bias | Critique inputs contain only anonymous labels (Expert A/B/C) + position + key points — **no lens name, no model** | +| False consensus | Consensus rate = agreed/(agreed+disputed), **computed deterministically by script — never self-reported by a model** | +| Splitting the difference | Below threshold → one extra debate round; still below → **disagreements are stated explicitly for human adjudication, never force-merged** | +| Fabricated facts | Objectively verifiable agreed items are checked by a fact verifier (it reads files / runs commands when it can); failures are dropped | + +A single expert failing is tolerated; the conclusion is annotated with sample completeness (absent lenses / valid expert count). + +## Installation + +**Local directory marketplace** (put this repo anywhere on your machine): + +1. ZCode → Plugin Marketplace → Add → Add Plugin Marketplace, paste the repo **directory** path +2. Personal → the marketplace → expert-panel → Install +3. Takes effect in a new session + +**CLI** (after the marketplace is added): + +```bash +zcode plugins install expert-panel@ +``` + +Requirements: ZCode desktop (the dynamic-workflows runtime is bundled). The run ledger uses `python3` if present (missing python3 does not block conclusions; the run is just not logged). + +## Usage + +Three ways to trigger it once installed: + +- Natural language: "use the expert panel to evaluate X", "have the expert panel settle this", "give this decision a multi-perspective check" +- Slash command: `/expert-panel ` +- Direct call: `CreateWorkflow(path=, args={...})` + +### Parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| question | string | required | The question/decision the panel should settle | +| profile | string | standard | quick (3 experts/1 round, minutes) \| standard (2 rounds) \| deep (auto-adds contrarian + synthesizer lenses, 2 rounds) | +| experts | json | [务实工程师, 风险与边界审查, 产品价值] | Expert lens names (no duplicates) | +| threshold | number | 0.6 | Consensus-rate threshold; below it one extra debate round runs | +| max_rounds | number | quick=1, else 2 | Max rounds including the first | +| evidence | json | empty | Evidence pack: texts or file paths (experts/verifier read files with their own tools) | +| basis | json | empty | Review rubric: file paths or entries; when given, verdicts must cite the rubric item by item | +| ledger_path | string | ~/.expert-panel/ledger.jsonl | Run ledger JSONL path | +| simulate_failures | json | empty | Test-only: indexes of experts to simulate as failed | + +### Model selection + +By default every role uses the session default model — this is deliberate: **zero config, runs on any ZCode environment**. To switch models uniformly, pass `subagent_model` on the `CreateWorkflow` call (look up ids with `ListModels` on the current provider). Per-role model routing is intentionally unsupported by the host: a subagent's model must be known before the run starts. + +### Interpreting results + +- `agreementRate ≥ threshold`: consensus reached. `agreed` holds verified stable conclusions; `unverifiable` holds value judgements (cannot be objectively verified — they are the panel's opinion only). +- `< threshold`: `disputed` lists each side's arguments for human adjudication — **this is a feature, not a failure**. +- `absent` non-empty or fewer than 2 valid experts: the conclusion is annotated with sample completeness; mind the caveat when quoting. + +Each run appends one ledger line (`ledger_path`): question digest / profile / expert counts / rounds / consensus rate / agreed·disputed·rejected counts / stable flag. + +### 30-second verification + +In a new session, send: + +``` +Use the expert panel, quick profile, to evaluate: Markdown files vs a notes app for a personal knowledge base? +``` + +Expected (within minutes): an "Expert Panel Conclusion" card with a consensus rate (e.g. 74%), agreed items (with verification marks), disputed items (if any), an executive summary and a final recommendation; while running, the expert board advances from "independent answering" to "converged". A rate below 60% triggers an extra debate round and explicit disagreements — that is also normal behavior. + +## Data behavior & uninstall + +- Each run appends one JSONL line to `~/.expert-panel/ledger.jsonl` (relocate via `ledger_path`); beyond that it **writes nothing** — no hooks, no MCP server, no outbound network requests (review subagents only use your current session's own model channel). +- Uninstall: Settings → Plugins → expert-panel → Uninstall. The only residue is `~/.expert-panel/` (the ledger dir); delete it to remove everything. + +## Design notes + +- **Anonymity protocol**: critique inputs carry only anonymous labels/positions/key points; lens names exist only inside each expert's own system prompt during independent answering. +- **Deterministic consensus rate**: the script computes `agreed/(agreed+disputed)`; round gating keys off that value alone — a model cannot shade the arithmetic. +- **Deep-profile guardrail**: a contrarian lens is mandatory (to counter "debate amplifying biased consensus"); a synthesizer is added when headcount is short. +- **Origins**: the mechanism draws on the anonymous cross-critique and chair ideas of karpathy/llm-council, plus the author's own expert-panel workflow iterated in production on ZCode (v2.1: rubric injection, single-expert fault tolerance, sample-completeness annotation). No code was copied from llm-council (which currently ships no license); this plugin is an independent implementation, MIT-licensed. + +## Changelog + +Version is whatever the marketplace shows (the source of truth is plugin.json). + +- 0.1.1: fix ledger write for bare-filename custom paths; pre-release cleanup (neutral git identity). +- 0.1.0: first releasable version, full six-stage mechanism. + +## License + +MIT — see [LICENSE](LICENSE) diff --git a/external_plugins/expert-panel/README_CN.md b/external_plugins/expert-panel/README_CN.md new file mode 100644 index 0000000..abc9acc --- /dev/null +++ b/external_plugins/expert-panel/README_CN.md @@ -0,0 +1,105 @@ +# expert-panel —— 专家团共识评审(ZCode 插件) + +把一个重要决策交给多位不同视角的专家:**独立作答 → 真匿名互评 → 仲裁归纳 → 共识率门控加辩 → 主席综合 → 事实复核**,产出「稳定一致项 + 明示分歧」的结构化结论。零配置,装完即用,所有角色跑在你当前的会话模型上。 + +## 为什么需要它 + +单个模型一次回答的问题:从众(先入为主)、权威偏置(谁名气大听谁的)、硬合成(把分歧和稀泥成"总体可行")。本工作流逐条对治: + +| 问题 | 对策 | +|---|---| +| 从众 | 每位专家**先独立作答**,之后才看到别人的立场 | +| 权威偏置 | 互评输入只有「专家A/B/C」匿名标签 + 立场 + 要点,**不含视角名与模型** | +| 虚假共识 | 共识率 = 一致项/(一致项+分歧项),**由脚本确定性计算,不信模型自报** | +| 和稀泥 | 共识率低于阈值自动加辩一轮;仍不足则**明示分歧交人工裁决,绝不硬合成** | +| 编造事实 | 可客观验证的一致项由事实复核员核验(能读文件/跑命令就去验),不实剔除 | + +单专家失败自动容错,结论标注样本完整度(缺席视角/有效专家数)。 + +## 安装 + +**本地目录市场**(先把本仓库放到本机任意目录): + +1. ZCode → 插件市场 → 添加 → 添加插件市场,粘贴本仓库所在**目录**路径 +2. 个人 → 该市场 → expert-panel → 安装 +3. 新会话生效 + +**CLI**(已添加市场后): + +```bash +zcode plugins install expert-panel@<市场名> +``` + +要求:ZCode 桌面版(自带 dynamic workflows 运行时)。台账落盘用到 `python3`(缺失不阻断结论,仅不入账)。 + +## 使用 + +装好后三种触发方式: + +- 自然语言:「用专家团评估一下 X」「让专家团给个结论」「这个问题帮我多视角把关」 +- 斜杠命令:`/expert-panel <你的问题>` +- 直接调用:`CreateWorkflow(path=<插件内 workflows/expert-panel.dwf.ts>, args={...})` + +### 参数 + +| 参数 | 类型 | 默认 | 说明 | +|---|---|---|---| +| question | string | 必填 | 要专家团给出稳定结论的问题/决策 | +| profile | string | standard | quick(3 专家/1 轮,数分钟)\| standard(2 轮)\| deep(自动补反方质询+综合连接原型,2 轮) | +| experts | json | [务实工程师, 风险与边界审查, 产品价值] | 专家视角名数组(勿重复) | +| threshold | number | 0.6 | 共识率阈值,低于自动加辩一轮 | +| max_rounds | number | quick=1,其余=2 | 最大轮数(含首轮) | +| evidence | json | 空 | 证据包:文本或文件路径数组(专家/复核员用工具自行读取) | +| basis | json | 空 | 评测依据:文件路径或条目数组,要求逐条对照判定并引用条目 | +| ledger_path | string | ~/.expert-panel/ledger.jsonl | 运行台账 JSONL 落盘路径 | +| simulate_failures | json | 空 | 测试用:模拟作答失败的专家下标 | + +### 模型选择 + +默认所有角色用会话默认模型——这是刻意设计:**零配置、在任何 ZCode 环境都能跑**。要统一换模型,在 `CreateWorkflow` 调用上传 `subagent_model`(用 `ListModels` 查当前 provider 可用的 id)。按角色分模型(如互评用轻量模型)宿主刻意不支持:子代理模型必须在运行前已知。 + +### 结果解读 + +- `agreementRate ≥ threshold`:达成共识。`agreed` 为经复核的稳定结论;`unverifiable` 是价值判断类(无法客观复核,仅代表专家团意见)。 +- `< threshold`:`disputed` 已列明各方论据,交人工裁决——**这是特性不是失败**。 +- `absent` 非空或有效专家 < 2:结论已标注样本完整度,引用时注意口径。 + +### 30 秒验证(装完即测) + +新会话里发一句: + +``` +用专家团 quick 档评估:个人知识库用 Markdown 文件还是笔记软件? +``` + +预期输出(数分钟内):一张「专家团结论」卡片,含共识率百分比(如 74%)、一致项清单(带复核标记)、分歧项清单(若有)、执行摘要与最终建议;运行中可见专家状态看板从「独立作答」推进到「已收敛」。共识率低于 60% 时会自动加辩一轮并明示分歧——同样算工作正常。 + +每次运行一行追加台账(`ledger_path`):问题摘要 / 档位 / 专家数 / 有效数 / 轮数 / 共识率 / 一致·分歧·剔除计数 / 是否达成。 + +## 输出形态 + +运行中:专家团状态看板(独立作答→交叉互评→修订表态→已收敛)+ 共识率收敛曲线 + 指标条。 +结束后:主产物为一份结论卡片(一致项/分歧项/复核剔除/执行摘要/最终建议/仲裁附录),并返回结构化 JSON(供编排方程序化消费)。 + +## 数据行为与卸载 + +- 每次运行追加一行 JSONL 到 `~/.expert-panel/ledger.jsonl`(可用 `ledger_path` 改路径);除此之外**不写任何文件**,无 hooks、无 MCP server、无出站网络请求(评审子代理只用你当前会话本身的模型通道)。 +- 卸载:Settings → Plugins → expert-panel → 卸载。残留物仅 `~/.expert-panel/`(台账目录),删除即彻底清除。 + +## 设计说明 + +- **匿名协议**:互评阶段输入仅含匿名标签/立场/要点;视角名只在独立作答阶段存在于各自的 system prompt 中。 +- **共识率确定性**:脚本按 `一致/(一致+分歧)` 计算,轮间比较与加辩门控全部基于该值,模型无法影响分母分子之外的东西。 +- **deep 档护栏**:强制包含反共识原型(Contrarian),对冲「辩论放大偏差共识」;人数不足再补综合连接(Synthesizer)。 +- **渊源**:机制蓝本来自 karpathy/llm-council 的匿名互评与主席制思想 + 本作者在 ZCode 上长期使用的 expert-panel 工作流实证迭代(v2.1:评测依据注入、单专家容错、样本完整度标注)。 + +## Changelog + +版本以插件市场显示为准(事实源在 plugin.json)。 + +- 0.1.1:修复自定义 ledger_path 为裸文件名时无法落账的边界问题;开源前清理(git 历史中性身份)。 +- 0.1.0:首个可发布版本,六阶段机制全量移植。 + +## License + +MIT(见 [LICENSE](LICENSE)) diff --git a/external_plugins/expert-panel/commands/expert-panel.md b/external_plugins/expert-panel/commands/expert-panel.md new file mode 100644 index 0000000..0d3a620 --- /dev/null +++ b/external_plugins/expert-panel/commands/expert-panel.md @@ -0,0 +1,8 @@ +--- +description: 把一个问题交给专家团共识评审(独立作答→匿名互评→仲裁→加辩→主席→复核) +--- + +按 expert-panel skill(本插件 `skills/expert-panel/SKILL.md`)的流程调用专家团工作流: + +- 用户消息中除指令外的内容就是要评审的问题(question);用户点名了档位就用(quick/standard/deep),没点名按 skill 默认 standard,急活可用 quick。 +- 调用方式、完整参数表、结果解读一律以 skill 为准。 diff --git a/external_plugins/expert-panel/skills/expert-panel/SKILL.md b/external_plugins/expert-panel/skills/expert-panel/SKILL.md new file mode 100644 index 0000000..99ce4f9 --- /dev/null +++ b/external_plugins/expert-panel/skills/expert-panel/SKILL.md @@ -0,0 +1,63 @@ +--- +name: expert-panel +description: 专家团共识评审:把重要决策/方案交给多位不同视角专家——独立作答→真匿名互评→仲裁归纳(共识率脚本确定性计算、不足自动加辩)→主席综合→事实复核,产出「稳定一致项 + 明示分歧」的结构化结论。当用户要"更稳"的结论、说 用专家团评估X / 让专家团给结论 / 多视角评审 / 共识评审 / 重要决策把关 时使用;急活用 quick 档(数分钟)。 +--- + +# expert-panel —— 专家团共识评审 + +## 机制(为什么可信) + +1. **独立作答**:N 位不同视角专家先各自独立表态,互不知晓(无从众)。 +2. **真匿名互评**:互评输入只有「专家A/B/C」标签 + 立场 + 要点,不含视角名(防权威偏置);各自提出反对/补充并按说服力排序。 +3. **仲裁归纳**:归纳为一致项/分歧项。**共识率 = 一致/(一致+分歧),由脚本确定性计算,不信模型自报**。 +4. **共识率门控加辩**:低于阈值(默认 0.6)自动加辩一轮,仍不足则**明示分歧交人工裁决,绝不硬合成**。 +5. **主席综合**:执行摘要 + 最终建议。 +6. **事实复核**:可客观验证的一致项由复核员核验(能读文件/跑命令就去验),不实的剔除;价值判断类标 unverifiable 原样保留。 + +单专家失败自动容错,结论标注样本完整度(缺席视角/有效专家数)。 + +## 怎么调用 + +工作流文件随本插件分发:**本 skill 目录的 `../../workflows/expert-panel.dwf.ts`**(相对本 SKILL.md)。 + +用 `CreateWorkflow` 的 **path 方式**调用(不要复制进用户的 workflows 目录,升级由插件统一管): + +``` +CreateWorkflow( + path = <按上述相对位置解析出的绝对路径>, + name = "专家团", + args = { question, profile, ... } +) +``` + +### 参数 + +| 参数 | 类型 | 默认 | 说明 | +|---|---|---|---| +| question | string | 必填 | 要专家团给出稳定结论的问题/决策 | +| profile | string | standard | quick(3 专家/1 轮,数分钟)\| standard(2 轮)\| deep(自动补反方质询+综合连接原型,2 轮) | +| experts | json | [务实工程师, 风险与边界审查, 产品价值] | 专家视角名数组(勿重复) | +| threshold | number | 0.6 | 共识率阈值,低于自动加辩一轮 | +| max_rounds | number | quick=1,其余=2 | 最大轮数(含首轮) | +| evidence | json | 空 | 证据包:文本或文件路径数组(专家/复核员用工具自行读取) | +| basis | json | 空 | 评测依据:文件路径或条目数组,给了就要求逐条对照判定并引用条目 | +| ledger_path | string | ~/.expert-panel/ledger.jsonl | 运行台账 JSONL 落盘路径 | +| simulate_failures | json | 空 | 测试用:要模拟作答失败的专家下标数组 | + +### 模型选择(可选) + +所有角色默认用**会话默认模型,零配置可跑**。要统一换模型:在 `CreateWorkflow` 调用上传 `subagent_model`(id 用 `ListModels` 查当前 provider 的行)。 +注意:按角色分模型(如互评用轻量模型)是宿主刻意不支持的——子代理模型必须在运行前已知;请勿尝试在脚本里做运行时路由。 + +### 结果解读 + +- `agreementRate ≥ threshold`:达成共识,`agreed` 为经复核的稳定结论;`unverifiable` 是价值判断类。 +- `< threshold`:`disputed` 已列明各方论据,交人工裁决——这是特性不是失败。 +- `absent` 非空或有效专家 < 2:结论已标注样本完整度,引用时注意口径。 +- 每轮运行一行追加台账(`ledger_path`):问题摘要/档位/专家数/轮数/共识率/一致分歧计数。 + +## 边界 + +- 价值判断类结论无法客观复核(标 unverifiable),仅代表专家团意见。 +- 台账写入失败不阻断结论(结果 `verified` 里会注明未入账)。 +- 深档会显著增加 token 消耗,重要决策再用。 diff --git a/external_plugins/expert-panel/workflows/expert-panel.dwf.ts b/external_plugins/expert-panel/workflows/expert-panel.dwf.ts new file mode 100644 index 0000000..b613609 --- /dev/null +++ b/external_plugins/expert-panel/workflows/expert-panel.dwf.ts @@ -0,0 +1,406 @@ +/* zcode-workflow +description: 专家团共识工作流:N 位不同视角的专家独立作答 → 真匿名交叉互评(只见 专家A/B/C 标签, + 不见视角名)→ 仲裁人归纳一致项/分歧项(共识率由脚本确定性计算,低于阈值自动加辩)→ 主席执行摘要 → + 可验证事实复核。单专家失败自动容错并标注样本完整度;profile(quick/standard/deep)控制轮数与专家构成。 + 可移植版:所有角色用会话默认模型,零配置可跑;要统一换模型在调用层用 CreateWorkflow 的 subagent_model。 + 输出按「一致(稳定)/分歧(明示)」分离,防硬合成与偏见共识。 +whenToUse: 用户要"更稳"的结论/重要决策/方案评审/多视角评估时;说"用专家团评估 X / 让专家团给结论"。 + 急活用 profile=quick(数分钟),重要产出用 standard,最高把关用 deep。共识率不足自动加辩, + 仍不足则明示分歧交人工裁决。 +args: + basis: + type: json + description: 评测依据,文件路径或条目数组;给了就要求专家与复核员逐条对照依据判定并引用所在条目(空数组=明确不用,默认不注入) + evidence: + type: json + description: 证据包,文本或文件路径数组(路径由专家/复核员用工具自行读取),默认空 + experts: + type: json + description: 专家视角名数组(勿重复),默认 [务实工程师, 风险与边界审查, 产品价值] + ledger_path: + type: string + description: 运行台账 JSONL 落盘路径(每轮一行),默认 ~/.expert-panel/ledger.jsonl + max_rounds: + type: number + description: 最大轮数(含首轮),不填由 profile 决定(quick=1,其余=2) + profile: + type: string + description: quick(3专家/1轮)| standard(默认,2轮)| deep(自动补足反方质询与综合连接两类原型/2轮) + default: standard + question: + type: string + description: 要专家团给出稳定结论的问题/决策 + required: true + simulate_failures: + type: json + description: 测试用,要模拟作答失败的专家下标数组,默认空 + threshold: + type: number + description: 共识率阈值,低于则再辩一轮,仍低则明示分歧交人工裁决 + default: 0.6 +*/ +interface ExpertAnswer { + /** 对问题的立场或方案(一句话)。 */ + position: string; + /** 支撑要点,每条尽量独立可核。 */ + keyPoints: string[]; + /** 自评置信度 0-1。 */ + confidence: number; +} + +interface Objection { + /** 针对哪位匿名专家,填其标签(如 专家B)。 */ + target: string; + /** 反对或补充意见。 */ + comment: string; +} + +interface Critique { + /** 对其他专家立场的反对与补充。 */ + objections: Objection[]; + /** 其他专家按说服力从高到低的标签排序(如 ["专家C","专家A"])。 */ + ranking: string[]; +} + +interface Revision { + /** 修订后的立场(可维持原判)。 */ + position: string; + /** 修订后要点。 */ + keyPoints: string[]; + /** 是否改变了立场。 */ + changed: boolean; +} + +interface Dispute { + /** 分歧主题。 */ + topic: string; + /** 各方论据摘要(用专家标签指代)。 */ + sides: string; +} + +interface Consensus { + /** 各专家均认可的结论——稳定输出。 */ + agreed: string[]; + /** 仍存分歧的结论。 */ + disputed: Dispute[]; + /** 仲裁人的最终建议。 */ + recommendation: string; +} + +interface Verification { + /** 复核后保留的一致项。 */ + agreed: string[]; + /** 复核未通过的项及原因。 */ + rejected: { item: string; reason: string }[]; + /** 价值判断类或无法客观复核的项,原样保留。 */ + unverifiable: string[]; +} + +interface ChairSummary { + /** 3-5 句执行摘要:共识是什么、分歧在哪、怎么收敛的。 */ + executiveSummary: string; + /** 面向用户的明确最终建议,含适用条件。 */ + finalRecommendation: string; +} + +/** 共识率由脚本确定性计算,不信仲裁人自报的数字。 */ +function computeRate(c: Consensus): number { + const a = c.agreed.length; + const d = c.disputed.length; + return a + d === 0 ? 0 : a / (a + d); +} + +/** 专家下标 → 匿名标签(专家A/B/C…)。 */ +function labelOf(i: number): string { + return `专家${String.fromCharCode(65 + i)}`; +} + +// ---------- 参数解析 ---------- +const question = String(args.question); +const profileRaw = String(args.profile ?? "standard"); +const profile: "quick" | "standard" | "deep" = profileRaw === "quick" ? "quick" : profileRaw === "deep" ? "deep" : "standard"; +const defaultLenses = ["务实工程师", "风险与边界审查", "产品价值"]; +let lenses: string[] = Array.isArray(args.experts) && (args.experts as unknown[]).length > 0 + ? (args.experts as unknown[]).map(String) + : defaultLenses; +if (profile === "quick") lenses = lenses.slice(0, 3); +if (profile === "deep") { + // 护栏:deep 档必须含反共识原型(对冲「辩论放大偏差共识」),人数不足再补综合型 + if (!lenses.some((l) => /反方|反对|质询|质疑|Contrarian|魔鬼/i.test(l))) lenses = [...lenses, "反方质询(Contrarian)"]; + if (lenses.length < 4) lenses = [...lenses, "综合连接(Synthesizer)"]; +} +const threshold = typeof args.threshold === "number" && args.threshold > 0 && args.threshold <= 1 ? args.threshold : 0.6; +const maxRounds = typeof args.max_rounds === "number" && args.max_rounds >= 1 + ? Math.floor(args.max_rounds) + : profile === "quick" ? 1 : 2; +const simulateFailures: number[] = Array.isArray(args.simulate_failures) + ? (args.simulate_failures as unknown[]).map(Number).filter((n) => Number.isInteger(n)) + : []; +const evidence: string[] = Array.isArray(args.evidence) + ? (args.evidence as unknown[]).map(String).filter((s) => s.trim().length > 0) + : []; +const evidenceBlock = evidence.length > 0 + ? `\n证据包(${evidence.length} 项;文件路径请用你的工具读取后再引用,结论须标注证据出处):\n${evidence.map((e, i) => `[${i + 1}] ${e}`).join("\n")}` + : ""; +const basisGiven = Array.isArray(args.basis); +const basis: string[] = basisGiven + ? (args.basis as unknown[]).map(String).filter((s) => s.trim().length > 0) + : []; +const basisBlock = basis.length > 0 + ? `\n评测依据(调用方指定;评审前用工具读取与问题相关的原文,结论须对照依据逐条判定并引用所在条目,不凭印象打分;与问题无关的条目可略过并说明):\n${basis.map((b, i) => `[${i + 1}] ${b}`).join("\n")}` + : ""; +const ledgerPath = typeof args.ledger_path === "string" && args.ledger_path.trim().length > 0 + ? args.ledger_path.trim() + : "~/.expert-panel/ledger.jsonl"; + +// ---------- 仪表盘 ---------- +artifact.board("panel", { + title: "专家团状态", + key: "name", + status: "stage", + columns: ["独立作答", "交叉互评", "修订表态", "已收敛", "缺席"], + cardTitle: "name", + detail: [{ field: "position", label: "立场" }], +}); +artifact.chart("consensus", { + title: "共识率收敛曲线", + x: { field: "round", label: "轮次" }, + y: { field: "rate", label: "共识率" }, + baseline: { field: "target", label: "阈值" }, +}); +artifact.metrics("stats", { + title: "专家团指标", + metrics: [ + { field: "ratePct", label: "共识率" }, + { field: "round", label: "轮次" }, + { field: "alive", label: "有效专家" }, + { field: "profileLabel", label: "档位" }, + ], +}); + +const experts = lenses.map((lens) => + agent(`专家·${lens}`, { + system: + `你是「${lens}」视角的资深专家。先独立给出立场、3-6 条支撑要点和置信度;` + + "只凭自己的专业判断,不要猜测别人会怎么说,也不要为了和谐改变有依据的立场;确实被说服才改。用中文回答。", + }), +); + +phase("独立作答:每位专家独立给出立场与论据"); +// 仲裁需要全部有效专家的独立立场,此处必须等齐;单个专家失败不拖垮整体(allSettled) +const settled = await Promise.allSettled( + lenses.map((lens, i) => + simulateFailures.includes(i) + ? Promise.reject(new Error("模拟作答失败(测试注入)")) + : experts[i].ask(`问题:${question}\n视角:${lens}${evidenceBlock}${basisBlock}`), + ), +); +const active: number[] = []; +const answers: ExpertAnswer[] = []; +settled.forEach((s, i) => { + if (s.status === "fulfilled") { + active.push(i); + answers.push(s.value); + report({ name: lenses[i], stage: "独立作答", position: s.value.position }, "panel"); + } else { + report({ name: lenses[i], stage: "缺席", position: `作答失败:${String(s.reason).slice(0, 80)}` }, "panel"); + } +}); +if (active.length === 0) throw new Error("所有专家均未完成作答,无法仲裁"); +const absent = lenses.filter((_, i) => !active.includes(i)); +log(`${active.length}/${lenses.length} 位专家完成独立作答${absent.length > 0 ? `(缺席:${absent.join("、")})` : ""}`); + +/** 第 m 位有效专家能看到的他人立场:只有匿名标签、立场、要点——不含视角名与模型。 */ +function othersFor(m: number) { + return active + .map((idx, n) => ({ idx, n })) + .filter((o) => o.n !== m) + .map((o) => ({ 专家: labelOf(o.idx), position: answers[o.n].position, keyPoints: answers[o.n].keyPoints })); +} + +phase("交叉互评:匿名互看立场,提出反对与补充"); +const critiqueSettled = await Promise.allSettled( + active.map((idx, m) => + agent(`互评·${lenses[idx]}`, { + system: + `你是「${lenses[idx]}」视角的独立评审。只针对其他专家的匿名立场提出反对与补充,` + + "不重复自己的论点,就事论事。用中文回答。", + }).ask( + `问题:${question}\n你此前的立场:${answers[m].position}\n` + + `其他专家的匿名立场:${JSON.stringify(othersFor(m))}\n` + + "请只针对他们的立场提出反对与补充(target 填对方标签),并给出 ranking(对方标签数组,说服力从高到低)。", + ), + ), +); +const critiques: Critique[] = critiqueSettled.map((s) => (s.status === "fulfilled" ? s.value : { objections: [], ranking: [] })); +active.forEach((idx, m) => report({ name: lenses[idx], stage: "交叉互评", position: answers[m].position }, "panel")); + +phase("仲裁归纳:区分一致项与分歧项"); +const arbitrator = agent("仲裁人", { + system: + "你是仲裁人:把各位专家的表态归纳为『一致项』(所有人都认可,视为稳定结论)与『分歧项』(列明各方论据,不强行折中)," + + "并给出最终建议。宁可如实呈现分歧,不要制造虚假共识。用中文回答。", +}); +const positionsOf = () => + active.map((idx, m) => ({ + 专家: labelOf(idx), + position: answers[m].position, + keyPoints: answers[m].keyPoints, + confidence: answers[m].confidence, + })); +let consensus = await arbitrator.ask( + `问题:${question}\n专家表态(匿名):${JSON.stringify(positionsOf())}\n相互意见:${JSON.stringify(critiques)}\n请归纳共识与分歧。`, +); +let rate = computeRate(consensus); +let round = 1; +report({ round, rate, target: threshold }, "consensus"); +report({ ratePct: Math.round(rate * 100) + "%", round, alive: `${active.length}/${lenses.length}`, profileLabel: `${profile}` }, "stats"); +log(`第 1 轮共识率 ${Math.round(rate * 100)}%(一致 ${consensus.agreed.length} / 分歧 ${consensus.disputed.length})`); + +phase("修订表态与再仲裁:共识率不足自动加辩"); +while (rate < threshold && round < maxRounds) { + round += 1; + const revised = await Promise.allSettled( + active.map((idx, m) => + experts[idx].ask( + `仲裁归纳出的分歧项:${JSON.stringify(consensus.disputed)}\n你此前的立场:${answers[m].position}\n` + + "请针对分歧重新表态(可坚持,被说服则修改并说明)。", + ), + ), + ); + const revisedPositions = active.map((idx, m) => { + const r = revised[m]; + if (r.status === "fulfilled") { + answers[m] = { position: r.value.position, keyPoints: r.value.keyPoints, confidence: answers[m].confidence }; + report({ name: lenses[idx], stage: "修订表态", position: r.value.position }, "panel"); + return { 专家: labelOf(idx), position: r.value.position, keyPoints: r.value.keyPoints, changed: r.value.changed }; + } + return { 专家: labelOf(idx), position: answers[m].position, keyPoints: answers[m].keyPoints, changed: false }; + }); + consensus = await arbitrator.ask( + `第 ${round} 轮专家表态(匿名):${JSON.stringify(revisedPositions)}\n上一轮归纳:${JSON.stringify(consensus)}\n请重新归纳一致项与分歧项。`, + ); + rate = computeRate(consensus); + report({ round, rate, target: threshold }, "consensus"); + report({ ratePct: Math.round(rate * 100) + "%", round, alive: `${active.length}/${lenses.length}`, profileLabel: `${profile}` }, "stats"); + log(`第 ${round} 轮收敛后共识率 ${Math.round(rate * 100)}%`); +} +active.forEach((idx, m) => report({ name: lenses[idx], stage: "已收敛", position: answers[m].position }, "panel")); + +phase("主席综合:产出执行摘要与最终建议"); +const chair = agent("主席", { + system: + "你是专家团主席。综合专家立场、互评排序与仲裁归纳,为用户写终稿:执行摘要 + 明确的最终建议;" + + "分歧未解决时如实呈现分歧而非硬折中;有效专家不足 2 位时须明示结论未经多方独立验证。用中文回答。", +}); +const rankings = critiques.map((c, m) => ({ 评审: labelOf(active[m]), ranking: c.ranking })); +const chairOut = await chair.ask( + `问题:${question}\n有效专家 ${active.length}/${lenses.length}\n` + + `专家最终立场(匿名):${JSON.stringify(positionsOf())}\n互评排序:${JSON.stringify(rankings)}\n` + + `最终仲裁归纳:${JSON.stringify(consensus)}\n请给出执行摘要与最终建议。`, +); + +phase("事实复核与落账:复核可验证一致项,记录本次运行"); +const verification = await agent("事实复核员", { + system: + "你是事实复核员:只复核可客观验证的事实型断言(读代码/查文件/跑命令能判定的就去做;" + + "给了证据包则逐条对证并标注出处);价值判断类结论归入 unverifiable 原样保留。不改任何文件。用中文回答。", +}).ask(`问题语境:${question}\n待复核一致项:${JSON.stringify(consensus.agreed)}${evidenceBlock}${basisBlock}`); + +const stable = rate >= threshold; +// 运行台账:一行 JSONL 追加到 ledger_path(默认 ~/.expert-panel/ledger.jsonl;时间戳由 python 侧生成) +const ledgerEntry = { + question: question.slice(0, 120), + profile, + experts: lenses.length, + alive: active.length, + rounds: round, + rate: Math.round(rate * 1000) / 1000, + agreed: verification.agreed.length, + disputed: consensus.disputed.length, + rejected: verification.rejected.length, + stable, +}; +const LEDGER_CODE = [ + "import sys, json, os, datetime", + "p = os.path.expanduser(sys.argv[2])", + "d = os.path.dirname(p)", + "if d: os.makedirs(d, exist_ok=True)", + "e = json.loads(sys.argv[1]); e['ts'] = datetime.datetime.now().isoformat(timespec='seconds')", + "open(p, 'a', encoding='utf-8').write(json.dumps(e, ensure_ascii=False) + '\\n')", + "print(p)", +].join("\n"); +const ledger = await world.run("python3", ["-c", LEDGER_CODE, JSON.stringify(ledgerEntry), ledgerPath]); +log(ledger.exitCode === 0 ? `台账已追加:${ledger.stdout.trim()}` : `台账写入失败(不阻断结论):${ledger.stderr.slice(0, 120)}`); +const sampleNote = absent.length > 0 + ? `样本完整度 ${active.length}/${lenses.length}(缺席:${absent.join("、")}),结论未经全体专家独立验证` + : active.length < 2 + ? "仅 1 位有效专家,共识率反映的是无异议而非多方独立验证" + : ""; + +await artifact.markdown( + "summary", + [ + `# 专家团结论·${question.slice(0, 40)}`, + "", + `- 共识率:${Math.round(rate * 100)}%(阈值 ${Math.round(threshold * 100)}%,${round} 轮收敛,${stable ? "达成共识" : "未达阈值"})`, + `- 档位 ${profile} · 有效专家 ${active.length}/${lenses.length}:${active.map((i) => lenses[i]).join("、")}`, + `- 一致项(稳定结论):${consensus.agreed.length} 条;分歧项:${consensus.disputed.length} 条;复核剔除:${verification.rejected.length} 条`, + ...(sampleNote ? [`- ⚠️ ${sampleNote}`] : []), + "", + "## 一致项(经复核)", + ...verification.agreed.map((s) => `- ✅ ${s}`), + ...verification.unverifiable.map((s) => `- ❓ ${s}(价值判断/无法客观复核)`), + "", + "## 仍存分歧(明示,不强行折中)", + ...consensus.disputed.map((d) => `- ⚠️ ${d.topic}:${d.sides}`), + ...(verification.rejected.length > 0 + ? ["", "## 复核未通过", ...verification.rejected.map((r) => `- ❌ ${r.item}(${r.reason})`)] + : []), + "", + "## 执行摘要(主席)", + chairOut.executiveSummary, + "", + "## 最终建议(主席)", + chairOut.finalRecommendation, + "", + "## 仲裁建议(附录)", + consensus.recommendation, + ].join("\n"), + { + title: `专家团结论·${question.slice(0, 24)}`, + primary: true, + description: `共识率 ${Math.round(rate * 100)}%,一致 ${consensus.agreed.length} 条 / 分歧 ${consensus.disputed.length} 条,有效专家 ${active.length}/${lenses.length}。`, + }, +); + +return { + conclusion: stable + ? `专家团达成共识(共识率 ${Math.round(rate * 100)}%,${round} 轮):${verification.agreed.length} 条稳定结论 + ${consensus.disputed.length} 条明示分歧;仲裁建议已给出。${sampleNote ? "⚠️ " + sampleNote : ""}` + : `共识率 ${Math.round(rate * 100)}% 低于阈值,未强行合成:${consensus.disputed.length} 条分歧已列明各方论据,建议人工裁决。${sampleNote ? "⚠️ " + sampleNote : ""}`, + agreed: verification.agreed, + unverifiable: verification.unverifiable, + disputed: consensus.disputed, + rejected: verification.rejected, + executiveSummary: chairOut.executiveSummary, + recommendation: chairOut.finalRecommendation || consensus.recommendation, + arbiterNote: consensus.recommendation, + agreementRate: rate, + rounds: round, + profile, + participants: active.map((i) => lenses[i]), + absent, + verified: [ + "每位专家独立作答后才进入互评(无从众)", + "互评输入只含匿名标签/立场/要点,不含视角名(防权威偏置)", + "共识率由脚本按一致项/(一致项+分歧项) 确定性计算,非模型自报", + "可验证事实经事实复核员把关", + ...(basis.length > 0 ? ["评测依据已按 basis 参数注入,结论要求逐条对照依据"] : []), + ledger.exitCode === 0 ? `运行台账已追加 ${ledger.stdout.trim()}` : "台账写入失败,本次运行未入账", + ], + notCovered: [ + stable ? "" : "共识不足,结论未定", + "价值判断类结论无法客观复核,仅代表专家团意见", + absent.length > 0 ? `缺席专家视角未覆盖:${absent.join("、")}` : "", + ].filter(Boolean), +}; diff --git a/marketplace.json b/marketplace.json index 350c942..ded1d7c 100644 --- a/marketplace.json +++ b/marketplace.json @@ -1386,6 +1386,25 @@ "unity-cli", "skills" ] + }, + { + "name": "expert-panel", + "displayName": "专家团共识评审 Expert Panel", + "displayName_i18n": { + "en": "Expert Panel Consensus Review", + "zh-CN": "专家团共识评审" + }, + "version": "0.1.1", + "source": "./external_plugins/expert-panel", + "description": "把重要决策交给多位不同视角专家:独立作答→真匿名互评→仲裁归纳(共识率由脚本确定性计算,不足自动加辩)→主席综合→事实复核,产出稳定一致项+明示分歧,绝不硬合成。零配置,默认用会话模型。", + "description_i18n": { + "en": "Expert-panel consensus review: independent answers from multiple expert lenses → truly anonymous cross-critique → arbitration (consensus rate computed by script, extra debate round below threshold) → chair synthesis → fact verification; quick/standard/deep profiles, zero config, conclusions separate agreed items from explicit disagreements.", + "zh-CN": "专家团共识评审:多位不同视角专家独立作答→真匿名互评→仲裁归纳(共识率脚本计算、不足自动加辩)→主席综合→事实复核;quick/standard/deep 三档,零配置,结论按一致/分歧分离绝不硬合成。" + }, + "category": "productivity", + "author": { + "name": "Zhang24y" + } } ] } From 141bcd7c99e66452a0b0952d42d116231c805e38 Mon Sep 17 00:00:00 2001 From: Zhang24y <177240371+Zhang24y@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:36:59 +0800 Subject: [PATCH 2/4] feat(expert-panel): add plugin icon --- assets/expert-panel/icon.png | Bin 0 -> 31319 bytes marketplace.json | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 assets/expert-panel/icon.png diff --git a/assets/expert-panel/icon.png b/assets/expert-panel/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..56f3d89a440118846c7aa2b752a0b8d6da62a15d GIT binary patch literal 31319 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 Date: Fri, 23 Oct 2026 17:34:07 +0800 Subject: [PATCH 3/4] feat(expert-panel): bump to 0.1.2 with ledger schema v2 --- .../expert-panel/.zcode-plugin/plugin.json | 2 +- external_plugins/expert-panel/README.md | 1 + external_plugins/expert-panel/README_CN.md | 1 + .../workflows/expert-panel.dwf.ts | 25 +++++++++++++++++-- marketplace.json | 5 ++-- 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/external_plugins/expert-panel/.zcode-plugin/plugin.json b/external_plugins/expert-panel/.zcode-plugin/plugin.json index ab674fd..0f44ceb 100644 --- a/external_plugins/expert-panel/.zcode-plugin/plugin.json +++ b/external_plugins/expert-panel/.zcode-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "expert-panel", - "version": "0.1.1", + "version": "0.1.2", "description": "专家团共识评审:多位不同视角专家独立作答→真匿名互评→仲裁归纳(共识率脚本计算、不足自动加辩)→主席综合→事实复核;quick/standard/deep 三档,零配置,结论按一致/分歧分离绝不硬合成。", "description_i18n": { "en": "Expert-panel consensus review: independent answers from multiple expert lenses → truly anonymous cross-critique → arbitration (consensus rate computed by script, extra debate round below threshold) → chair synthesis → fact verification; quick/standard/deep profiles, zero config, conclusions separate agreed items from explicit disagreements.", diff --git a/external_plugins/expert-panel/README.md b/external_plugins/expert-panel/README.md index df78418..da2576c 100644 --- a/external_plugins/expert-panel/README.md +++ b/external_plugins/expert-panel/README.md @@ -92,6 +92,7 @@ Expected (within minutes): an "Expert Panel Conclusion" card with a consensus ra Version is whatever the marketplace shows (the source of truth is plugin.json). +- 0.1.2: ledger schema v2 — dual consensus rates (rate_pre at arbitration time / rate_post recomputed after verification, fixing the old unrecomputable-rate bug), per-round convergence series, and threshold/question_id fields logged in full. - 0.1.1: fix ledger write for bare-filename custom paths; pre-release cleanup (neutral git identity). - 0.1.0: first releasable version, full six-stage mechanism. diff --git a/external_plugins/expert-panel/README_CN.md b/external_plugins/expert-panel/README_CN.md index abc9acc..80035d8 100644 --- a/external_plugins/expert-panel/README_CN.md +++ b/external_plugins/expert-panel/README_CN.md @@ -97,6 +97,7 @@ zcode plugins install expert-panel@<市场名> 版本以插件市场显示为准(事实源在 plugin.json)。 +- 0.1.2:台账 schema v2——双口径共识率(rate_pre 仲裁时点 / rate_post 复核后重算,修复旧版 rate 不可复算问题)、逐轮收敛序列、threshold/question_id 等字段全量入账。 - 0.1.1:修复自定义 ledger_path 为裸文件名时无法落账的边界问题;开源前清理(git 历史中性身份)。 - 0.1.0:首个可发布版本,六阶段机制全量移植。 diff --git a/external_plugins/expert-panel/workflows/expert-panel.dwf.ts b/external_plugins/expert-panel/workflows/expert-panel.dwf.ts index b613609..f856ec9 100644 --- a/external_plugins/expert-panel/workflows/expert-panel.dwf.ts +++ b/external_plugins/expert-panel/workflows/expert-panel.dwf.ts @@ -31,6 +31,9 @@ args: type: string description: 要专家团给出稳定结论的问题/决策 required: true + question_id: + type: string + description: 题目标识(基准跑批/回溯归因用),默认空 simulate_failures: type: json description: 测试用,要模拟作答失败的专家下标数组,默认空 @@ -252,6 +255,8 @@ let consensus = await arbitrator.ask( `问题:${question}\n专家表态(匿名):${JSON.stringify(positionsOf())}\n相互意见:${JSON.stringify(critiques)}\n请归纳共识与分歧。`, ); let rate = computeRate(consensus); +// 逐轮共识率序列(schema v2:台账记录收敛轨迹) +const roundsSeq: number[] = [Math.round(rate * 1000) / 1000]; let round = 1; report({ round, rate, target: threshold }, "consensus"); report({ ratePct: Math.round(rate * 100) + "%", round, alive: `${active.length}/${lenses.length}`, profileLabel: `${profile}` }, "stats"); @@ -281,6 +286,7 @@ while (rate < threshold && round < maxRounds) { `第 ${round} 轮专家表态(匿名):${JSON.stringify(revisedPositions)}\n上一轮归纳:${JSON.stringify(consensus)}\n请重新归纳一致项与分歧项。`, ); rate = computeRate(consensus); + roundsSeq.push(Math.round(rate * 1000) / 1000); report({ round, rate, target: threshold }, "consensus"); report({ ratePct: Math.round(rate * 100) + "%", round, alive: `${active.length}/${lenses.length}`, profileLabel: `${profile}` }, "stats"); log(`第 ${round} 轮收敛后共识率 ${Math.round(rate * 100)}%`); @@ -308,18 +314,33 @@ const verification = await agent("事实复核员", { }).ask(`问题语境:${question}\n待复核一致项:${JSON.stringify(consensus.agreed)}${evidenceBlock}${basisBlock}`); const stable = rate >= threshold; -// 运行台账:一行 JSONL 追加到 ledger_path(默认 ~/.expert-panel/ledger.jsonl;时间戳由 python 侧生成) +// schema v2(2026-10-23 P0 修复):双口径共识率——rate_pre=仲裁时点(复核前)、rate_post=复核后重算, +// 消除旧版三时点混用(rate 不可复算)问题。tokens 由回填脚本补齐;adopted 为采纳标记预留。 +const ratePre = Math.round(rate * 1000) / 1000; +const ratePostDenom = verification.agreed.length + consensus.disputed.length; +const ratePost = ratePostDenom === 0 ? 0 : Math.round((verification.agreed.length / ratePostDenom) * 1000) / 1000; const ledgerEntry = { + schema: 2, + version: "0.1.2", question: question.slice(0, 120), + question_id: String(args.question_id ?? ""), profile, + model: "session-default", experts: lenses.length, alive: active.length, rounds: round, - rate: Math.round(rate * 1000) / 1000, + rounds_seq: roundsSeq, + threshold, + rate_pre: ratePre, + rate_post: ratePost, + rate: ratePre, agreed: verification.agreed.length, disputed: consensus.disputed.length, rejected: verification.rejected.length, + unverifiable: verification.unverifiable.length, stable, + tokens: null, + adopted: null, }; const LEDGER_CODE = [ "import sys, json, os, datetime", diff --git a/marketplace.json b/marketplace.json index 8211f14..740636d 100644 --- a/marketplace.json +++ b/marketplace.json @@ -1394,7 +1394,7 @@ "en": "Expert Panel Consensus Review", "zh-CN": "专家团共识评审" }, - "version": "0.1.1", + "version": "0.1.2", "source": "./external_plugins/expert-panel", "description": "把重要决策交给多位不同视角专家:独立作答→真匿名互评→仲裁归纳(共识率由脚本确定性计算,不足自动加辩)→主席综合→事实复核,产出稳定一致项+明示分歧,绝不硬合成。零配置,默认用会话模型。", "description_i18n": { @@ -1404,8 +1404,7 @@ "category": "productivity", "author": { "name": "Zhang24y" - }, - "icon": "https://cdn-zcode.z.ai/zcode/official-plugin/assets/expert-panel/icon.png" + } } ] } From f47c323792e76f1ed4dc7917298375d90cdb7d42 Mon Sep 17 00:00:00 2001 From: Zhang24y <177240371+Zhang24y@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:40:28 +0800 Subject: [PATCH 4/4] feat(expert-panel): bump to 0.1.2 with ledger schema v2