pi-pstack-native 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,10 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 1.0.1 — 2026-11-01
4
4
 
5
+ - Show Chinese beside the English text in `/setup-pstack` prompts, options, review rows and notices, in both the direct UI and RPC mode. English option values stay stable, so existing UI integrations keep working.
6
+ - Suggest the first panel seat from the upstream Claude family and the second from the Grok family when Pi exposes them, instead of reusing the parent model.
7
+ - Keep cross-judge selection inside the configured panel pool. When no eligible entry exists, report it and return candidates for the coordinator to compare instead of widening the pool to any available model.
8
+ - Ask the optional verification follow-up to answer in Chinese and English.
9
+
10
+ ## 1.0.0 — 2026-10-30
11
+
12
+ - Publish `pi-pstack-native@1.0.0` on npm and verify the public `latest` dist-tag.
5
13
  - Archive a commit-bound authenticated real-model receipt: three models passed the read-only probe on `5deb723`, and `SEMANTICS.json.evidence.realModel` records `status: "passed"` with that receipt.
6
14
  - Validate live GitHub delivery against a disposable repository: create, read, close and merge now have a recorded run. Add an opt-in `--merge` gate to the live tier; it stays closed unless `PSTACK_E2E_LIVE_MERGE=1` is set with an explicit write target.
7
- - Record the npm registry state: `pi-pstack-native` is unpublished, so `1.0.0` is unclaimed; no npm credentials exist in the validation environment.
15
+ - Record the npm registry state before publication: `pi-pstack-native` was unpublished, so `1.0.0` was unclaimed; no npm credentials existed in the validation environment.
8
16
  - Record the maintainer semantic sign-off in `SEMANTICS.json.review` for the 11 core entrypoints and their declared deviations.
9
17
  - State the 1.0.0 guarantee scope explicitly in `docs/PUBLISHING.md`, including the exclusion of Windows real-model workflow acceptance and of branch-protection, review, auto-merge and merge-queue behaviour.
10
18
 
package/README.md CHANGED
@@ -52,9 +52,13 @@ In Pi, choose role models and a child reasoning level:
52
52
  /poteto-mode Fix the login bug, run relevant tests, and report the evidence.
53
53
  ```
54
54
 
55
- Setup starts with a reasoning budget and previews role and task models and suggested panels from the models available to this Pi session. Page through the table, save it, or edit individual choices and panel seats. Existing settings are retained; cancelling before saving leaves the file unchanged. Unavailable models and unsupported reasoning are marked so they can be fixed in the same setup. `inherit-parent` and `auto` use the parent model. Repeated seats are separate sessions, not model diversity.
55
+ Setup provides Chinese and English guidance. It starts with a reasoning budget, then previews roles, task models, and suggested panels using models available to this Pi session. Review the pages and save the settings, or edit individual choices and panel seats. Existing settings are retained; cancelling before saving leaves the file unchanged. Unavailable models and unsupported reasoning are marked so you can fix them before saving. `inherit-parent` and `auto` use the parent session's model.
56
56
 
57
- After saving, setup starts a model turn to inspect real-app and formal verification capabilities and offer to create missing project-local Skills. The agent is instructed to ask before creating a Skill; the follow-up depends on the selected model.
57
+ The four upstream budget presets map to Pi thinking levels: `unlimited` to `max`, `large` to `xhigh`, `medium` to `high`, and `small` to `medium`. Pi also offers its own thinking levels and a fallback policy. Model support varies; setup shows when a choice is unsupported or uses a lower level under the selected fallback policy. Unlike Cursor pstack, Pi sets a thinking level instead of rewriting model slugs.
58
+
59
+ A Pi role sets a general model for that kind of child agent. A task model overrides that role for a specific workflow and purpose; choosing the role default removes that override. Pi exposes these choices separately, so its list does not match the upstream rule line for line. A panel is an ordered list of model seats. Each runner or reviewer seat starts a separate child session; repeated models do not add model diversity. The Arena cross-judge pool is an ordered preference list for one judge. When the parent model's family is known, the judge must be from a different family. Parent aliases in that pool cannot act as judges.
60
+
61
+ After saving, if a parent model is selected, setup starts a model turn to inspect real-app and formal verification capabilities and offer to create missing project-local Skills. The agent is instructed to ask before creating a Skill; the follow-up depends on the selected model.
58
62
 
59
63
  Arena and architect use a cross-judge with confined read-only access to verified candidates when an eligible different-family model is available. Otherwise they return candidates for the coordinator to inspect and select. Interrogate uses isolated adversaries and separate reproduction sessions; the coordinator must inspect actual command evidence. See [model setup and routing](docs/REFERENCE.md#model-setup-and-routing) for usage and limitations.
60
64
 
package/README.zh-CN.md CHANGED
@@ -53,9 +53,13 @@ pi install npm:pi-pstack-native
53
53
  /setup-pstack
54
54
  ```
55
55
 
56
- 命令先询问推理预算,再分页展示角色、任务模型和根据当前 Pi 可用模型建议的面板。可以直接保存,也可以只修改某个角色、任务或面板席位;不可用模型和不支持的推理等级会标出,可在向导中修正。已有自定义设置会保留,保存前取消则不写入。一个面板席位对应一个独立子会话;多个席位使用相同模型不代表模型多样性。`inherit-parent` 和 `auto` 使用父会话模型。
56
+ 配置界面提供中文和英文说明。命令先询问推理预算,再分页展示角色、任务模型和根据当前 Pi 可用模型建议的面板。你可以直接保存,也可以只修改某个角色、任务或面板席位。界面会标出不可用的模型和不支持的推理等级。请在保存前修正这些选项。已有设置会保留。保存前取消不会写入文件。`inherit-parent` 和 `auto` 使用父会话的模型。
57
57
 
58
- 保存后,命令会启动一次模型对话,要求代理检查项目是否已有真实应用验证和形式化验证能力,并在缺少时询问是否创建项目本地 Skill。代理被要求在创建前等待同意;后续行为取决于所选模型。
58
+ 原版的 4 档预算对应 Pi 的推理等级:`unlimited` 对应 `max`,`large` 对应 `xhigh`,`medium` 对应 `high`,`small` 对应 `medium`。Pi 还提供其他推理等级和回退策略。回退策略决定模型不支持所选等级时是否允许使用较低等级。不同模型支持的等级不同。界面会显示不支持的选项和实际使用的较低等级。Cursor 原版通过更改模型名称设置等级。Pi 使用独立的推理等级设置。
59
+
60
+ Pi 的角色模型为一类子代理设置通用模型。任务模型为指定工作流和用途覆盖该角色的模型。选择角色默认值会删除该任务覆盖。Pi 分开显示这些设置,因此列表与原版规则不逐行相同。面板是按顺序排列的模型席位列表。每个执行者或评审席位启动一个独立子会话。多个席位使用相同模型不会增加模型多样性。Arena 交叉裁判池按顺序选择一个裁判。父模型的家族已知时,裁判必须来自不同的家族。池中的父会话别名不能担任裁判。
61
+
62
+ 保存后,如果已选择父会话模型,命令会启动一次模型对话。代理会检查项目是否已有真实应用验证和形式化验证能力。缺少时,代理会询问是否创建项目本地 Skill。代理被要求在创建前等待同意;后续行为取决于所选模型。
59
63
 
