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 +10 -2
- package/README.md +6 -2
- package/README.zh-CN.md +6 -2
- package/REVIEW.md +1 -1
- package/SEMANTICS.json +10 -6
- package/dist/models/router.js +6 -7
- package/dist/setup/command.js +177 -26
- package/package.json +1 -1
- package/src/models/router.ts +6 -5
- package/src/setup/command.ts +149 -26
- package/test/model-panels.test.ts +26 -0
- package/test/model-router.test.ts +2 -3
- package/test/setup-command.test.ts +40 -2
- package/test/setup-rpc.test.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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`
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
配置界面提供中文和英文说明。命令先询问推理预算,再分页展示角色、任务模型和根据当前 Pi 可用模型建议的面板。你可以直接保存,也可以只修改某个角色、任务或面板席位。界面会标出不可用的模型和不支持的推理等级。请在保存前修正这些选项。已有设置会保留。保存前取消不会写入文件。`inherit-parent` 和 `auto` 使用父会话的模型。
|
|
57
57
|
|
|
58
|
-
|
|
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
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": "
|
|
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": "
|
|
2379
|
-
"test/setup-command.test.ts": "
|
|
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": "
|
|
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": "
|
|
2396
|
-
"test/setup-rpc.test.ts": "
|
|
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
|
},
|
package/dist/models/router.js
CHANGED
|
@@ -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
|
-
|
|
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 &&
|
|
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.
|
package/dist/setup/command.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =>
|
|
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 =>
|
|
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
|
|
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
|
|
448
|
+
const action = await selectBilingual(ctx, title, actions);
|
|
298
449
|
if (action === undefined) {
|
|
299
|
-
ctx
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|
package/src/models/router.ts
CHANGED
|
@@ -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
|
-
|
|
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 &&
|
|
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.
|
package/src/setup/command.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =>
|
|
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 =>
|
|
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
|
|
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
|
|
259
|
-
if (action === undefined) { ctx
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
307
|
-
if (selected.kind === "choice" && !await editPanel(ctx, next, selected.value, available)) { ctx
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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[]) => {
|
|
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 () => {
|
package/test/setup-rpc.test.ts
CHANGED
|
@@ -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;
|