@maxiaochao/pi-toolkit 0.9.3 → 0.9.6

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.
Files changed (85) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +40 -90
  3. package/extensions/{btw.ts → btw/index.ts} +1 -1
  4. package/extensions/{tests/btw-history.mjs → btw/tests/history.mjs} +1 -1
  5. package/package.json +19 -10
  6. package/packages/cache-export/CHANGELOG.md +5 -0
  7. package/packages/cache-export/README.md +70 -0
  8. package/packages/cache-export/TESTING.md +17 -0
  9. package/packages/cache-export/docs/demo.gif +0 -0
  10. package/packages/cache-export/docs/screenshot.png +0 -0
  11. package/{extensions/cache-export/index.ts → packages/cache-export/extensions/cache-export.ts} +1 -1
  12. package/packages/cache-export/package.json +22 -0
  13. package/{extensions/cache-export → packages/cache-export/src}/render.ts +1 -1
  14. package/{extensions → packages}/cache-export/tests/e2e-browser.mjs +2 -2
  15. package/{extensions → packages}/cache-export/tests/run-tests.mjs +2 -2
  16. package/packages/codex-edit/CHANGELOG.md +47 -0
  17. package/packages/codex-edit/README.md +130 -0
  18. package/packages/codex-edit/TESTING.md +51 -0
  19. package/packages/codex-edit/benchmark/explicit-edit-100.json +2824 -0
  20. package/packages/codex-edit/benchmark/export-evidence.mjs +77 -0
  21. package/packages/codex-edit/benchmark/incident-cases.mjs +37 -0
  22. package/packages/codex-edit/benchmark/prepare.mjs +181 -0
  23. package/packages/codex-edit/benchmark/results/explicit-edit-100-sol-low-v3.json +4562 -0
  24. package/packages/codex-edit/benchmark/run-with-budget.mjs +77 -0
  25. package/packages/codex-edit/benchmark/schedule.mjs +19 -0
  26. package/packages/codex-edit/benchmark/select-tasks.mjs +167 -0
  27. package/{extensions → packages}/codex-edit/codex-edit-explainer.html +43 -22
  28. package/packages/codex-edit/docs/demo.gif +0 -0
  29. package/packages/codex-edit/docs/screenshot.png +0 -0
  30. package/packages/codex-edit/docs/test-report.html +181 -0
  31. package/packages/codex-edit/docs/test-report.md +242 -0
  32. package/packages/codex-edit/docs/test-summary.md +9 -0
  33. package/{extensions/codex-edit/index.ts → packages/codex-edit/extensions/codex-edit.ts} +7 -5
  34. package/packages/codex-edit/package.json +54 -0
  35. package/{extensions/codex-edit → packages/codex-edit/src}/parser.ts +27 -5
  36. package/{extensions → packages}/codex-edit/summary.md +1 -1
  37. package/packages/codex-edit/tests/benchmark.test.mjs +81 -0
  38. package/packages/codex-edit/tests/comparison-cases.mjs +396 -0
  39. package/packages/codex-edit/tests/comparison-harness.mjs +44 -0
  40. package/packages/codex-edit/tests/generate-test-report.mjs +589 -0
  41. package/packages/codex-edit/tests/legacy-parser-v0.1.7.mjs +128 -0
  42. package/packages/codex-edit/tests/native-vs-codex.ts +109 -0
  43. package/packages/codex-edit/tests/package.test.mjs +51 -0
  44. package/packages/codex-edit/tests/parser.test.mjs +308 -0
  45. package/packages/codex-edit/tests/released-vs-local.test.mjs +31 -0
  46. package/packages/tiny-subagent/CHANGELOG.md +12 -0
  47. package/packages/tiny-subagent/README.md +128 -0
  48. package/packages/tiny-subagent/TESTING.md +20 -0
  49. package/packages/tiny-subagent/agents/worker.md +9 -0
  50. package/packages/tiny-subagent/docs/demo.gif +0 -0
  51. package/packages/tiny-subagent/docs/screenshot.png +0 -0
  52. package/packages/tiny-subagent/extensions/tiny-subagent.ts +228 -0
  53. package/packages/tiny-subagent/package.json +44 -0
  54. package/packages/tiny-subagent/src/personas.ts +110 -0
  55. package/packages/tiny-subagent/src/runner.ts +319 -0
  56. package/packages/tiny-subagent/tests/run-tests.mjs +111 -0
  57. package/packages/todo/CHANGELOG.md +5 -0
  58. package/packages/todo/README.md +63 -0
  59. package/packages/todo/TESTING.md +20 -0
  60. package/packages/todo/docs/demo.gif +0 -0
  61. package/packages/todo/docs/screenshot.png +0 -0
  62. package/{extensions → packages/todo/extensions}/todo.ts +1 -1
  63. package/packages/todo/package.json +25 -0
  64. package/{extensions → packages/todo}/tests/todo-state.mjs +1 -1
  65. package/packages/worktree/CHANGELOG.md +5 -0
  66. package/packages/worktree/README.md +60 -0
  67. package/packages/worktree/TESTING.md +21 -0
  68. package/packages/worktree/docs/demo.gif +0 -0
  69. package/packages/worktree/docs/screenshot.png +0 -0
  70. package/packages/worktree/package.json +20 -0
  71. package/packages/worktree/scripts/install.mjs +26 -0
  72. package/packages/worktree/tests/install.test.mjs +25 -0
  73. package/packages/worktree/tests/run-tests.sh +46 -0
  74. package/scripts/install-agents.mjs +8 -18
  75. package/extensions/codex-edit/README.md +0 -53
  76. package/extensions/codex-edit/tests/package.test.mjs +0 -38
  77. package/extensions/codex-edit/tests/parser.test.mjs +0 -89
  78. /package/extensions/{btw-scroll.ts → btw/scroll.ts} +0 -0
  79. /package/{extensions/cache-export → packages/cache-export/src}/tau-assets.ts +0 -0
  80. /package/{extensions → packages}/codex-edit/config.json +0 -0
  81. /package/{extensions/codex-edit → packages/codex-edit/src}/model-routing.ts +0 -0
  82. /package/{extensions → packages/todo/src}/todo-state.ts +0 -0
  83. /package/{bin → packages/worktree/bin}/pi-worktree +0 -0
  84. /package/{bin → packages/worktree/bin}/pi-worktree-completion.bash +0 -0
  85. /package/{skills → packages/worktree/skills}/pi-worktree/SKILL.md +0 -0
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { readFile } from "node:fs/promises";
4
+
5
+ function usage(message) {
6
+ if (message) console.error(message);
7
+ console.error("Usage: run-with-budget.mjs --progress <progress.json> --stop-cost <usd> --max-cost <usd> -- <command> [args...]");
8
+ process.exit(2);
9
+ }
10
+
11
+ const separator = process.argv.indexOf("--");
12
+ if (separator < 0) usage("Missing -- before the runner command");
13
+ const options = process.argv.slice(2, separator);
14
+ const command = process.argv.slice(separator + 1);
15
+ const valueFor = (name) => {
16
+ const index = options.indexOf(name);
17
+ return index < 0 ? undefined : options[index + 1];
18
+ };
19
+ const progressPath = valueFor("--progress");
20
+ const stopCost = Number(valueFor("--stop-cost"));
21
+ const maxCost = Number(valueFor("--max-cost"));
22
+ if (!progressPath || !command.length || !Number.isFinite(stopCost) || !Number.isFinite(maxCost) || stopCost <= 0 || maxCost < stopCost) usage();
23
+
24
+ function observedCost(progress) {
25
+ return (progress.results ?? []).reduce((sum, result) => sum + (result.recovery?.attempts ?? []).reduce(
26
+ (attemptSum, attempt) => attemptSum + Number(attempt.execution?.costUsd ?? 0),
27
+ 0,
28
+ ), 0);
29
+ }
30
+
31
+ const child = spawn(command[0], command.slice(1), {
32
+ stdio: "inherit",
33
+ detached: process.platform !== "win32",
34
+ });
35
+ let stopping = false;
36
+ const stop = (signal = "SIGTERM") => {
37
+ if (stopping) return;
38
+ stopping = true;
39
+ if (process.platform === "win32") child.kill(signal);
40
+ else process.kill(-child.pid, signal);
41
+ };
42
+ process.on("SIGINT", () => stop("SIGINT"));
43
+ process.on("SIGTERM", () => stop("SIGTERM"));
44
+
45
+ const timer = setInterval(async () => {
46
+ try {
47
+ const progress = JSON.parse(await readFile(progressPath, "utf8"));
48
+ const cost = observedCost(progress);
49
+ if (cost >= stopCost) {
50
+ console.error(`Budget stop: observed $${cost.toFixed(6)} >= scheduling threshold $${stopCost.toFixed(6)}`);
51
+ stop();
52
+ }
53
+ } catch (error) {
54
+ if (error?.code !== "ENOENT") console.error(`Budget monitor warning: ${error.message}`);
55
+ }
56
+ }, 1_000);
57
+ timer.unref();
58
+
59
+ const exit = await new Promise((resolve) => child.on("exit", (code, signal) => resolve({ code, signal })));
60
+ clearInterval(timer);
61
+ let finalCost = 0;
62
+ try {
63
+ finalCost = observedCost(JSON.parse(await readFile(progressPath, "utf8")));
64
+ } catch {}
65
+ if (finalCost > maxCost) {
66
+ console.error(`HARD BUDGET VIOLATION: observed $${finalCost.toFixed(6)} > $${maxCost.toFixed(6)}`);
67
+ process.exit(3);
68
+ }
69
+ if (stopping) {
70
+ console.error(`Runner stopped by budget gate at $${finalCost.toFixed(6)} (hard cap $${maxCost.toFixed(6)})`);
71
+ process.exit(0);
72
+ }
73
+ if (exit.signal) {
74
+ console.error(`Runner exited from ${exit.signal}`);
75
+ process.exit(1);
76
+ }
77
+ process.exit(exit.code ?? 1);
@@ -0,0 +1,19 @@
1
+ export function interleaveNeutralAndChallenge(entries, neutralPerChallenge = 4) {
2
+ if (!Number.isInteger(neutralPerChallenge) || neutralPerChallenge < 1) {
3
+ throw new Error("neutralPerChallenge must be a positive integer");
4
+ }
5
+ const neutral = entries.filter((entry) => entry.cohort === "neutral");
6
+ const challenge = entries.filter((entry) => entry.cohort === "challenge");
7
+ const unknown = entries.filter((entry) => !["neutral", "challenge"].includes(entry.cohort));
8
+ if (unknown.length) throw new Error(`Unknown cohort: ${unknown[0].cohort}`);
9
+ if (neutral.length !== challenge.length * neutralPerChallenge) {
10
+ throw new Error(`Expected a ${neutralPerChallenge}:1 neutral/challenge ratio, found ${neutral.length}:${challenge.length}`);
11
+ }
12
+
13
+ const scheduled = [];
14
+ for (let index = 0; index < challenge.length; index += 1) {
15
+ scheduled.push(...neutral.slice(index * neutralPerChallenge, (index + 1) * neutralPerChallenge));
16
+ scheduled.push(challenge[index]);
17
+ }
18
+ return scheduled;
19
+ }
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from "node:crypto";
4
+ import { execFileSync } from "node:child_process";
5
+ import { mkdirSync, writeFileSync } from "node:fs";
6
+ import { dirname, resolve } from "node:path";
7
+ import { interleaveNeutralAndChallenge } from "./schedule.mjs";
8
+
9
+ const SOURCE_COMMIT = "5827e3fb65845f4b76adb06e5919d94ec4ffd5cf";
10
+ const SEED = "pi-toolkit-codex-edit-100-v1";
11
+ const output = resolve("packages/codex-edit/benchmark/explicit-edit-100.json");
12
+ const sourceFlag = process.argv.indexOf("--source");
13
+ if (sourceFlag < 0 || !process.argv[sourceFlag + 1]) {
14
+ throw new Error("Usage: node select-tasks.mjs --source /path/to/explicit-edit-benchmark");
15
+ }
16
+ const source = resolve(process.argv[sourceFlag + 1]);
17
+ const commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: source, encoding: "utf8" }).trim();
18
+ if (commit !== SOURCE_COMMIT) throw new Error(`Expected source ${SOURCE_COMMIT}, found ${commit}`);
19
+
20
+ const tasks = JSON.parse(
21
+ execFileSync(process.execPath, ["scripts/list-tasks.mjs"], {
22
+ cwd: source,
23
+ encoding: "utf8",
24
+ maxBuffer: 10 * 1024 * 1024,
25
+ }),
26
+ );
27
+ if (tasks.length !== 226) throw new Error(`Expected 226 source tasks, found ${tasks.length}`);
28
+
29
+ const neutralQuotas = new Map([
30
+ ["language-replace", 13],
31
+ ["language-select", 13],
32
+ ["language-insert", 13],
33
+ ["literal", 4],
34
+ ["replace-all", 3],
35
+ ["select-one", 3],
36
+ ["unicode-fix", 3],
37
+ ["unique", 2],
38
+ ["multi-file", 2],
39
+ ["select-subset", 2],
40
+ ["delete-subset", 2],
41
+ ["insert-subset", 2],
42
+ ["distinct-edits", 2],
43
+ ["replace-block", 2],
44
+ ["delete-block", 2],
45
+ ["move-block", 2],
46
+ ["copy-block", 2],
47
+ ["insert-block", 2],
48
+ ["insert-payload", 2],
49
+ ["copy-within", 2],
50
+ ["move-between", 2],
51
+ ]);
52
+
53
+ const challengeQuotas = new Map([
54
+ ["select-one", 1],
55
+ ["select-subset", 2],
56
+ ["delete-subset", 1],
57
+ ["insert-subset", 1],
58
+ ["multi-file", 2],
59
+ ["move-between", 2],
60
+ ["distinct-edits", 2],
61
+ ["replace-block", 1],
62
+ ["delete-block", 1],
63
+ ["move-block", 1],
64
+ ["copy-block", 1],
65
+ ["insert-block", 1],
66
+ ["insert-payload", 1],
67
+ ["copy-within", 1],
68
+ ["literal", 1],
69
+ ["unicode-fix", 1],
70
+ ]);
71
+
72
+ const byCategory = new Map();
73
+ for (const task of tasks) {
74
+ const group = byCategory.get(task.category) ?? [];
75
+ group.push(task);
76
+ byCategory.set(task.category, group);
77
+ }
78
+ if ([...neutralQuotas.values()].reduce((sum, count) => sum + count, 0) !== 80) {
79
+ throw new Error("Neutral quotas must total 80");
80
+ }
81
+ if ([...challengeQuotas.values()].reduce((sum, count) => sum + count, 0) !== 20) {
82
+ throw new Error("Challenge quotas must total 20");
83
+ }
84
+ if ([...byCategory.keys()].some((category) => !neutralQuotas.has(category))) {
85
+ throw new Error("Neutral quotas do not cover every source category");
86
+ }
87
+
88
+ const rank = (label, task) =>
89
+ createHash("sha256")
90
+ .update(`${SEED}\0${label}\0${task.id}\0${task.fixtureSha256}`)
91
+ .digest("hex");
92
+ const compareRank = (label) => (left, right) => rank(label, left).localeCompare(rank(label, right));
93
+
94
+ const neutral = [];
95
+ for (const [category, quota] of neutralQuotas) {
96
+ const group = byCategory.get(category) ?? [];
97
+ if (group.length < quota) throw new Error(`Insufficient neutral tasks in ${category}`);
98
+ neutral.push(...group.toSorted(compareRank(`neutral:${category}`)).slice(0, quota));
99
+ }
100
+ const selectedIds = new Set(neutral.map((task) => task.id));
101
+
102
+ const challenge = [];
103
+ for (const [category, quota] of challengeQuotas) {
104
+ const candidates = (byCategory.get(category) ?? [])
105
+ .filter((task) => !selectedIds.has(task.id))
106
+ .toSorted((left, right) =>
107
+ right.scale - left.scale ||
108
+ Number(right.variant === "unicode") - Number(left.variant === "unicode") ||
109
+ compareRank(`challenge:${category}`)(left, right),
110
+ );
111
+ if (candidates.length < quota) throw new Error(`Insufficient challenge tasks in ${category}`);
112
+ const chosen = candidates.slice(0, quota);
113
+ challenge.push(...chosen);
114
+ for (const task of chosen) selectedIds.add(task.id);
115
+ }
116
+
117
+ const entry = (task, cohort) => ({
118
+ id: task.id,
119
+ fixtureSha256: task.fixtureSha256,
120
+ category: task.category,
121
+ scale: task.scale,
122
+ language: task.language,
123
+ variant: task.variant,
124
+ cohort,
125
+ reasons: [{ profile: "pi-toolkit", reason: `${cohort}-cohort-v1` }],
126
+ });
127
+ const selected = [
128
+ ...neutral.toSorted((left, right) => left.id.localeCompare(right.id)).map((task) => entry(task, "neutral")),
129
+ ...challenge
130
+ .toSorted((left, right) => left.id.localeCompare(right.id))
131
+ .map((task) => entry(task, "challenge")),
132
+ ];
133
+ const included = interleaveNeutralAndChallenge(selected);
134
+ if (included.length !== 100 || new Set(included.map(({ id }) => id)).size !== 100) {
135
+ throw new Error("Selection must contain 100 unique tasks");
136
+ }
137
+
138
+ const excluded = tasks
139
+ .filter((task) => !selectedIds.has(task.id))
140
+ .toSorted((left, right) => left.id.localeCompare(right.id))
141
+ .map((task) => ({
142
+ id: task.id,
143
+ fixtureSha256: task.fixtureSha256,
144
+ reasons: [{ profile: "pi-toolkit", reason: "not-selected-v1" }],
145
+ }));
146
+ const manifest = {
147
+ version: 1,
148
+ criterion: "frozen-proportional-80-plus-declared-challenge-20-v1",
149
+ profiles: ["native-edit", "released-codex-edit", "local-codex-edit"],
150
+ source: {
151
+ repository: "https://github.com/alexshpunt/explicit-edit-benchmark",
152
+ commit: SOURCE_COMMIT,
153
+ taskCount: tasks.length,
154
+ },
155
+ selection: {
156
+ seed: SEED,
157
+ neutral: Object.fromEntries(neutralQuotas),
158
+ challenge: Object.fromEntries(challengeQuotas),
159
+ challengeOrdering: "descending scale, Unicode first, then seeded SHA-256 rank",
160
+ scheduling: "four-neutral-then-one-challenge-v1",
161
+ },
162
+ included,
163
+ excluded,
164
+ };
165
+ mkdirSync(dirname(output), { recursive: true });
166
+ writeFileSync(output, `${JSON.stringify(manifest, null, 2)}\n`);
167
+ console.log(`Wrote ${output}: ${neutral.length} neutral + ${challenge.length} challenge`);
@@ -135,20 +135,20 @@ footer { max-width: 1180px; margin: 0 auto; padding: 0 26px 54px; color: var(--m
135
135
  <div class="topbar-inner">
136
136
  <div class="brand">Pi <span>apply_patch</span> 方案说明</div>
137
137
  <nav class="nav" aria-label="页面导航">
138
- <a href="#answer">结论</a><a href="#architecture">架构</a><a href="#config">配置</a><a href="#principle">原理</a><a href="#evidence">数据</a>
138
+ <a href="#answer">结论</a><a href="#install">安装</a><a href="#architecture">架构</a><a href="#config">配置</a><a href="#principle">原理</a><a href="#evidence">数据</a>
139
139
  </nav>
140
140
  </div>
141
141
  </header>
142
142
  <main>
143
143
  <section class="hero-grid" id="answer">
144
144
  <div>
145
- <div class="eyebrow">GPT-5.6 / Pi / Codex-style editing</div>
145
+ <div class="eyebrow">GPT-5.6 / Pi / Codex harness apply_patch</div>
146
146
  <h1>它变好了,但改善主要在“第一次编辑能不能正确开始”。</h1>
147
- <p class="lede">公平对照下,apply_patch 没有提升最终正确率,却把 GPT-5.6 的首次编辑成功率从 85% 提到 95%,并消除了观察到的编辑协议失败。它适合作为 GPT/Codex 模型的候选工具,不应无条件替代所有模型的 edit。</p>
147
+ <p class="lede">这个扩展让选中的 GPT 模型在 Pi 中使用与 Codex harness 相同的 apply_patch 协议;其他模型仍使用 Pi 原生 edit。公平对照下,GPT-5.6 的首次编辑成功率从 85% 提到 95%,并消除了观察到的编辑协议失败。</p>
148
148
  </div>
149
149
  <aside class="signal">
150
- <strong>当前包状态:可安装,按模型路由</strong>
151
- <p>这个 npm 包包含真正的 apply_patch extension。安装后,GPT/Codex 模型按配置使用 apply_patch,其他模型继续使用 edit。</p>
150
+ <strong>当前状态:根包内置,也可单独安装</strong>
151
+ <p><code>@maxiaochao/pi-toolkit</code> 默认包含本扩展;只需要 apply_patch 时可安装 <code>@maxiaochao/pi-codex-edit</code>。两种方式使用同一份实现。</p>
152
152
  </aside>
153
153
  </section>
154
154
 
@@ -161,6 +161,26 @@ footer { max-width: 1180px; margin: 0 auto; padding: 0 26px 54px; color: var(--m
161
161
  </div>
162
162
  </section>
163
163
 
164
+ <section class="section" id="install">
165
+ <div class="section-head"><div><div class="eyebrow">Distribution</div><h2>完整工具包与独立子包二选一</h2></div><p class="section-note">同时安装没有额外收益,还可能重复注册 <code>apply_patch</code>。</p></div>
166
+ <div class="explain">
167
+ <article class="panel">
168
+ <span class="badge green">完整工具包</span>
169
+ <h3 style="margin-top:16px">随 pi-toolkit 默认加载</h3>
170
+ <div class="code-wrap"><button class="copy" data-copy="toolkit-install">复制</button><pre id="toolkit-install">pi install npm:@maxiaochao/pi-toolkit
171
+ pi update npm:@maxiaochao/pi-toolkit</pre></div>
172
+ <p>根 manifest 直接加载 <code>packages/codex-edit/extensions/codex-edit.ts</code>。只有发布了新的 toolkit 版本,根包用户才会收到 Codex Edit 更新。</p>
173
+ </article>
174
+ <article class="panel">
175
+ <span class="badge amber">独立子包</span>
176
+ <h3 style="margin-top:16px">只安装 Codex Edit</h3>
177
+ <div class="code-wrap"><button class="copy" data-copy="child-install">复制</button><pre id="child-install">pi install npm:@maxiaochao/pi-codex-edit
178
+ pi update npm:@maxiaochao/pi-codex-edit</pre></div>
179
+ <p>子包拥有独立版本和发布流程。只发布子包不会更新已安装的 toolkit;实现或配置变更需要分别发布两个 npm 包。</p>
180
+ </article>
181
+ </div>
182
+ </section>
183
+
164
184
  <section class="section">
165
185
  <div class="section-head"><div><div class="eyebrow">先解释指标</div><h2>“编辑协议失败:1 → 0”到底是什么?</h2></div><p class="section-note">这是本次 benchmark 自定义的分类名,不是 HTTP、Responses API 或 custom tool 协议错误。</p></div>
166
186
  <div class="explain">
@@ -187,7 +207,7 @@ footer { max-width: 1180px; margin: 0 auto; padding: 0 26px 54px; color: var(--m
187
207
  <div class="arch-row">
188
208
  <div class="arch-node"><small>01 · Model state</small><strong>当前模型配置</strong><p>ctx.model.provider、api、id、compat 能力标记。</p></div>
189
209
  <div class="arch-arrow">→</div>
190
- <div class="arch-node"><small>02 · Routing</small><strong>syncEditorTools()</strong><p>命中的 GPT/Codex 模型启用 apply_patch;其他模型启用 edit。</p></div>
210
+ <div class="arch-node"><small>02 · Routing</small><strong>syncEditorTools()</strong><p>ID glob 和 Responses API 均命中时启用 apply_patch;其他模型启用 Pi edit。</p></div>
191
211
  <div class="arch-arrow">→</div>
192
212
  <div class="arch-node"><small>03 · Provider</small><strong>Responses serializer</strong><p>Grammar 支持时序列化为 type=custom + Lark definition,否则回退为普通 function tool。</p></div>
193
213
  <div class="arch-arrow">→</div>
@@ -210,51 +230,52 @@ footer { max-width: 1180px; margin: 0 auto; padding: 0 26px 54px; color: var(--m
210
230
  </section>
211
231
 
212
232
  <section class="section" id="config">
213
- <div class="section-head"><div><div class="eyebrow">Configuration</div><h2>当前配置与可选扩展</h2></div><p class="section-note">默认只为已稳定验证的 Sol High 启用;Luna 的性能结果存在长尾波动,需要显式 opt-in。</p></div>
233
+ <div class="section-head"><div><div class="eyebrow">Configuration</div><h2>当前模型路由</h2></div><p class="section-note">默认覆盖 Responses API 上的全部 <code>gpt-5.6-*</code> 变体;provider 不参与匹配。</p></div>
214
234
  <div class="tabs" role="tablist" aria-label="配置视图">
215
235
  <button class="tab" role="tab" aria-selected="true" aria-controls="config-current" id="tab-current">当前实际配置</button>
216
- <button class="tab" role="tab" aria-selected="false" aria-controls="config-recommended" id="tab-recommended">Luna 显式启用</button>
236
+ <button class="tab" role="tab" aria-selected="false" aria-controls="config-recommended" id="tab-recommended">添加模型</button>
217
237
  <button class="tab" role="tab" aria-selected="false" aria-controls="config-route" id="tab-route">路由规则</button>
218
238
  </div>
219
239
  <div class="tab-panel active" id="config-current" role="tabpanel" aria-labelledby="tab-current">
220
240
  <div class="code-wrap"><button class="copy" data-copy="current-config">复制</button><pre id="current-config">{
221
241
  "enabled": true,
222
242
  "models": [
223
- { "provider": "pudu-openai-proxy", "model": "gpt-5.6-sol" }
243
+ "gpt-5.6-*"
224
244
  ]
225
245
  }</pre></div>
226
- <div class="callout">models.json 中的 provider API key 不在页面展示。建议使用环境变量或 Pi auth 存储,并轮换已经出现在配置文件中的 key。</div>
246
+ <div class="callout">路由还要求 <code>model.api</code> 为 <code>openai-responses</code> 或 <code>openai-codex-responses</code>。grammar 不可用时 Pi 回退为普通 function tool。</div>
227
247
  </div>
228
248
  <div class="tab-panel" id="config-recommended" role="tabpanel" aria-labelledby="tab-recommended">
229
249
  <div class="code-wrap"><button class="copy" data-copy="recommended-config">复制</button><pre id="recommended-config">{
230
250
  "enabled": true,
231
251
  "models": [
232
- { "provider": "pudu-openai-proxy", "model": "gpt-5.6-sol" },
233
- { "provider": "pudu-openai-proxy", "model": "gpt-5.6-luna" }
252
+ "gpt-5.6-*",
253
+ "gpt-5.7-*",
254
+ "my-codex-model"
234
255
  ]
235
256
  }</pre></div>
236
- <p>Luna High 的有效重测中,两组都 20/20 正常结束,最终正确率都是 18/20;首次编辑成功率为 edit 90%、apply_patch 100%。按 17 个两组都正确的配对样本,apply_patch 中位总耗时多 2.3s;按全部 20 个配对样本,中位多 1.2s,9 例更快、11 例更慢。因此 Luna 有协议适配收益,但没有稳定的总耗时优势,当前不默认启用。</p>
257
+ <p>每项按完整 <code>model.id</code> 做区分大小写的匹配,<code>*</code> 是通配符。精确 ID 也可以直接作为规则。添加规则只改变路由,不代表模型已经通过协议验证;未命中规则的模型继续使用 Pi 原生 <code>edit</code>。</p>
237
258
  </div>
238
259
  <div class="tab-panel" id="config-route" role="tabpanel" aria-labelledby="tab-route">
239
260
  <div class="code-wrap"><button class="copy" data-copy="route-rule">复制</button><pre id="route-rule">model = ctx.model
240
261
 
241
262
  if enabled
242
263
  and model.api in ["openai-responses", "openai-codex-responses"]
243
- and provider/model matches allowlist:
264
+ and model.id matches any models glob:
244
265
  active = ["apply_patch"]
245
266
  else:
246
267
  active = ["edit"]
247
268
 
248
269
  # Preserve all unrelated active tools.
249
270
  # Register both tools permanently for session replay.</pre></div>
250
- <p>切换发生在 <code>session_start</code> 和 <code>model_select</code>。不重新定义同名工具,只改变 active tool 集合。</p>
271
+ <p><code>provider</code> 不参与匹配;即使 ID 命中,其他 API 上的模型仍使用 Pi <code>edit</code>。切换发生在 <code>session_start</code> 和 <code>model_select</code>。独立包修改 <code>config.json</code>,完整 toolkit 修改 <code>packages/codex-edit/config.json</code>,但包更新可能覆盖本地配置。</p>
251
272
  </div>
252
273
  </section>
253
274
 
254
275
  <section class="section" id="principle">
255
276
  <div class="section-head"><div><div class="eyebrow">Design principles</div><h2>这个工具的三个核心原则</h2></div></div>
256
277
  <div class="principles">
257
- <article class="principle"><h3>模型协议对齐</h3><p>GPT/Codex 模型不再被要求适应 Pi 自定义的 <code>oldText/newText</code> JSON;它看到原生的 <code>apply_patch</code> 名称和 patch grammar。</p></article>
278
+ <article class="principle"><h3>模型协议对齐</h3><p>选中的 GPT 模型不再被要求适应 Pi 自定义的 <code>oldText/newText</code> JSON;它看到与 Codex harness 相同的 <code>apply_patch</code> 名称和 patch grammar。</p></article>
258
279
  <article class="principle"><h3>执行端独立防护</h3><p>grammar 不是安全策略。执行端仍要做 workspace 边界、符号链接、目录、覆盖冲突、权限和取消检查。</p></article>
259
280
  <article class="principle"><h3>按能力选择,不靠 AGENTS 对抗</h3><p>不在 AGENTS 中要求模型“不要使用 apply_patch”。扩展只让模型看到适合自己的一个编辑工具,避免两套协议同时暴露。</p></article>
260
281
  </div>
@@ -276,7 +297,7 @@ else:
276
297
  </tbody>
277
298
  </table>
278
299
  <div class="panel" style="margin-top:18px">
279
- <div class="status"><i></i>最终判断:候选实现保留,默认自动启用暂不打开</div>
300
+ <div class="status"><i></i>最终判断:已投入使用,但只对 allowlist 中的 GPT/Codex 模型路由 apply_patch</div>
280
301
  <p>本页的耗时比较优先使用同一 fixture 的配对差值,避免两组成功样本构成不同造成中位数偏差。runner 的 180s 进程上限是删失边界,不是 API timeout;本次有效 Luna 重测没有触发该上限。</p>
281
302
  </div>
282
303
  </section>
@@ -284,14 +305,14 @@ else:
284
305
  <section class="section">
285
306
  <div class="section-head"><div><div class="eyebrow">Files</div><h2>实现位置</h2></div></div>
286
307
  <div class="panel">
287
- <p><code>extensions/codex-edit/index.ts</code>:工具注册、执行、防护、renderer 和模型路由。</p>
288
- <p><code>extensions/codex-edit/parser.ts</code>:patch grammar 对应的 parser、hunk matcher 和 workspace path resolver。</p>
289
- <p><code>extensions/codex-edit/tests/parser.test.mjs</code>:6 个纯函数/文件操作测试,当前全部通过。</p>
290
- <p><code>/home/maxiaochao/.pi/agent/evaluations/gpt-edit-benchmark/report-final.md</code>:最终 benchmark 报告。</p>
308
+ <p><code>packages/codex-edit/extensions/codex-edit.ts</code>:工具注册、执行、防护、renderer 和模型路由。</p>
309
+ <p><code>packages/codex-edit/src/parser.ts</code>:patch grammar 对应的 parser、顺序 hunk matcher、同路径 update 合并和 workspace path resolver。</p>
310
+ <p><code>packages/codex-edit/tests/</code>:22 个 Node 测试与 12 个共享对比 case,覆盖 native edit vs local Codex Edit、released v0.1.7 vs local parser、matcher、路径、结构重构、报告重建和工作区边界。</p>
311
+ <p><code>packages/codex-edit/docs/test-report.html</code>:当前自包含测试报告;同目录 Markdown 是权威中间源。</p>
291
312
  </div>
292
313
  </section>
293
314
  </main>
294
- <footer>说明页基于 Pi 0.84.4、Codex apply_patch 源码分析和 20-case GPT-5.6 benchmark。页面内没有包含任何 API key。</footer>
315
+ <footer>说明页基于 Pi 0.85.1、Codex apply_patch 源码分析、历史 20-case 证据和已完成的 100-task 三臂基准。正式运行完成 300/300 条 chain,本地版满足全部验收门槛;页面内没有包含任何 API key。</footer>
295
316
  <script>
296
317
  const tabs = [...document.querySelectorAll('.tab')];
297
318
  const panels = [...document.querySelectorAll('.tab-panel')];