60
64
  Arena 和 Architect 在有可用异家族模型时使用交叉裁判;否则返回候选,由协调者选择和整合。Interrogate 在隔离工作区执行对抗调查,并由独立会话复跑;协调者仍须核对实际命令证据。详见[模型配置与路由](docs/REFERENCE.md#model-setup-and-routing)。
61
65
 
package/REVIEW.md CHANGED
@@ -1,4 +1,4 @@
1
- # v1.0.0 review & validation
1
+ # v1.0.1 review & validation
2
2
 
3
3
  ## Verified by the default local check
4
4
 
package/SEMANTICS.json CHANGED
@@ -2348,7 +2348,7 @@
2348
2348
  "src/extension.ts": "4f3743957540d722956a4eb501c1b7b9600d246ea55c3cf1ec46f5db51ec494b",
2349
2349
  "src/agent/profiles.ts": "8c845fa35a3db95319202ba9be73162c21323525d106eebddd9a5d72a49596ae",
2350
2350
  "src/agent/host.ts": "bdc2b76f56c1c533ad2367da6e2016bf53d6b3739b424021735a84e25fbd8416",
2351
- "src/models/router.ts": "45b5c042b063cd1e6c43c1f5db2404d72bad1a10086252ae9906553d7978344a",
2351
+ "src/models/router.ts": "1e8913355c2400741deaeecc3389c8fabd2b82a79a688fd358bcae87b201083e",
2352
2352
  "src/runtime/config.ts": "fd9178d910169de0bef7eb91f5fc80000fb3b96ecd7d65cd022b33780aed89b8",
2353
2353
  "src/runtime/lock.ts": "f7b68acb71d1e46d208f1720716143102ac2373beaee05c4ed8e2899c9e5af83",
2354
2354
  "src/contracts/brief.ts": "56241c23f75109eee0d9ad74f7d4f9743e0ce0dab7c1dcdf38345ba7c880a759",
@@ -2375,8 +2375,8 @@
2375
2375
  "test/verification.test.ts": "0b5490574f15768e72b15e08113960416ea7ef9b730bd4d373515b36a6bc0882",
2376
2376
  "test/latest-upstream.test.ts": "a66f9c6ec5e1991247570a7acb3394905fbe57b0a68c3ceb3a6abb0a9e69b370",
2377
2377
  "test/statechart.test.ts": "ef48f8de33f769e30b1091e92f0b23742b014fb844eceaf1221005ac6f9a5757",
2378
- "test/model-router.test.ts": "e2117cc4ab2e8b917267a0c3c05914a252f11cd7b3c15f9db6f1a29c557cc97a",
2379
- "test/setup-command.test.ts": "bda5f617a4058d7e08b4dd329ca4df8a2e040449078c3d11a3fb7b756ee1690d",
2378
+ "test/model-router.test.ts": "24234556f342d16274aa376f4f866950eedd1bc4f388c70ced98467945f0014c",
2379
+ "test/setup-command.test.ts": "6a85ab091e9871b696c6bd9e7d895d4f82e5edb22bdf42a7cfbab2e78dd398f4",
2380
2380
  "test/transaction.test.ts": "8d6b429ab0418f21237c99aa701fd3b8bd55c93c10adb3bd766787c33182693e",
2381
2381
  "test/benchmark.test.ts": "c8b40b94ec8778dabfd73b9282932e0c5e575e4964bcc50576fbb7c854e964f0",
2382
2382
  "scripts/check-statechart.mjs": "3e6fbf7cc00b623298a2d2ad0728b9762eae950f1fd2435d14a4c3ac6d38b2f1",
@@ -2384,7 +2384,7 @@
2384
2384
  "src/tools/workflow.ts": "942da3e7229938716660efd013b21218fd127305de38e030cc46325a1b8ac191",
2385
2385
  "src/agent/candidates.ts": "c8f3ef5f83fe44203894d8c370e5dea0f520c24e1c20dbf9d8add2ac682bb1ce",
2386
2386
  "src/worktree/snapshot.ts": "3e2691076b095b143a42bddf6bc38cd9a89ac3dcc91dbc90dadc820ba8acd799",
2387
- "test/model-panels.test.ts": "b4506fdfd05ffd0ea91e9736ae42a6694534fb53c4b976a2f7a35f8922b87b70",
2387
+ "test/model-panels.test.ts": "c7ba8dc9820935eb2eeabac43d203599b85d403899c39c3442dfb094994cce4d",
2388
2388
  "test/review-snapshot.test.ts": "cc93d43a402ed7a327ebc3ae734861e1fafc95968a6482e6810657705c57c937",
2389
2389
  "test/swarm-race.test.ts": "00447c4342ffd809f91cf37f3b0dfda8a88aa97e767e36de0f6ad365f02e2d69",
2390
2390
  "test/e2e/harness.ts": "9363a5beb9b41cbf4b74e6912544968dfecf4e5f0a5ab4829d30d78f13111618",
@@ -2392,8 +2392,8 @@
2392
2392
  "scripts/check-e2e.mjs": "f8a3332d01fe09fd18215a54da569f37e7955c39c5e4fbacb606c64df30e7c95",
2393
2393
  "scripts/build.mjs": "4c1ef7c3a43cdb0cf76be0440d06885770b1d38bd7f77a056cd6439de6bc15ac",
2394
2394
  "scripts/build-statechart.mjs": "4c30f6b1cb106d0c7d4031412331ad5473d947846cc4868bbac1fe6f14807c16",
2395
- "src/setup/command.ts": "3f97bab59272eb9ac7781ecc98bd98e62b0bb585b9178267ad599eee88dfe08e",
2396
- "test/setup-rpc.test.ts": "aa939cdd5b9d05976298fae3e99d93049e10909818ec841cbafd53ae2110164e"
2395
+ "src/setup/command.ts": "d2adc2c097c43758da6a3b780ede0bfa4c356da4e45c3cb9b23ef59d169e4e90",
2396
+ "test/setup-rpc.test.ts": "799722896e8102d7ba685b92b6b150beaf40abdd9ba66f370c4ea3ab2c6ff27d"
2397
2397
  },
2398
2398
  "driftReviews": [],
2399
2399
  "evidence": {
@@ -2524,6 +2524,10 @@
2524
2524
  {
2525
2525
  "file": "test/model-panels.test.ts",
2526
2526
  "name": "candidate inspection rejects workspace escapes and stale verified artifacts"
2527
+ },
2528
+ {
2529
+ "file": "test/model-panels.test.ts",
2530
+ "name": "judge selection respects the configured pool even when other families are available"
2527
2531
  }
2528
2532
  ]
2529
2533
  },
@@ -80,11 +80,6 @@ export function selectJudgeFromPool(ctx, pool) {
80
80
  };
81
81
  for (const selector of pool)
82
82
  resolve(selector);
83
- if (!choices.some(eligible) && typeof ctx.modelRegistry.getAvailable === 'function') {
84
- for (const selector of availableModelSelectors(ctx))
85
- if (!pool.includes(selector))
86
- resolve(selector);
87
- }
88
83
  const selected = choices.find(eligible);
89
84
  if (!parentFamily)
90
85
  notes.push('The parent model family is unknown; judge family independence cannot be established.');
@@ -141,9 +136,13 @@ export function availableModelSelectors(ctx) {
141
136
  export function suggestedPanels(ctx) {
142
137
  const available = availableModelSelectors(ctx);
143
138
  const parent = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
144
- const first = parent && available.includes(parent) ? parent : available[0] ?? 'inherit-parent';
139
+ // Upstream starts its panels with a judgment model and a code model.
140
+ // Use only models Pi exposes, and prefer the upstream families when present.
141
+ const first = available.find(item => modelFamily(item) === 'claude')
142
+ ?? (parent && available.includes(parent) ? parent : available[0]) ?? 'inherit-parent';
145
143
  const family = modelFamily(first);
146
- const second = available.find(item => item !== first && family && modelFamily(item) && modelFamily(item) !== family)
144
+ const second = available.find(item => item !== first && modelFamily(item) === 'grok' && family !== 'grok')
145
+ ?? available.find(item => item !== first && family && modelFamily(item) && modelFamily(item) !== family)
147
146
  ?? available.find(item => item !== first) ?? first;
148
147
  const runners = [first, second];
149
148
  // A judge pool is ordered by preference, not a copy of the entire registry.
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { availableModelSelectors, lowerSupportedThinking, supportsThinking, suggestedPanels } from "../models/router.js";
2
+ import { availableModelSelectors, lowerSupportedThinking, modelFamily, supportsThinking, suggestedPanels } from "../models/router.js";
3
3
  import { loadModels, saveModels } from "../runtime/config.js";
4
4
  import { modelConfigPath } from "../runtime/home.js";
5
5
  const ROLE_DESCRIPTIONS = {
@@ -39,6 +39,156 @@ const BUDGETS = [
39
39
  { value: "minimal", label: "minimal — least reasoning effort" },
40
40
  { value: "low", label: "low — lower reasoning effort" },
41
41
  ];
42
+ // Keep the English text stable for integrations while showing Chinese beside it.
43
+ const ZH_OPTIONS = {
44
+ "Next page →": "下一页 →", "← Previous page": "← 上一页",
45
+ "Cancel setup": "取消配置", "Back to review": "返回检查", "Back": "返回",
46
+ "Save settings": "保存设置", "Edit a role": "编辑通用角色", "Edit a task model": "编辑任务模型",
47
+ "Edit a panel": "编辑模型面板", "Change budget": "修改推理预算", "Change fallback": "修改回退策略",
48
+ "Done editing panel": "完成面板编辑", "Add a seat": "添加席位", "Change model": "更换模型",
49
+ "Remove seat": "删除席位", "Move up": "上移", "Move down": "下移", "Back to panel": "返回面板",
50
+ "Use role default": "使用角色默认模型",
51
+ "inherit-parent — parent model": "inherit-parent — 使用父会话模型", "auto — parent model": "auto — 使用父会话模型",
52
+ "unlimited — max reasoning": "不限 — max 推理", "large — xhigh reasoning": "大 — xhigh 推理",
53
+ "medium — high reasoning": "中 — high 推理", "small — medium reasoning": "小 — medium 推理",
54
+ "inherit-parent — use the parent session's reasoning level": "inherit-parent — 使用父会话的推理级别",
55
+ "off — disable reasoning": "off — 关闭推理", "minimal — least reasoning effort": "minimal — 最低推理强度",
56
+ "low — lower reasoning effort": "low — 较低推理强度",
57
+ "disabled — block unsupported reasoning": "disabled — 禁止不支持的推理级别",
58
+ "same-family — allow reported lower reasoning and model fallback": "same-family — 允许降低推理级别并回退模型",
59
+ };
60
+ const ZH_NAMES = {
61
+ worker: "执行开发", architect: "设计和开发", reviewer: "只读审查", scout: "只读探索",
62
+ verifier: "只读验证", synthesizer: "汇总结果", integrator: "合并工作",
63
+ "poteto-agent": "执行 Poteto 委派任务", "comment-sicko": "审查注释和变通方案",
64
+ feature: "功能开发", refactoring: "代码重构", "bug-fix": "修复错误", "performance issue": "性能问题",
65
+ hillclimb: "迭代改进", "how explorer": "探索实现方式", "how explainer": "解释实现方式",
66
+ "why investigators": "调查原因", "why synthesizer": "汇总原因", "swarm workers": "并行执行任务",
67
+ "reflect tooling": "反思:工具分析", "reflect judgment": "反思:判断", "reflect divergent": "反思:发散分析",
68
+ "reflect synthesizer": "反思:汇总", "arena runners": "arena 方案执行", "arena cross-judge pool": "arena 交叉评委池",
69
+ "architect runners": "architect 方案执行", "interrogate reviewers": "interrogate 审查",
70
+ };
71
+ function chineseName(name) {
72
+ return ZH_NAMES[name] ? `${name}(${ZH_NAMES[name]})` : name;
73
+ }
74
+ function optionChinese(option) {
75
+ if (ZH_OPTIONS[option])
76
+ return ZH_OPTIONS[option];
77
+ const seat = /^Seat (\d+): (.+)$/.exec(option);
78
+ if (seat)
79
+ return `席位 ${seat[1]}:${seat[2]}`;
80
+ const count = /^(.+) — (\d+) seats$/.exec(option);
81
+ if (count)
82
+ return `${chineseName(count[1])} — ${count[2]} 个席位`;
83
+ const role = /^(.+) — (.+)$/.exec(option);
84
+ if (role)
85
+ return `${chineseName(role[1])} — 当前模型:${role[2] === "role default" ? "角色默认模型" : role[2]}`;
86
+ return undefined;
87
+ }
88
+ function titleChinese(title) {
89
+ if (title.startsWith("Child reasoning budget"))
90
+ return "子代理推理预算。large 对应原版默认值;current 表示当前值。";
91
+ if (title.startsWith("Review pstack settings"))
92
+ return "检查 pstack 设置。need attention 表示需要处理。通用角色提供默认模型;任务模型覆盖对应任务的默认模型。";
93
+ if (title.startsWith("Choose role"))
94
+ return "选择要编辑的通用角色。任务模型没有单独设置时,使用对应角色的模型。";
95
+ if (title.startsWith("Choose task model"))
96
+ return "选择要编辑的任务模型。任务设置会覆盖对应角色的默认模型。";
97
+ if (title.startsWith("Choose panel"))
98
+ return "选择要编辑的模型面板。执行和审查面板的每个席位运行一个子代理。arena 评委池只选一名评委。父模型家族已知时,评委必须来自不同家族;父模型别名不担任评委。";
99
+ if (title.startsWith("Model fallback policy"))
100
+ return "模型回退策略";
101
+ if (title.startsWith("Add seat to "))
102
+ return "为面板添加席位。评委池按顺序选择一名评委。其他面板的每个席位运行一个子代理。";
103
+ if (title.startsWith("Model for "))
104
+ return `选择模型:${chineseName(title.slice("Model for ".length))}`;
105
+ if (title.startsWith("Seat "))
106
+ return "编辑席位";
107
+ if (/\(\d+ seats; \d+–16 allowed\)/.test(title))
108
+ return `编辑面板:${chineseName(title.slice(0, title.indexOf(" (")))}。评委池按顺序选择一名评委。其他面板的每个席位运行一个子代理。席位数量必须在显示的范围内。`;
109
+ return undefined;
110
+ }
111
+ async function selectBilingual(ctx, title, options) {
112
+ const chinese = titleChinese(title);
113
+ const lines = title.split("\n").map((line, index) => {
114
+ if (index === 0)
115
+ return line;
116
+ const translation = reviewRowChinese(line);
117
+ return translation ? `${line} / ${translation}` : line;
118
+ });
119
+ const labels = options.map(option => {
120
+ const translation = optionChinese(option);
121
+ return translation ? `${option} / ${translation}` : option;
122
+ });
123
+ const selected = await ctx.ui.select(chinese ? `${lines.join("\n")}\n${chinese}` : lines.join("\n"), labels);
124
+ const index = labels.indexOf(selected ?? "");
125
+ // Accept the English value as well for existing UI integrations.
126
+ return index >= 0 ? options[index] : selected;
127
+ }
128
+ function reviewRowChinese(row) {
129
+ if (row.startsWith("Budget:"))
130
+ return "推理预算与回退策略";
131
+ const label = /^([^:]+):/.exec(row)?.[1];
132
+ const role = label?.startsWith("Role ") ? label.slice(5) : undefined;
133
+ const name = role ?? label?.replace(/ \d+$/, "");
134
+ const description = name && ZH_NAMES[name] ? `${role ? "通用角色:" : ""}${ZH_NAMES[name]}` : undefined;
135
+ let status;
136
+ if (row.includes("not available; choose a model"))
137
+ status = "模型不可用,请重新选择模型";
138
+ else {
139
+ const unsupported = /does not support (\S+) reasoning/.exec(row);
140
+ const fallback = /uses (\S+) reasoning \(fallback\)/.exec(row);
141
+ const seatCount = /: (\d+) seats \[needs (\d+–16)\]/.exec(row);
142
+ if (unsupported)
143
+ status = `模型不支持 ${unsupported[1]} 推理,请选择其他模型或调整推理预算`;
144
+ else if (row.includes("[needs choice:"))
145
+ status = "需要重新选择模型或调整推理预算";
146
+ else if (row.includes("parent model not selected yet"))
147
+ status = "尚未选择父模型";
148
+ else if (fallback)
149
+ status = `回退到 ${fallback[1]} 推理级别`;
150
+ else if (row.includes("Pi maps "))
151
+ status = "Pi 会将推理级别映射到显示的级别";
152
+ else if (row.includes("role default"))
153
+ status = "使用角色默认模型";
154
+ else if (seatCount)
155
+ status = `当前 ${seatCount[1]} 个席位,需要 ${seatCount[2]} 个席位`;
156
+ else if (row.includes("repeated model seats"))
157
+ status = "重复模型不会增加模型多样性";
158
+ else if (row.includes("parent aliases do not judge"))
159
+ status = "父模型别名不担任评委";
160
+ }
161
+ return [description, status].filter(Boolean).join(";") || undefined;
162
+ }
163
+ function notifyBilingual(ctx, message, type) {
164
+ let chinese;
165
+ if (message.startsWith("Invalid selection"))
166
+ chinese = "选择无效,请重试。";
167
+ else if (message.startsWith("Setup cancelled"))
168
+ chinese = "配置已取消,未写入文件。";
169
+ else if (message.startsWith("/setup-pstack requires"))
170
+ chinese = "/setup-pstack 需要交互界面。";
171
+ else if (message.startsWith("No available Pi models"))
172
+ chinese = "未检测到可用的 Pi 模型。仍可选择父模型别名;配置 Pi 模型后可重新运行。";
173
+ else if (message.startsWith("A panel can have"))
174
+ chinese = "一个面板最多有 16 个席位。";
175
+ else if (message.startsWith("Cannot save yet")) {
176
+ const count = /Cannot save yet: (.+) needs (\d+–16) seats/.exec(message);
177
+ const details = count ? `${chineseName(count[1])} 需要 ${count[2]} 个席位` : reviewRowChinese(message);
178
+ chinese = `暂时无法保存。${details ?? "请修改相关选项或推理预算"}。`;
179
+ }
180
+ else if (message.startsWith("models.json changed"))
181
+ chinese = "models.json 在配置期间发生变化。未写入文件,请重新运行配置。";
182
+ else if (message.startsWith("Could not save"))
183
+ chinese = "无法保存 pstack 设置。";
184
+ else if (message.startsWith("Saved pstack settings"))
185
+ chinese = `已保存 pstack 设置。${message.includes("Repeated seats") ? "重复模型不会增加模型多样性。" : ""}${message.includes("Checking optional") ? "接下来检查可选的项目验证。" : "请选择父模型,再按需使用验证技能。"}`;
186
+ else if (message.startsWith("Cannot read"))
187
+ chinese = "无法读取配置文件。请先修复无效的 JSON。";
188
+ else if (message.startsWith("Cannot safely edit"))
189
+ chinese = "无法安全编辑配置文件。请先修复无效的模型配置。";
190
+ ctx.ui.notify(chinese ? `${message}\n${chinese}` : message, type);
191
+ }
42
192
  // Pi's standard selector renders every option, without scrolling. Page long lists
43
193
  // so they remain usable for users with a large model registry and in RPC mode.
44
194
  async function pick(ctx, title, items, backLabel) {
@@ -53,7 +203,7 @@ async function pick(ctx, title, items, backLabel) {
53
203
  if (page > 0)
54
204
  options.push("← Previous page");
55
205
  options.push(backLabel);
56
- const selected = await ctx.ui.select(`${title}${pages > 1 ? ` (${page + 1}/${pages})` : ""}`, options);
206
+ const selected = await selectBilingual(ctx, `${title}${pages > 1 ? ` (${page + 1}/${pages})` : ""}`, options);
57
207
  if (selected === undefined)
58
208
  return { kind: "cancel" };
59
209
  if (selected === "Next page →") {
@@ -69,7 +219,7 @@ async function pick(ctx, title, items, backLabel) {
69
219
  const item = visible.find(candidate => candidate.label === selected);
70
220
  if (item)
71
221
  return { kind: "choice", value: item.value };
72
- ctx.ui.notify("Invalid selection; try again.", "error");
222
+ notifyBilingual(ctx, "Invalid selection; try again.", "error");
73
223
  }
74
224
  }
75
225
  function snapshot() {
@@ -204,7 +354,7 @@ async function editPanel(ctx, config, name, available) {
204
354
  }
205
355
  if (chosen.value === "add") {
206
356
  if (seats.length >= 16) {
207
- ctx.ui.notify("A panel can have at most 16 seats.", "error");
357
+ notifyBilingual(ctx, "A panel can have at most 16 seats.", "error");
208
358
  continue;
209
359
  }
210
360
  const model = await chooseModel(ctx, available, `Add seat to ${info.label}`);
@@ -215,7 +365,7 @@ async function editPanel(ctx, config, name, available) {
215
365
  continue;
216
366
  }
217
367
  const index = Number(chosen.value);
218
- const action = await ctx.ui.select(`Seat ${index + 1}: ${seats[index]}`, ["Change model", "Remove seat", "Move up", "Move down", "Back to panel"]);
368
+ const action = await selectBilingual(ctx, `Seat ${index + 1}: ${seats[index]}`, ["Change model", "Remove seat", "Move up", "Move down", "Back to panel"]);
219
369
  if (action === undefined)
220
370
  return false;
221
371
  if (action === "Remove seat")
@@ -239,10 +389,11 @@ const VERIFICATION_FOLLOWUP = [
239
389
  "If neither exists, offer once to create a project-local verification skill using /skill:create-verification-skill; ask and wait for the user's answer before creating anything.",
240
390
  "Separately check for a project-local skill that runs a model checker, proof, or other formal verifier against the code. If none exists, offer once to create one using /skill:create-formal-verification-skill; ask and wait for the user's answer.",
241
391
  "Do not count the bundled create-* instructions as existing project verification. Respect a no or cancellation, and do not claim a skill ran unless it was actually invoked. If a requested skill is unavailable, say so.",
392
+ "When speaking to the user, provide both Chinese and English.",
242
393
  ].join(" ");
243
394
  export async function runSetup(pi, ctx) {
244
395
  if (!ctx.hasUI) {
245
- ctx.ui.notify("/setup-pstack requires an interactive UI.", "error");
396
+ notifyBilingual(ctx, "/setup-pstack requires an interactive UI.", "error");
246
397
  return;
247
398
  }
248
399
  let original, config, raw;
@@ -252,12 +403,12 @@ export async function runSetup(pi, ctx) {
252
403
  config = loadModels();
253
404
  }
254
405
  catch (error) {
255
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
406
+ notifyBilingual(ctx, error instanceof Error ? error.message : String(error), "error");
256
407
  return;
257
408
  }
258
409
  let available = availableModelSelectors(ctx);
259
410
  if (!available.length)
260
- ctx.ui.notify("No available Pi models detected. Parent aliases are still selectable; configure Pi models and rerun setup to choose explicit models.", "warning");
411
+ notifyBilingual(ctx, "No available Pi models detected. Parent aliases are still selectable; configure Pi models and rerun setup to choose explicit models.", "warning");
261
412
  const next = {
262
413
  ...raw, ...config, roles: { ...config.roles },
263
414
  panels: Object.fromEntries(Object.entries(config.panels ?? {}).map(([name, seats]) => [name, [...seats]])),
@@ -266,9 +417,9 @@ export async function runSetup(pi, ctx) {
266
417
  for (const [name, seats] of Object.entries(suggestedPanels(ctx)))
267
418
  next.panels[name] ??= seats;
268
419
  if (original === undefined && available.length) {
269
- const code = available.find(model => /(?:grok|composer)/i.test(model)) ?? available[0];
420
+ const code = available.find(model => modelFamily(model) === "grok") ?? available[0];
270
421
  const parent = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "";
271
- const judgment = available.find(model => /claude/i.test(model)) ?? (available.includes(parent) ? parent : available[0]);
422
+ const judgment = available.find(model => modelFamily(model) === "claude") ?? (available.includes(parent) ? parent : available[0]);
272
423
  next.roles.worker = code;
273
424
  for (const role of ["architect", "reviewer", "verifier", "synthesizer", "integrator", "poteto-agent", "comment-sicko"])
274
425
  next.roles[role] = judgment;
@@ -277,7 +428,7 @@ export async function runSetup(pi, ctx) {
277
428
  }
278
429
  const first = await chooseBudget(ctx, original === undefined ? "xhigh" : next.thinkingLevel, original === undefined, true);
279
430
  if (first.kind !== "choice") {
280
- ctx.ui.notify("Setup cancelled; nothing was written.", "info");
431
+ notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info");
281
432
  return;
282
433
  }
283
434
  next.thinkingLevel = first.value;
@@ -294,9 +445,9 @@ export async function runSetup(pi, ctx) {
294
445
  actions.push("Next page →");
295
446
  if (page > 0)
296
447
  actions.push("← Previous page");
297
- const action = await ctx.ui.select(title, actions);
448
+ const action = await selectBilingual(ctx, title, actions);
298
449
  if (action === undefined) {
299
- ctx.ui.notify("Setup cancelled; nothing was written.", "info");
450
+ notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info");
300
451
  return;
301
452
  }
302
453
  if (action === "Next page →") {
@@ -310,7 +461,7 @@ export async function runSetup(pi, ctx) {
310
461
  if (action === "Change budget") {
311
462
  const selected = await chooseBudget(ctx, next.thinkingLevel, false);
312
463
  if (selected.kind === "cancel") {
313
- ctx.ui.notify("Setup cancelled; nothing was written.", "info");
464
+ notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info");
314
465
  return;
315
466
  }
316
467
  if (selected.kind === "choice")
@@ -325,7 +476,7 @@ export async function runSetup(pi, ctx) {
325
476
  policies.sort((a, b) => Number(b.value === next.fallbackPolicy) - Number(a.value === next.fallbackPolicy));
326
477
  const selected = await pick(ctx, "Model fallback policy", policies, "Back to review");
327
478
  if (selected.kind === "cancel") {
328
- ctx.ui.notify("Setup cancelled; nothing was written.", "info");
479
+ notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info");
329
480
  return;
330
481
  }
331
482
  if (selected.kind === "choice")
@@ -335,13 +486,13 @@ export async function runSetup(pi, ctx) {
335
486
  if (action === "Edit a role") {
336
487
  const selected = await pick(ctx, "Choose role to edit", roleKeys(next).map(value => ({ value, label: `${value} — ${next.roles[value] ?? "inherit-parent"}` })), "Back to review");
337
488
  if (selected.kind === "cancel") {
338
- ctx.ui.notify("Setup cancelled; nothing was written.", "info");
489
+ notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info");
339
490
  return;
340
491
  }
341
492
  if (selected.kind === "choice") {
342
493
  const model = await chooseModel(ctx, available, `Model for ${selected.value}`, next.roles[selected.value]);
343
494
  if (model.kind === "cancel") {
344
- ctx.ui.notify("Setup cancelled; nothing was written.", "info");
495
+ notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info");
345
496
  return;
346
497
  }
347
498
  if (model.kind === "choice" && model.value)
@@ -352,14 +503,14 @@ export async function runSetup(pi, ctx) {
352
503
  if (action === "Edit a task model") {
353
504
  const selected = await pick(ctx, "Choose task model to edit", taskRoles(next).map(value => ({ value, label: `${value.label} — ${next.workflows[value.name]?.[value.role] ?? "role default"}` })), "Back to review");
354
505
  if (selected.kind === "cancel") {
355
- ctx.ui.notify("Setup cancelled; nothing was written.", "info");
506
+ notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info");
356
507
  return;
357
508
  }
358
509
  if (selected.kind === "choice") {
359
510
  const task = selected.value;
360
511
  const model = await chooseModel(ctx, available, `Model for ${task.label}`, next.workflows[task.name]?.[task.role], true);
361
512
  if (model.kind === "cancel") {
362
- ctx.ui.notify("Setup cancelled; nothing was written.", "info");
513
+ notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info");
363
514
  return;
364
515
  }
365
516
  if (model.kind === "choice") {
@@ -379,17 +530,17 @@ export async function runSetup(pi, ctx) {
379
530
  if (action === "Edit a panel") {
380
531
  const selected = await pick(ctx, "Choose panel to edit", panelKeys(next).map(value => ({ value, label: `${PANEL_INFO[value]?.label ?? value} — ${(next.panels[value] ?? []).length} seats` })), "Back to review");
381
532
  if (selected.kind === "cancel") {
382
- ctx.ui.notify("Setup cancelled; nothing was written.", "info");
533
+ notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info");
383
534
  return;
384
535
  }
385
536
  if (selected.kind === "choice" && !await editPanel(ctx, next, selected.value, available)) {
386
- ctx.ui.notify("Setup cancelled; nothing was written.", "info");
537
+ notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info");
387
538
  return;
388
539
  }
389
540
  continue;
390
541
  }
391
542
  if (action !== "Save settings") {
392
- ctx.ui.notify("Invalid selection; try again.", "error");
543
+ notifyBilingual(ctx, "Invalid selection; try again.", "error");
393
544
  continue;
394
545
  }
395
546
  // Registry/auth state may change while a dialog is open; recheck just before saving.
@@ -397,25 +548,25 @@ export async function runSetup(pi, ctx) {
397
548
  const finalInvalid = finalRows.filter(row => row.error);
398
549
  if (finalInvalid.length) {
399
550
  page = Math.floor(finalRows.findIndex(row => row.error) / size);
400
- ctx.ui.notify(`Cannot save yet: ${finalInvalid[0].error}. Edit the affected choice or change the budget.`, "error");
551
+ notifyBilingual(ctx, `Cannot save yet: ${finalInvalid[0].error}. Edit the affected choice or change the budget.`, "error");
401
552
  continue;
402
553
  }
403
554
  try {
404
555
  if (snapshot() !== original) {
405
- ctx.ui.notify("models.json changed during setup; nothing was written. Rerun setup to review the current settings.", "error");
556
+ notifyBilingual(ctx, "models.json changed during setup; nothing was written. Rerun setup to review the current settings.", "error");
406
557
  return;
407
558
  }
408
559
  saveModels(next);
409
560
  }
410
561
  catch (error) {
411
- ctx.ui.notify(`Could not save pstack settings: ${error instanceof Error ? error.message : String(error)}`, "error");
562
+ notifyBilingual(ctx, `Could not save pstack settings: ${error instanceof Error ? error.message : String(error)}`, "error");
412
563
  return;
413
564
  }
414
565
  const duplicates = panelKeys(next).filter(name => name !== "arena-judges" && (() => {
415
566
  const values = (next.panels[name] ?? []).map(value => value === "auto" || value === "inherit-parent" ? ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "parent" : value);
416
567
  return new Set(values).size < values.length;
417
568
  })());
418
- ctx.ui.notify(`Saved pstack settings to ${modelConfigPath()}.${duplicates.length ? ` Repeated seats do not add model diversity: ${duplicates.join(", ")}.` : ""}${ctx.model ? " Checking optional project verification next." : " Select a parent model and use /skill:create-verification-skill or /skill:create-formal-verification-skill if needed."}`, "info");
569
+ notifyBilingual(ctx, `Saved pstack settings to ${modelConfigPath()}.${duplicates.length ? ` Repeated seats do not add model diversity: ${duplicates.join(", ")}.` : ""}${ctx.model ? " Checking optional project verification next." : " Select a parent model and use /skill:create-verification-skill or /skill:create-formal-verification-skill if needed."}`, "info");
419
570
  if (ctx.model)
420
571
  pi.sendUserMessage(VERIFICATION_FOLLOWUP, ctx.isIdle() ? { expandPromptTemplates: true } : { expandPromptTemplates: true, deliverAs: "followUp" });
421
572
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-pstack-native",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "pi-pstack-native: pstack semantics rebuilt on Pi's official AgentSession APIs with native durable workflows, worktree isolation, model routing, and scheduling.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -79,9 +79,6 @@ export function selectJudgeFromPool(ctx: ExtensionContext, pool: string[]): { mo
79
79
  } catch (error) { notes.push(`Judge pool entry ${selector} unavailable: ${error instanceof Error ? error.message : String(error)}`); }
80
80
  };
81
81
  for (const selector of pool) resolve(selector);
82
- if (!choices.some(eligible) && typeof ctx.modelRegistry.getAvailable === 'function') {
83
- for (const selector of availableModelSelectors(ctx)) if (!pool.includes(selector)) resolve(selector);
84
- }
85
82
  const selected = choices.find(eligible);
86
83
  if (!parentFamily) notes.push('The parent model family is unknown; judge family independence cannot be established.');
87
84
  if (!selected) notes.push('No eligible cross-judge is available; the coordinator must compare and select the base.');
@@ -134,9 +131,13 @@ export function availableModelSelectors(ctx: ExtensionContext): string[] {
134
131
  export function suggestedPanels(ctx: ExtensionContext): Record<string, string[]> {
135
132
  const available = availableModelSelectors(ctx);
136
133
  const parent = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
137
- const first = parent && available.includes(parent) ? parent : available[0] ?? 'inherit-parent';
134
+ // Upstream starts its panels with a judgment model and a code model.
135
+ // Use only models Pi exposes, and prefer the upstream families when present.
136
+ const first = available.find(item => modelFamily(item) === 'claude')
137
+ ?? (parent && available.includes(parent) ? parent : available[0]) ?? 'inherit-parent';
138
138
  const family = modelFamily(first);
139
- const second = available.find(item => item !== first && family && modelFamily(item) && modelFamily(item) !== family)
139
+ const second = available.find(item => item !== first && modelFamily(item) === 'grok' && family !== 'grok')
140
+ ?? available.find(item => item !== first && family && modelFamily(item) && modelFamily(item) !== family)
140
141
  ?? available.find(item => item !== first) ?? first;
141
142
  const runners = [first, second];
142
143
  // A judge pool is ordered by preference, not a copy of the entire registry.
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { availableModelSelectors, lowerSupportedThinking, supportsThinking, suggestedPanels } from "../models/router.ts";
3
+ import { availableModelSelectors, lowerSupportedThinking, modelFamily, supportsThinking, suggestedPanels } from "../models/router.ts";
4
4
  import { loadModels, saveModels } from "../runtime/config.ts";
5
5
  import { modelConfigPath } from "../runtime/home.ts";
6
6
  import type { ModelConfig } from "../runtime/types.ts";
@@ -49,6 +49,128 @@ const BUDGETS: Item<ModelConfig["thinkingLevel"]>[] = [
49
49
  { value: "low", label: "low — lower reasoning effort" },
50
50
  ];
51
51
 
52
+ // Keep the English text stable for integrations while showing Chinese beside it.
53
+ const ZH_OPTIONS: Record<string, string> = {
54
+ "Next page →": "下一页 →", "← Previous page": "← 上一页",
55
+ "Cancel setup": "取消配置", "Back to review": "返回检查", "Back": "返回",
56
+ "Save settings": "保存设置", "Edit a role": "编辑通用角色", "Edit a task model": "编辑任务模型",
57
+ "Edit a panel": "编辑模型面板", "Change budget": "修改推理预算", "Change fallback": "修改回退策略",
58
+ "Done editing panel": "完成面板编辑", "Add a seat": "添加席位", "Change model": "更换模型",
59
+ "Remove seat": "删除席位", "Move up": "上移", "Move down": "下移", "Back to panel": "返回面板",
60
+ "Use role default": "使用角色默认模型",
61
+ "inherit-parent — parent model": "inherit-parent — 使用父会话模型", "auto — parent model": "auto — 使用父会话模型",
62
+ "unlimited — max reasoning": "不限 — max 推理", "large — xhigh reasoning": "大 — xhigh 推理",
63
+ "medium — high reasoning": "中 — high 推理", "small — medium reasoning": "小 — medium 推理",
64
+ "inherit-parent — use the parent session's reasoning level": "inherit-parent — 使用父会话的推理级别",
65
+ "off — disable reasoning": "off — 关闭推理", "minimal — least reasoning effort": "minimal — 最低推理强度",
66
+ "low — lower reasoning effort": "low — 较低推理强度",
67
+ "disabled — block unsupported reasoning": "disabled — 禁止不支持的推理级别",
68
+ "same-family — allow reported lower reasoning and model fallback": "same-family — 允许降低推理级别并回退模型",
69
+ };
70
+
71
+ const ZH_NAMES: Record<string, string> = {
72
+ worker: "执行开发", architect: "设计和开发", reviewer: "只读审查", scout: "只读探索",
73
+ verifier: "只读验证", synthesizer: "汇总结果", integrator: "合并工作",
74
+ "poteto-agent": "执行 Poteto 委派任务", "comment-sicko": "审查注释和变通方案",
75
+ feature: "功能开发", refactoring: "代码重构", "bug-fix": "修复错误", "performance issue": "性能问题",
76
+ hillclimb: "迭代改进", "how explorer": "探索实现方式", "how explainer": "解释实现方式",
77
+ "why investigators": "调查原因", "why synthesizer": "汇总原因", "swarm workers": "并行执行任务",
78
+ "reflect tooling": "反思:工具分析", "reflect judgment": "反思:判断", "reflect divergent": "反思:发散分析",
79
+ "reflect synthesizer": "反思:汇总", "arena runners": "arena 方案执行", "arena cross-judge pool": "arena 交叉评委池",
80
+ "architect runners": "architect 方案执行", "interrogate reviewers": "interrogate 审查",
81
+ };
82
+
83
+ function chineseName(name: string): string {
84
+ return ZH_NAMES[name] ? `${name}(${ZH_NAMES[name]})` : name;
85
+ }
86
+
87
+ function optionChinese(option: string): string | undefined {
88
+ if (ZH_OPTIONS[option]) return ZH_OPTIONS[option];
89
+ const seat = /^Seat (\d+): (.+)$/.exec(option);
90
+ if (seat) return `席位 ${seat[1]}:${seat[2]}`;
91
+ const count = /^(.+) — (\d+) seats$/.exec(option);
92
+ if (count) return `${chineseName(count[1]!)} — ${count[2]} 个席位`;
93
+ const role = /^(.+) — (.+)$/.exec(option);
94
+ if (role) return `${chineseName(role[1]!)} — 当前模型:${role[2] === "role default" ? "角色默认模型" : role[2]}`;
95
+ return undefined;
96
+ }
97
+
98
+ function titleChinese(title: string): string | undefined {
99
+ if (title.startsWith("Child reasoning budget")) return "子代理推理预算。large 对应原版默认值;current 表示当前值。";
100
+ if (title.startsWith("Review pstack settings")) return "检查 pstack 设置。need attention 表示需要处理。通用角色提供默认模型;任务模型覆盖对应任务的默认模型。";
101
+ if (title.startsWith("Choose role")) return "选择要编辑的通用角色。任务模型没有单独设置时,使用对应角色的模型。";
102
+ if (title.startsWith("Choose task model")) return "选择要编辑的任务模型。任务设置会覆盖对应角色的默认模型。";
103
+ if (title.startsWith("Choose panel")) return "选择要编辑的模型面板。执行和审查面板的每个席位运行一个子代理。arena 评委池只选一名评委。父模型家族已知时,评委必须来自不同家族;父模型别名不担任评委。";
104
+ if (title.startsWith("Model fallback policy")) return "模型回退策略";
105
+ if (title.startsWith("Add seat to ")) return "为面板添加席位。评委池按顺序选择一名评委。其他面板的每个席位运行一个子代理。";
106
+ if (title.startsWith("Model for ")) return `选择模型:${chineseName(title.slice("Model for ".length))}`;
107
+ if (title.startsWith("Seat ")) return "编辑席位";
108
+ if (/\(\d+ seats; \d+–16 allowed\)/.test(title)) return `编辑面板:${chineseName(title.slice(0, title.indexOf(" (")))}。评委池按顺序选择一名评委。其他面板的每个席位运行一个子代理。席位数量必须在显示的范围内。`;
109
+ return undefined;
110
+ }
111
+
112
+ async function selectBilingual(ctx: ExtensionContext, title: string, options: string[]): Promise<string | undefined> {
113
+ const chinese = titleChinese(title);
114
+ const lines = title.split("\n").map((line, index) => {
115
+ if (index === 0) return line;
116
+ const translation = reviewRowChinese(line);
117
+ return translation ? `${line} / ${translation}` : line;
118
+ });
119
+ const labels = options.map(option => {
120
+ const translation = optionChinese(option);
121
+ return translation ? `${option} / ${translation}` : option;
122
+ });
123
+ const selected = await ctx.ui.select(chinese ? `${lines.join("\n")}\n${chinese}` : lines.join("\n"), labels);
124
+ const index = labels.indexOf(selected ?? "");
125
+ // Accept the English value as well for existing UI integrations.
126
+ return index >= 0 ? options[index] : selected;
127
+ }
128
+
129
+ function reviewRowChinese(row: string): string | undefined {
130
+ if (row.startsWith("Budget:")) return "推理预算与回退策略";
131
+ const label = /^([^:]+):/.exec(row)?.[1];
132
+ const role = label?.startsWith("Role ") ? label.slice(5) : undefined;
133
+ const name = role ?? label?.replace(/ \d+$/, "");
134
+ const description = name && ZH_NAMES[name] ? `${role ? "通用角色:" : ""}${ZH_NAMES[name]}` : undefined;
135
+ let status: string | undefined;
136
+ if (row.includes("not available; choose a model")) status = "模型不可用,请重新选择模型";
137
+ else {
138
+ const unsupported = /does not support (\S+) reasoning/.exec(row);
139
+ const fallback = /uses (\S+) reasoning \(fallback\)/.exec(row);
140
+ const seatCount = /: (\d+) seats \[needs (\d+–16)\]/.exec(row);
141
+ if (unsupported) status = `模型不支持 ${unsupported[1]} 推理,请选择其他模型或调整推理预算`;
142
+ else if (row.includes("[needs choice:")) status = "需要重新选择模型或调整推理预算";
143
+ else if (row.includes("parent model not selected yet")) status = "尚未选择父模型";
144
+ else if (fallback) status = `回退到 ${fallback[1]} 推理级别`;
145
+ else if (row.includes("Pi maps ")) status = "Pi 会将推理级别映射到显示的级别";
146
+ else if (row.includes("role default")) status = "使用角色默认模型";
147
+ else if (seatCount) status = `当前 ${seatCount[1]} 个席位,需要 ${seatCount[2]} 个席位`;
148
+ else if (row.includes("repeated model seats")) status = "重复模型不会增加模型多样性";
149
+ else if (row.includes("parent aliases do not judge")) status = "父模型别名不担任评委";
150
+ }
151
+ return [description, status].filter(Boolean).join(";") || undefined;
152
+ }
153
+
154
+ function notifyBilingual(ctx: ExtensionContext, message: string, type: "info" | "warning" | "error"): void {
155
+ let chinese: string | undefined;
156
+ if (message.startsWith("Invalid selection")) chinese = "选择无效,请重试。";
157
+ else if (message.startsWith("Setup cancelled")) chinese = "配置已取消,未写入文件。";
158
+ else if (message.startsWith("/setup-pstack requires")) chinese = "/setup-pstack 需要交互界面。";
159
+ else if (message.startsWith("No available Pi models")) chinese = "未检测到可用的 Pi 模型。仍可选择父模型别名;配置 Pi 模型后可重新运行。";
160
+ else if (message.startsWith("A panel can have")) chinese = "一个面板最多有 16 个席位。";
161
+ else if (message.startsWith("Cannot save yet")) {
162
+ const count = /Cannot save yet: (.+) needs (\d+–16) seats/.exec(message);
163
+ const details = count ? `${chineseName(count[1]!)} 需要 ${count[2]} 个席位` : reviewRowChinese(message);
164
+ chinese = `暂时无法保存。${details ?? "请修改相关选项或推理预算"}。`;
165
+ }
166
+ else if (message.startsWith("models.json changed")) chinese = "models.json 在配置期间发生变化。未写入文件,请重新运行配置。";
167
+ else if (message.startsWith("Could not save")) chinese = "无法保存 pstack 设置。";
168
+ else if (message.startsWith("Saved pstack settings")) chinese = `已保存 pstack 设置。${message.includes("Repeated seats") ? "重复模型不会增加模型多样性。" : ""}${message.includes("Checking optional") ? "接下来检查可选的项目验证。" : "请选择父模型,再按需使用验证技能。"}`;
169
+ else if (message.startsWith("Cannot read")) chinese = "无法读取配置文件。请先修复无效的 JSON。";
170
+ else if (message.startsWith("Cannot safely edit")) chinese = "无法安全编辑配置文件。请先修复无效的模型配置。";
171
+ ctx.ui.notify(chinese ? `${message}\n${chinese}` : message, type);
172
+ }
173
+
52
174
  // Pi's standard selector renders every option, without scrolling. Page long lists
53
175
  // so they remain usable for users with a large model registry and in RPC mode.
54
176
  async function pick<T>(ctx: ExtensionContext, title: string, items: Item<T>[], backLabel: string): Promise<Pick<T>> {
@@ -61,14 +183,14 @@ async function pick<T>(ctx: ExtensionContext, title: string, items: Item<T>[], b
61
183
  if (page + 1 < pages) options.push("Next page →");
62
184
  if (page > 0) options.push("← Previous page");
63
185
  options.push(backLabel);
64
- const selected = await ctx.ui.select(`${title}${pages > 1 ? ` (${page + 1}/${pages})` : ""}`, options);
186
+ const selected = await selectBilingual(ctx, `${title}${pages > 1 ? ` (${page + 1}/${pages})` : ""}`, options);
65
187
  if (selected === undefined) return { kind: "cancel" };
66
188
  if (selected === "Next page →") { page++; continue; }
67
189
  if (selected === "← Previous page") { page--; continue; }
68
190
  if (selected === backLabel) return { kind: "back" };
69
191
  const item = visible.find(candidate => candidate.label === selected);
70
192
  if (item) return { kind: "choice", value: item.value };
71
- ctx.ui.notify("Invalid selection; try again.", "error");
193
+ notifyBilingual(ctx, "Invalid selection; try again.", "error");
72
194
  }
73
195
  }
74
196
 
@@ -191,14 +313,14 @@ async function editPanel(ctx: ExtensionContext, config: Draft, name: string, ava
191
313
  if (chosen.kind === "back") return true;
192
314
  if (chosen.value === "done") { config.panels[name] = seats; return true; }
193
315
  if (chosen.value === "add") {
194
- if (seats.length >= 16) { ctx.ui.notify("A panel can have at most 16 seats.", "error"); continue; }
316
+ if (seats.length >= 16) { notifyBilingual(ctx, "A panel can have at most 16 seats.", "error"); continue; }
195
317
  const model = await chooseModel(ctx, available, `Add seat to ${info.label}`);
196
318
  if (model.kind === "cancel") return false;
197
319
  if (model.kind === "choice" && model.value) seats.push(model.value);
198
320
  continue;
199
321
  }
200
322
  const index = Number(chosen.value);
201
- const action = await ctx.ui.select(`Seat ${index + 1}: ${seats[index]}`, ["Change model", "Remove seat", "Move up", "Move down", "Back to panel"]);
323
+ const action = await selectBilingual(ctx, `Seat ${index + 1}: ${seats[index]}`, ["Change model", "Remove seat", "Move up", "Move down", "Back to panel"]);
202
324
  if (action === undefined) return false;
203
325
  if (action === "Remove seat") seats.splice(index, 1);
204
326
  else if (action === "Move up" && index > 0) [seats[index - 1], seats[index]] = [seats[index]!, seats[index - 1]!];
@@ -217,15 +339,16 @@ const VERIFICATION_FOLLOWUP = [
217
339
  "If neither exists, offer once to create a project-local verification skill using /skill:create-verification-skill; ask and wait for the user's answer before creating anything.",
218
340
  "Separately check for a project-local skill that runs a model checker, proof, or other formal verifier against the code. If none exists, offer once to create one using /skill:create-formal-verification-skill; ask and wait for the user's answer.",
219
341
  "Do not count the bundled create-* instructions as existing project verification. Respect a no or cancellation, and do not claim a skill ran unless it was actually invoked. If a requested skill is unavailable, say so.",
342
+ "When speaking to the user, provide both Chinese and English.",
220
343
  ].join(" ");
221
344
 
222
345
  export async function runSetup(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
223
- if (!ctx.hasUI) { ctx.ui.notify("/setup-pstack requires an interactive UI.", "error"); return; }
346
+ if (!ctx.hasUI) { notifyBilingual(ctx, "/setup-pstack requires an interactive UI.", "error"); return; }
224
347
  let original: string | undefined, config: ModelConfig, raw: Record<string, unknown>;
225
348
  try { original = snapshot(); raw = parseExisting(original); config = loadModels(); }
226
- catch (error) { ctx.ui.notify(error instanceof Error ? error.message : String(error), "error"); return; }
349
+ catch (error) { notifyBilingual(ctx, error instanceof Error ? error.message : String(error), "error"); return; }
227
350
  let available = availableModelSelectors(ctx);
228
- if (!available.length) ctx.ui.notify("No available Pi models detected. Parent aliases are still selectable; configure Pi models and rerun setup to choose explicit models.", "warning");
351
+ if (!available.length) notifyBilingual(ctx, "No available Pi models detected. Parent aliases are still selectable; configure Pi models and rerun setup to choose explicit models.", "warning");
229
352
  const next: Draft = {
230
353
  ...raw, ...config, roles: { ...config.roles },
231
354
  panels: Object.fromEntries(Object.entries(config.panels ?? {}).map(([name, seats]) => [name, [...seats]])),
@@ -233,16 +356,16 @@ export async function runSetup(pi: ExtensionAPI, ctx: ExtensionContext): Promise
233
356
  };
234
357
  for (const [name, seats] of Object.entries(suggestedPanels(ctx))) next.panels[name] ??= seats;
235
358
  if (original === undefined && available.length) {
236
- const code = available.find(model => /(?:grok|composer)/i.test(model)) ?? available[0]!;
359
+ const code = available.find(model => modelFamily(model) === "grok") ?? available[0]!;
237
360
  const parent = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "";
238
- const judgment = available.find(model => /claude/i.test(model)) ?? (available.includes(parent) ? parent : available[0]!);
361
+ const judgment = available.find(model => modelFamily(model) === "claude") ?? (available.includes(parent) ? parent : available[0]!);
239
362
  next.roles.worker = code;
240
363
  for (const role of ["architect", "reviewer", "verifier", "synthesizer", "integrator", "poteto-agent", "comment-sicko"]) next.roles[role] = judgment;
241
364
  next.roles.scout = code;
242
365
  next.workflows.reflect = { tooling: code, judgment, divergent: judgment, synthesizer: judgment };
243
366
  }
244
367
  const first = await chooseBudget(ctx, original === undefined ? "xhigh" : next.thinkingLevel, original === undefined, true);
245
- if (first.kind !== "choice") { ctx.ui.notify("Setup cancelled; nothing was written.", "info"); return; }
368
+ if (first.kind !== "choice") { notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info"); return; }
246
369
  next.thinkingLevel = first.value;
247
370
  let page = 0;
248
371
  for (;;) {
@@ -255,13 +378,13 @@ export async function runSetup(pi: ExtensionAPI, ctx: ExtensionContext): Promise
255
378
  const actions = ["Save settings", "Edit a role", "Edit a task model", "Edit a panel", "Change budget", "Change fallback"];
256
379
  if (page + 1 < pages) actions.push("Next page →");
257
380
  if (page > 0) actions.push("← Previous page");
258
- const action = await ctx.ui.select(title, actions);
259
- if (action === undefined) { ctx.ui.notify("Setup cancelled; nothing was written.", "info"); return; }
381
+ const action = await selectBilingual(ctx, title, actions);
382
+ if (action === undefined) { notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info"); return; }
260
383
  if (action === "Next page →") { page++; continue; }
261
384
  if (action === "← Previous page") { page--; continue; }
262
385
  if (action === "Change budget") {
263
386
  const selected = await chooseBudget(ctx, next.thinkingLevel, false);
264
- if (selected.kind === "cancel") { ctx.ui.notify("Setup cancelled; nothing was written.", "info"); return; }
387
+ if (selected.kind === "cancel") { notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info"); return; }
265
388
  if (selected.kind === "choice") next.thinkingLevel = selected.value;
266
389
  continue;
267
390
  }
@@ -272,27 +395,27 @@ export async function runSetup(pi: ExtensionAPI, ctx: ExtensionContext): Promise
272
395
  ];
273
396
  policies.sort((a, b) => Number(b.value === next.fallbackPolicy) - Number(a.value === next.fallbackPolicy));
274
397
  const selected = await pick(ctx, "Model fallback policy", policies, "Back to review");
275
- if (selected.kind === "cancel") { ctx.ui.notify("Setup cancelled; nothing was written.", "info"); return; }
398
+ if (selected.kind === "cancel") { notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info"); return; }
276
399
  if (selected.kind === "choice") next.fallbackPolicy = selected.value;
277
400
  continue;
278
401
  }
279
402
  if (action === "Edit a role") {
280
403
  const selected = await pick(ctx, "Choose role to edit", roleKeys(next).map(value => ({ value, label: `${value} — ${next.roles[value] ?? "inherit-parent"}` })), "Back to review");
281
- if (selected.kind === "cancel") { ctx.ui.notify("Setup cancelled; nothing was written.", "info"); return; }
404
+ if (selected.kind === "cancel") { notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info"); return; }
282
405
  if (selected.kind === "choice") {
283
406
  const model = await chooseModel(ctx, available, `Model for ${selected.value}`, next.roles[selected.value]);
284
- if (model.kind === "cancel") { ctx.ui.notify("Setup cancelled; nothing was written.", "info"); return; }
407
+ if (model.kind === "cancel") { notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info"); return; }
285
408
  if (model.kind === "choice" && model.value) next.roles[selected.value] = model.value;
286
409
  }
287
410
  continue;
288
411
  }
289
412
  if (action === "Edit a task model") {
290
413
  const selected = await pick(ctx, "Choose task model to edit", taskRoles(next).map(value => ({ value, label: `${value.label} — ${next.workflows[value.name]?.[value.role] ?? "role default"}` })), "Back to review");
291
- if (selected.kind === "cancel") { ctx.ui.notify("Setup cancelled; nothing was written.", "info"); return; }
414
+ if (selected.kind === "cancel") { notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info"); return; }
292
415
  if (selected.kind === "choice") {
293
416
  const task = selected.value;
294
417
  const model = await chooseModel(ctx, available, `Model for ${task.label}`, next.workflows[task.name]?.[task.role], true);
295
- if (model.kind === "cancel") { ctx.ui.notify("Setup cancelled; nothing was written.", "info"); return; }
418
+ if (model.kind === "cancel") { notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info"); return; }
296
419
  if (model.kind === "choice") {
297
420
  const values = { ...next.workflows[task.name] };
298
421
  if (model.value) values[task.role] = model.value; else delete values[task.role];
@@ -303,28 +426,28 @@ export async function runSetup(pi: ExtensionAPI, ctx: ExtensionContext): Promise
303
426
  }
304
427
  if (action === "Edit a panel") {
305
428
  const selected = await pick(ctx, "Choose panel to edit", panelKeys(next).map(value => ({ value, label: `${PANEL_INFO[value]?.label ?? value} — ${(next.panels[value] ?? []).length} seats` })), "Back to review");
306
- if (selected.kind === "cancel") { ctx.ui.notify("Setup cancelled; nothing was written.", "info"); return; }
307
- if (selected.kind === "choice" && !await editPanel(ctx, next, selected.value, available)) { ctx.ui.notify("Setup cancelled; nothing was written.", "info"); return; }
429
+ if (selected.kind === "cancel") { notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info"); return; }
430
+ if (selected.kind === "choice" && !await editPanel(ctx, next, selected.value, available)) { notifyBilingual(ctx, "Setup cancelled; nothing was written.", "info"); return; }
308
431
  continue;
309
432
  }
310
- if (action !== "Save settings") { ctx.ui.notify("Invalid selection; try again.", "error"); continue; }
433
+ if (action !== "Save settings") { notifyBilingual(ctx, "Invalid selection; try again.", "error"); continue; }
311
434
  // Registry/auth state may change while a dialog is open; recheck just before saving.
312
435
  const finalRows = reviewRows(ctx, new Set(availableModelSelectors(ctx)), next);
313
436
  const finalInvalid = finalRows.filter(row => row.error);
314
437
  if (finalInvalid.length) {
315
438
  page = Math.floor(finalRows.findIndex(row => row.error) / size);
316
- ctx.ui.notify(`Cannot save yet: ${finalInvalid[0]!.error}. Edit the affected choice or change the budget.`, "error");
439
+ notifyBilingual(ctx, `Cannot save yet: ${finalInvalid[0]!.error}. Edit the affected choice or change the budget.`, "error");
317
440
  continue;
318
441
  }
319
442
  try {
320
- if (snapshot() !== original) { ctx.ui.notify("models.json changed during setup; nothing was written. Rerun setup to review the current settings.", "error"); return; }
443
+ if (snapshot() !== original) { notifyBilingual(ctx, "models.json changed during setup; nothing was written. Rerun setup to review the current settings.", "error"); return; }
321
444
  saveModels(next);
322
- } catch (error) { ctx.ui.notify(`Could not save pstack settings: ${error instanceof Error ? error.message : String(error)}`, "error"); return; }
445
+ } catch (error) { notifyBilingual(ctx, `Could not save pstack settings: ${error instanceof Error ? error.message : String(error)}`, "error"); return; }
323
446
  const duplicates = panelKeys(next).filter(name => name !== "arena-judges" && (() => {
324
447
  const values = (next.panels[name] ?? []).map(value => value === "auto" || value === "inherit-parent" ? ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "parent" : value);
325
448
  return new Set(values).size < values.length;
326
449
  })());
327
- ctx.ui.notify(`Saved pstack settings to ${modelConfigPath()}.${duplicates.length ? ` Repeated seats do not add model diversity: ${duplicates.join(", ")}.` : ""}${ctx.model ? " Checking optional project verification next." : " Select a parent model and use /skill:create-verification-skill or /skill:create-formal-verification-skill if needed."}`, "info");
450
+ notifyBilingual(ctx, `Saved pstack settings to ${modelConfigPath()}.${duplicates.length ? ` Repeated seats do not add model diversity: ${duplicates.join(", ")}.` : ""}${ctx.model ? " Checking optional project verification next." : " Select a parent model and use /skill:create-verification-skill or /skill:create-formal-verification-skill if needed."}`, "info");
328
451
  if (ctx.model) pi.sendUserMessage(VERIFICATION_FOLLOWUP, ctx.isIdle() ? { expandPromptTemplates: true } : { expandPromptTemplates: true, deliverAs: "followUp" });
329
452
  return;
330
453
  }
@@ -13,10 +13,36 @@ import { buildTemplate } from '../src/templates/build.ts';
13
13
  import { planArtifacts } from '../src/workflow/artifacts.ts';
14
14
  import { registerWorkflowTool } from '../src/tools/workflow.ts';
15
15
  import { registerAgentTool } from '../src/tools/agents.ts';
16
+ import { selectJudgeFromPool, suggestedPanels } from '../src/models/router.ts';
16
17
  import { WorkflowEngine } from '../src/workflow/engine.ts';
17
18
  import { verify } from '../src/verification/runner.ts';
18
19
  import type { TaskBrief } from '../src/contracts/types.ts';
19
20
 
21
+ test('suggested panels prefer upstream model families over an unrelated parent', () => {
22
+ const models = [
23
+ { provider: 'openai', id: 'gpt-5' },
24
+ { provider: 'anthropic', id: 'claude-opus-4' },
25
+ { provider: 'xai', id: 'grok-4' },
26
+ ];
27
+ const ctx: any = { model: models[0], scopedModels: [], modelRegistry: { getAvailable: () => models } };
28
+ const panels = suggestedPanels(ctx);
29
+ assert.deepEqual(panels.arena, ['anthropic/claude-opus-4', 'xai/grok-4']);
30
+ assert.deepEqual(panels['arena-judges'], panels.arena);
31
+ assert.deepEqual(panels.architect, panels.arena);
32
+ assert.deepEqual(panels.interrogate, panels.arena);
33
+ });
34
+
35
+ test('judge selection respects the configured pool even when other families are available', () => {
36
+ const parent = { provider: 'p', id: 'claude-opus', reasoning: true };
37
+ const other = { provider: 'p', id: 'grok-4', reasoning: true };
38
+ const ctx: any = { model: parent, scopedModels: [], modelRegistry: {
39
+ getAvailable: () => [parent, other], find: (_provider: string, id: string) => [parent, other].find(model => model.id === id),
40
+ } };
41
+ const result = selectJudgeFromPool(ctx, ['inherit-parent', 'p/claude-opus']);
42
+ assert.equal(result.model, undefined);
43
+ assert(result.notes.some(note => note.includes('coordinator must compare')));
44
+ });
45
+
20
46
  test('configured review panels resolve models, plan, and dispatch one reviewer per seat', async () => {
21
47
  const root = mkdtempSync(join(tmpdir(), 'pstack-panels-'));
22
48
  const old = process.env.PSTACK_NATIVE_HOME;
@@ -112,12 +112,11 @@ test("judge pool prefers a recognized family different from the parent and repor
112
112
  assert(selected.notes.some(note => note.includes('not-registered') && note.includes('unavailable')));
113
113
  }));
114
114
 
115
- test('judge pool skips the known parent family and falls back to another available family', () => {
115
+ test('judge pool skips the parent family and uses only configured entries', () => {
116
116
  const parent: any = { provider: 'p', id: 'claude-opus', reasoning: true };
117
117
  const other: any = { provider: 'p', id: 'grok-4', reasoning: true };
118
118
  const ctx: any = { model: parent, thinkingLevel: 'off', scopedModels: [], modelRegistry: { find: (provider: string, id: string) => [parent, other].find(m => m.provider === provider && m.id === id), getAvailable: () => [parent, other] } };
119
- assert.equal(selectJudgeFromPool(ctx, ['inherit-parent', 'p/claude-opus']).model, 'p/grok-4');
120
- ctx.modelRegistry.getAvailable = () => [parent];
119
+ assert.equal(selectJudgeFromPool(ctx, ['inherit-parent', 'p/claude-opus', 'p/grok-4']).model, 'p/grok-4');
121
120
  const skipped = selectJudgeFromPool(ctx, ['auto', 'p/claude-opus']);
122
121
  assert.equal(skipped.model, undefined);
123
122
  assert(skipped.notes.some(note => note.includes('coordinator')));
@@ -18,7 +18,14 @@ async function setup(choose: Choose, models: any[] = [claude, grok], parent: any
18
18
  const ctx = {
19
19
  hasUI: true, model: parent, scopedModels, isIdle: () => true,
20
20
  modelRegistry: { getAvailable: () => models, find: (provider: string, id: string) => registryModels.find(m => m.provider === provider && m.id === id) },
21
- ui: { select: (title: string, options: string[]) => { titles.push(title); optionLists.push(options); assert(titles.length < 100, 'wizard must make progress'); return choose(title, options, titles.length); }, notify: (message: string) => notices.push(message) },
21
+ ui: { select: (title: string, options: string[]) => {
22
+ titles.push(title); optionLists.push(options); assert(titles.length < 100, 'wizard must make progress');
23
+ // Drive the real displayed option while keeping the existing English-based scenarios readable.
24
+ const english = options.map(option => option.replace(/ \/ [\u4e00-\u9fff].*$/, ''));
25
+ const selected = choose(title, english, titles.length);
26
+ const index = english.indexOf(selected ?? '');
27
+ return index < 0 ? selected : options[index];
28
+ }, notify: (message: string) => notices.push(message) },
22
29
  };
23
30
  await commands.get('setup-pstack').handler('', ctx);
24
31
  return { notices, titles, optionLists, messages };
@@ -39,6 +46,9 @@ test('fresh setup defaults to the upstream large budget, previews all mappings i
39
46
  for (const text of ['Role worker: xai/grok-4', 'feature: role default (xai/grok-4)', 'how explainer:', 'swarm workers:', 'arena runners 1:', 'arena cross-judge pool 1:', 'architect runners 2:', 'interrogate reviewers 2:']) assert(all.includes(text), text);
40
47
  assert(all.includes('Pi maps xhigh to high'));
41
48
  assert(optionLists.every(options => options.length <= 8));
49
+ assert(titles.some(title => /[\u4e00-\u9fff]/.test(title)), 'setup should show Chinese in its prompts');
50
+ assert(optionLists.flat().some(option => option.includes('Save settings') && option.includes('保存设置')));
51
+ assert(optionLists.flat().some(option => option.includes('Cancel setup') && option.includes('取消配置')));
42
52
  const config = loadModels();
43
53
  assert.equal(config.thinkingLevel, 'xhigh');
44
54
  assert.equal(config.roles.worker, grokId);
@@ -49,6 +59,7 @@ test('fresh setup defaults to the upstream large budget, previews all mappings i
49
59
  assert.match(messages[0]!.text, /existing harness.*not merely unit tests/);
50
60
  assert.match(messages[0]!.text, /ask and wait for the user's answer/);
51
61
  assert.match(messages[0]!.text, /create-formal-verification-skill/);
62
+ assert.match(messages[0]!.text, /provide both Chinese and English/);
52
63
  assert.equal(messages[0]!.options.expandPromptTemplates, true);
53
64
  }));
54
65
 
@@ -106,7 +117,7 @@ test('unsupported reasoning can be corrected without discarding model choices',
106
117
  test('edit one panel seat and preserve the rest and the feature/refactoring overrides', async () => withHome(async () => {
107
118
  saveModels({ version: 1, thinkingLevel: 'inherit-parent', roles: {}, workflows: { feature: { worker: claudeId }, refactoring: { worker: grokId } }, panels: { arena: [claudeId, grokId], extra: [grokId] } });
108
119
  let step = 0;
109
- await setup((title, options) => {
120
+ const result = await setup((title, options) => {
110
121
  if (title.startsWith('Child reasoning')) return options[0];
111
122
  if (title.startsWith('Review pstack')) return step++ === 0 ? 'Edit a panel' : 'Save settings';
112
123
  if (title.startsWith('Choose panel')) return options.find(x => x.startsWith('arena runners'));
@@ -120,6 +131,33 @@ test('edit one panel seat and preserve the rest and the feature/refactoring over
120
131
  assert.deepEqual(config.panels?.extra, [grokId]);
121
132
  assert.equal(config.workflows?.feature.worker, claudeId);
122
133
  assert.equal(config.workflows?.refactoring.worker, grokId);
134
+ assert(result.optionLists.flat().some(option => option.includes('Change model') && /[\u4e00-\u9fff]/.test(option)));
135
+ }));
136
+
137
+ test('a panel below its minimum cannot be saved and can be repaired without restarting setup', async () => withHome(async () => {
138
+ saveModels({ version: 1, thinkingLevel: 'inherit-parent', roles: {}, panels: { arena: [claudeId, grokId] } });
139
+ let review = 0, panelEdit = 0;
140
+ const result = await setup((title, options) => {
141
+ if (title.startsWith('Child reasoning')) return options[0];
142
+ if (title.startsWith('Review pstack')) {
143
+ review++;
144
+ return review === 2 ? 'Save settings' : review === 4 ? 'Save settings' : 'Edit a panel';
145
+ }
146
+ if (title.startsWith('Choose panel')) return options.find(option => option.startsWith('arena runners'));
147
+ if (title.startsWith('arena runners')) {
148
+ if (panelEdit === 0) { panelEdit++; return options.find(option => option.startsWith('Seat 2:')); }
149
+ if (panelEdit === 1) { panelEdit++; return 'Done editing panel'; }
150
+ if (panelEdit === 2) { panelEdit++; return 'Add a seat'; }
151
+ return 'Done editing panel';
152
+ }
153
+ if (title.startsWith('Seat 2:')) return 'Remove seat';
154
+ if (title.startsWith('Add seat to arena runners')) return options.find(option => option === grokId);
155
+ return undefined;
156
+ });
157
+ assert(result.notices.some(notice => /Cannot save yet.*arena runners needs 2–16 seats/.test(notice)));
158
+ assert(result.notices.some(notice => /arena 方案执行.*需要 2–16 个席位/.test(notice)));
159
+ assert.deepEqual(loadModels().panels?.arena, [claudeId, grokId]);
160
+ assert.equal(result.messages.length, 1);
123
161
  }));
124
162
 
125
163
  test('long model registries are paginated and the suggested judge pool is bounded', async () => withHome(async () => {
@@ -42,8 +42,10 @@ test('real Pi RPC runs setup and dispatches the optional verification follow-up'
42
42
  if (event.type === 'extension_ui_request' && event.method === 'select') {
43
43
  titles.push(event.title);
44
44
  const value = event.title.startsWith('Child reasoning') ? event.options.find((option: string) => option.startsWith('inherit-parent'))
45
- : event.title.startsWith('Review pstack') ? 'Save settings' : undefined;
45
+ : event.title.startsWith('Review pstack') ? event.options.find((option: string) => option.startsWith('Save settings')) : undefined;
46
46
  if (!value) { clearTimeout(timer); reject(new Error(`Unexpected setup dialog: ${event.title}`)); return; }
47
+ assert.match(event.title, /[\u4e00-\u9fff]/, 'setup prompts should include Chinese in RPC mode');
48
+ assert(event.options.some((option: string) => /[\u4e00-\u9fff]/.test(option)), 'setup options should include Chinese in RPC mode');
47
49
  send({ type: 'extension_ui_response', id: event.id, value });
48
50
  }
49
51
  if (event.type === 'extension_ui_request' && event.method === 'notify' && event.message.startsWith('Saved pstack settings')) saved = true;