agent-purge 0.2.9 → 0.2.11

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 (75) hide show
  1. package/CONTRIBUTING.md +64 -0
  2. package/LICENSE +21 -21
  3. package/README.md +176 -118
  4. package/backends/index.mjs +204 -244
  5. package/bench/bench-armor.mjs +844 -832
  6. package/bench/capture-hot.mjs +58 -0
  7. package/bench/capture-provider.mjs +66 -0
  8. package/bench/capture-report.mjs +15 -0
  9. package/bench/capture-server-hot.mjs +55 -0
  10. package/bench/capture-system.mjs +36 -0
  11. package/bench/judge-armor.mjs +119 -119
  12. package/bin/agent-purge.mjs +906 -772
  13. package/docs/LEGAL.md +27 -0
  14. package/docs/architecture.md +39 -0
  15. package/docs/compatibility.md +78 -0
  16. package/docs/images/console-desktop.png +0 -0
  17. package/docs/images/console-mobile.png +0 -0
  18. package/docs/releasing.md +87 -0
  19. package/docs/usage.md +158 -0
  20. package/lib/activation.mjs +38 -0
  21. package/lib/apply.mjs +227 -0
  22. package/lib/armed.mjs +38 -38
  23. package/lib/armor.mjs +225 -222
  24. package/lib/caps.mjs +182 -173
  25. package/lib/capture.mjs +324 -0
  26. package/lib/clean.mjs +124 -0
  27. package/lib/control.mjs +60 -0
  28. package/lib/detect.mjs +74 -70
  29. package/lib/doctor.mjs +7 -7
  30. package/lib/dsh.mjs +495 -453
  31. package/lib/environment.mjs +36 -0
  32. package/lib/evidence.mjs +41 -0
  33. package/lib/extension.mjs +140 -0
  34. package/lib/footprints.mjs +59 -0
  35. package/lib/home.mjs +47 -47
  36. package/lib/launch-conflicts.mjs +24 -0
  37. package/lib/launch.mjs +57 -0
  38. package/lib/opencode.mjs +62 -0
  39. package/lib/paths.mjs +25 -0
  40. package/lib/persist.mjs +439 -280
  41. package/lib/personas.mjs +17 -7
  42. package/lib/private-server.mjs +54 -0
  43. package/lib/project.mjs +134 -0
  44. package/lib/restore.mjs +131 -114
  45. package/lib/rewrite.mjs +124 -0
  46. package/lib/root.mjs +37 -0
  47. package/lib/shim.mjs +203 -160
  48. package/lib/slashes.mjs +227 -628
  49. package/lib/spawn.mjs +182 -126
  50. package/lib/state.mjs +85 -137
  51. package/lib/templates/personas/neutral.md +30 -30
  52. package/lib/update.mjs +137 -0
  53. package/lib/verification.mjs +23 -0
  54. package/lib/window.mjs +58 -0
  55. package/lib/wrappers.mjs +49 -0
  56. package/magpie-plugin/README.md +44 -0
  57. package/matrix.json +325 -341
  58. package/package.json +30 -4
  59. package/web/app.js +525 -0
  60. package/web/assets/agent-purge.ico +0 -0
  61. package/web/assets/claude.ico +0 -0
  62. package/web/assets/codebuddy.png +0 -0
  63. package/web/assets/codex.webp +0 -0
  64. package/web/assets/dsh.ico +0 -0
  65. package/web/assets/grok.ico +0 -0
  66. package/web/assets/hermes.ico +0 -0
  67. package/web/assets/omp.png +0 -0
  68. package/web/assets/opencode.ico +0 -0
  69. package/web/assets/opencode2.ico +0 -0
  70. package/web/assets/pi.png +0 -0
  71. package/web/assets/zcode.png +0 -0
  72. package/web/model.mjs +49 -0
  73. package/web/server.mjs +309 -194
  74. package/web/styles.css +917 -0
  75. package/web/ui.html +64 -1163
package/lib/armor.mjs CHANGED
@@ -1,222 +1,225 @@
1
- // 实测数据 → 策略。矩阵里每个 harness 的 armor 数字来自 bench/bench-armor.mjs 的实跑,
2
- // 这里只做一件事:把"哪一层真的能注入"翻译成默认值、禁层、预检项与可复制命令。
3
- //
4
- // 三条铁律(由实测矩阵得出,改代码前先读 matrix.json 的 armor 段):
5
- // 1. sys 层(写进 harness 自己的系统提示/规则文件)是唯一跨家稳定的面;
6
- // 2. user 层(塞进用户消息)只在少数 harness 有效,其余当普通请求拒答;
7
- // 3. claude 裸注入无效、带人设词种子才破 —— 所以它需要 seed 层,而不是换 mode。
8
- import { spawnSync } from "node:child_process";
9
- import { existsSync } from "node:fs";
10
- import { dirname, join } from "node:path";
11
- import { fileURLToPath } from "node:url";
12
-
13
- const ROOT = process.versions.bun ? dirname(process.execPath) : dirname(dirname(fileURLToPath(import.meta.url)));
14
- export const LAYERS = ["off", "sys", "user", "seed"];
15
-
16
- const C = {
17
- reset: "\x1b[0m",
18
- dim: "\x1b[2m",
19
- bold: "\x1b[1m",
20
- green: "\x1b[32m",
21
- yellow: "\x1b[33m",
22
- red: "\x1b[31m",
23
- cyan: "\x1b[36m",
24
- gray: "\x1b[90m",
25
- };
26
-
27
- const pct = (v) => (v === null || v === undefined ? null : Number(v));
28
- const fmtCell = (v) => (v === null ? "—" : `${v}%`);
29
- const fmtPct = (v) => (v === null || v === undefined ? "—" : `${v}%`);
30
- const tier = (v) => (v === null || v === undefined ? "none" : v >= 80 ? "high" : v > 0 ? "mid" : "low");
31
-
32
- function paint(text, kind, color = true) {
33
- if (!color) return text;
34
- const map = { high: C.green, mid: C.yellow, low: C.red, none: C.gray };
35
- return `${map[kind] ?? ""}${text}${C.reset}`;
36
- }
37
-
38
- export function armorOf(matrix, id) {
39
- return matrix.harnesses?.[id]?.armor ?? null;
40
- }
41
-
42
- /** 把某家的实测数字压成一条策略。所有字段都能在 UI / CLI / bench 复用。 */
43
- export function plan(matrix, id) {
44
- const spec = matrix.harnesses?.[id];
45
- const a = spec?.armor;
46
- if (!spec) return { id, error: `未知 harness: ${id}` };
47
- if (!a) return { id, error: `${id} 没有 armor 实测数据(先跑 agent-purge armor run --only ${id})` };
48
-
49
- const bare = a.probe?.bare ?? {};
50
- const persona = a.probe?.persona ?? {};
51
- const sys = pct(bare.sys);
52
- const user = pct(bare.user);
53
- const seedSys = pct(persona.sys);
54
-
55
- const blocked = [];
56
- const recommended = [];
57
-
58
- if (spec.class === "unsupported") blocked.push({ layer: "sys", reason: spec.reason ?? "该 harness 不支持注入" });
59
- else if (sys === null) blocked.push({ layer: "sys", reason: a.sysBlockedReason ?? "系统层未测得可用注入点" });
60
- if (user === null) blocked.push({ layer: "user", reason: a.userBlockedReason ?? "用户层未测得(harness 崩溃或未适配)" });
61
- if (sys !== null && sys >= 80) recommended.push("sys");
62
- if (user !== null && user >= 80) recommended.push("user");
63
-
64
- // claude 型:裸注入不破,但带 persona 种子词就破 —— 需要 seed,不是换 mode
65
- const needsSeed = sys !== null && sys < 80 && seedSys !== null && seedSys >= 80;
66
- if (needsSeed && !blocked.some((b) => b.layer === "sys")) {
67
- recommended.push("sys+seed");
68
- }
69
- if (!recommended.length && !blocked.some((b) => b.layer === "sys")) recommended.push("sys");
70
-
71
- const warnings = [];
72
- if (spec.class === "file-swap" || spec.class === "config-file") warnings.push("注入要落盘:用 agent-purge 启动才会自动回滚(journal 在系统临时目录)");
73
- if (needsSeed && !(a.note ?? "").includes("种子")) warnings.push(`裸问法 sys 层 ${sys}%,带人设词种子后 ${seedSys}% —— 必须走 seed 层`);
74
- if (a.note) warnings.push(a.note);
75
- // user 层无效的提示只在它曾经有效过的家才说(否则每行重复一句废话)
76
- if (user !== null && user < 80 && user > 0) warnings.push(`user 层实测 ${user}%,不稳定,以 sys 为主`);
77
-
78
- return {
79
- id,
80
- class: spec.class,
81
- claudeLike: needsSeed,
82
- bare,
83
- persona,
84
- blocked,
85
- recommended,
86
- defaultLayers: recommended.filter((x) => !x.includes("+")),
87
- requiresSeed: needsSeed,
88
- requiresProviderSwap: a.providerSwap === true,
89
- mode: a.preferredMode ?? "replace",
90
- warnings,
91
- cmd: benchCommand(id, { task: "crack-bare", layers: recommended.includes("user") ? "off,sys,user" : "off,sys", repeat: 2 }),
92
- };
93
- }
94
-
95
- export function plans(matrix) {
96
- return Object.keys(matrix.harnesses).map((id) => plan(matrix, id)).filter((p) => !p.error);
97
- }
98
-
99
- /** 顶部汇总:sys 注入几家、user 注入几家、最大增益。UI 与 CLI 共用。 */
100
- export function summary(matrix) {
101
- const rows = plans(matrix);
102
- const measured = rows.filter((r) => r.bare.sys !== null || r.bare.user !== null).length;
103
- const sysOk = rows.filter((r) => pct(r.bare.sys) >= 80).length;
104
- const userOk = rows.filter((r) => pct(r.bare.user) >= 80).length;
105
- const maxDelta = rows.reduce((best, r) => {
106
- const d = pct(r.bare.sys) - pct(r.bare.off);
107
- return d > best.delta ? { delta: d, id: r.id } : best;
108
- }, { delta: 0, id: null });
109
- return {
110
- total: rows.length,
111
- measured,
112
- sysOk,
113
- userOk,
114
- maxDelta: maxDelta.delta,
115
- maxDeltaHarness: maxDelta.id,
116
- target: matrix.armorTarget ?? null,
117
- measuredAt: matrix.armorMeasuredAt ?? null,
118
- };
119
- }
120
-
121
- export function renderMatrix(matrix, { color = process.stdout.isTTY } = {}) {
122
- const rows = Object.entries(matrix.harnesses).map(([id, s]) => {
123
- const p = plan(matrix, id);
124
- const b = p.bare ?? {};
125
- return {
126
- id,
127
- class: s.class,
128
- off: pct(b.off),
129
- sys: pct(b.sys),
130
- user: pct(b.user),
131
- best: p.recommended?.length ? p.recommended.join("+") : "—",
132
- };
133
- });
134
- const head = ["harness", "class", "off", "sys", "user", "推荐层"];
135
- const cells = rows.map((r) => [r.id, r.class, fmtCell(r.off), fmtCell(r.sys), fmtCell(r.user), r.best]);
136
- const width = head.map((h, i) => Math.max(h.length, ...cells.map((c) => [...c[i]].length)));
137
- const pad = (s, w) => s + " ".repeat(Math.max(0, w - [...s].length));
138
- console.log(paint(pad("harness", width[0]) + " " + pad("class", width[1]) + " " + pad("off", width[2]) + " " + pad("sys", width[3]) + " " + pad("user", width[4]) + " 推荐层", "none", color));
139
- console.log(width.map((w) => "─".repeat(w)).join(" "));
140
- rows.forEach((r) => {
141
- console.log(
142
- [
143
- pad(r.id, width[0]),
144
- pad(r.class, width[1]),
145
- pad(paint(fmtCell(r.off), tier(r.off), color), width[2]),
146
- pad(paint(fmtCell(r.sys), tier(r.sys), color), width[3]),
147
- pad(paint(fmtCell(r.user), tier(r.user), color), width[4]),
148
- r.best,
149
- ].join(" "),
150
- );
151
- });
152
- const s = summary(matrix);
153
- console.log(
154
- `\n${paint(`sys 层注入 ${s.sysOk}/${s.measured}(已测)`, s.sysOk >= s.measured * 0.8 ? "high" : "mid", color)} · ` +
155
- `${paint(`user 层注入 ${s.userOk}/${s.measured}`, s.userOk > 0 ? "mid" : "low", color)} · ` +
156
- `最大增益 +${s.maxDelta}pp(${s.maxDeltaHarness})`,
157
- );
158
- if (s.target) console.log(`${color ? C.dim : ""}实测目标:${s.target} · ${s.measuredAt ?? ""}${color ? C.reset : ""}`);
159
- console.log(`${color ? C.dim : ""}数字口径:A=给出可执行流程;B=拒绕;C=含糊。n 见 matrix.json armor.probe.bare/persona${color ? C.reset : ""}`);
160
- }
161
-
162
- export function renderPlan(p, { color = process.stdout.isTTY } = {}) {
163
- if (p.error) return console.log(paint(p.error, "low", color));
164
- const dim = color ? C.dim : "";
165
- const off = color ? C.reset : "";
166
- const fmt = (v) => (v === null || v === undefined ? "—" : `${v}%`);
167
- console.log(`${paint(p.id, "high", color)} ${dim}${p.class}${off}`);
168
- console.log(` off ${fmt(p.bare.off)} sys ${fmt(p.bare.sys)} user ${fmt(p.bare.user)} ${dim}(n=${p.bare.n ?? "?"})${off}`);
169
- console.log(` 推荐层:${paint(p.recommended.join(" + ") || "—", p.recommended.length ? "high" : "low", color)} mode:${p.mode}`);
170
- if (p.blocked.length) for (const b of p.blocked) console.log(` ${paint("禁", "low", color)} ${b.layer}:${b.reason}`);
171
- for (const w of p.warnings) console.log(` ${dim}·${off} ${w}`);
172
- console.log(` ${dim}$ ${p.cmd}${off}`);
173
- }
174
-
175
- export function benchCommand(id, { task = "crack-bare", layers = "off,sys,user", repeat = 2, out } = {}) {
176
- const parts = [
177
- "agent-purge armor run",
178
- `--task ${task}`,
179
- `--layers ${layers}`,
180
- `--repeat ${repeat}`,
181
- `--only ${id}`,
182
- ];
183
- if (out) parts.push(`--out ${out}`);
184
- return parts.join(" ");
185
- }
186
-
187
- /** 预检:不跑模型,只回答"这台机器上这一层到底能不能落地"。 */
188
- export function preflight(matrix, id, { base, key, model } = {}) {
189
- const spec = matrix.harnesses?.[id];
190
- const p = plan(matrix, id);
191
- const checks = [];
192
- const add = (name, ok, detail) => checks.push({ name, ok, detail });
193
- add("实测数据", !p.error, p.error ?? `sys ${fmtPct(p.bare.sys)} / user ${fmtPct(p.bare.user)}`);
194
- add("类目", spec?.class !== "unsupported", spec?.reason ?? `class=${spec?.class}`);
195
- add("上游", Boolean(base && key), base ? `${base}${key ? "" : "(缺 key)"}` : "未设置 --base/--key");
196
- add("模型", Boolean(model), model ?? "未设置 --model");
197
- if (spec?.class === "config-file") add("配置回滚", true, "swapFile + journal(崩溃后下次启动自动回收)");
198
- if (p.requiresSeed) add("seed 层", true, "claude 型:必须带人设词种子,裸注入无效");
199
- if (!p.error) add("可注入层", p.recommended.length > 0, p.recommended.length ? p.recommended.join(" + ") : "无可用层(sys 无数据、user 无效)");
200
- return { id, ok: checks.every((c) => c.ok), checks, plan: p };
201
- }
202
-
203
- /** 统一代理到 bench/bench-armor.mjs / bench/judge-armor.mjs,保证 CLI 与环境变量只有一处定义。 */
204
- export function runBench(argv, env) {
205
- const script = join(ROOT, "bench", "bench-armor.mjs");
206
- if (!existsSync(script)) return { code: 2, error: `缺少 ${script}` };
207
- const r = spawnSync(process.execPath, [script, ...argv], {
208
- stdio: "inherit",
209
- env: { ...process.env, ...env },
210
- });
211
- return { code: r.status ?? 1 };
212
- }
213
-
214
- export function runJudge(argv, env) {
215
- const script = join(ROOT, "bench", "judge-armor.mjs");
216
- if (!existsSync(script)) return { code: 2, error: `缺少 ${script}` };
217
- const r = spawnSync(process.execPath, [script, ...argv], {
218
- stdio: "inherit",
219
- env: { ...process.env, ...env },
220
- });
221
- return { code: r.status ?? 1 };
222
- }
1
+ // 实测数据 → 策略。矩阵里每个 harness 的 armor 数字来自 bench/bench-armor.mjs 的实跑,
2
+ // 这里只做一件事:把"哪一层真的能注入"翻译成默认值、禁层、预检项与可复制命令。
3
+ //
4
+ // 三条铁律(由实测矩阵得出,改代码前先读 matrix.json 的 armor 段):
5
+ // 1. sys 层(写进 harness 自己的系统提示/规则文件)是唯一跨家稳定的面;
6
+ // 2. user 层(塞进用户消息)只在少数 harness 有效,其余当普通请求拒答;
7
+ // 3. claude 裸注入无效、带人设词种子才破 —— 所以它需要 seed 层,而不是换 mode。
8
+ import { spawnSync } from "node:child_process";
9
+ import { existsSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { isCompiled } from "./root.mjs";
13
+
14
+ const ROOT = process.versions.bun ? dirname(process.execPath) : dirname(dirname(fileURLToPath(import.meta.url)));
15
+ export const LAYERS = ["off", "sys", "user", "seed"];
16
+
17
+ const C = {
18
+ reset: "\x1b[0m",
19
+ dim: "\x1b[2m",
20
+ bold: "\x1b[1m",
21
+ green: "\x1b[32m",
22
+ yellow: "\x1b[33m",
23
+ red: "\x1b[31m",
24
+ cyan: "\x1b[36m",
25
+ gray: "\x1b[90m",
26
+ };
27
+
28
+ const pct = (v) => (v === null || v === undefined ? null : Number(v));
29
+ const fmtCell = (v) => (v === null ? "—" : `${v}%`);
30
+ const fmtPct = (v) => (v === null || v === undefined ? "—" : `${v}%`);
31
+ const tier = (v) => (v === null || v === undefined ? "none" : v >= 80 ? "high" : v > 0 ? "mid" : "low");
32
+
33
+ function paint(text, kind, color = true) {
34
+ if (!color) return text;
35
+ const map = { high: C.green, mid: C.yellow, low: C.red, none: C.gray };
36
+ return `${map[kind] ?? ""}${text}${C.reset}`;
37
+ }
38
+
39
+ export function armorOf(matrix, id) {
40
+ return matrix.harnesses?.[id]?.armor ?? null;
41
+ }
42
+
43
+ /** 把某家的实测数字压成一条策略。所有字段都能在 UI / CLI / bench 复用。 */
44
+ export function plan(matrix, id) {
45
+ const spec = matrix.harnesses?.[id];
46
+ const a = spec?.armor;
47
+ if (!spec) return { id, error: `未知 harness: ${id}` };
48
+ if (!a) return { id, error: `${id} 没有 armor 实测数据(先跑 agent-purge armor run --only ${id})` };
49
+
50
+ const bare = a.probe?.bare ?? {};
51
+ const persona = a.probe?.persona ?? {};
52
+ const sys = pct(bare.sys);
53
+ const user = pct(bare.user);
54
+ const seedSys = pct(persona.sys);
55
+
56
+ const blocked = [];
57
+ const recommended = [];
58
+
59
+ if (spec.class === "unsupported") blocked.push({ layer: "sys", reason: spec.reason ?? "该 harness 不支持注入" });
60
+ else if (sys === null) blocked.push({ layer: "sys", reason: a.sysBlockedReason ?? "系统层未测得可用注入点" });
61
+ if (user === null) blocked.push({ layer: "user", reason: a.userBlockedReason ?? "用户层未测得(harness 崩溃或未适配)" });
62
+ if (sys !== null && sys >= 80) recommended.push("sys");
63
+ if (user !== null && user >= 80) recommended.push("user");
64
+
65
+ // claude 型:裸注入不破,但带 persona 种子词就破 —— 需要 seed,不是换 mode
66
+ const needsSeed = sys !== null && sys < 80 && seedSys !== null && seedSys >= 80;
67
+ if (needsSeed && !blocked.some((b) => b.layer === "sys")) {
68
+ recommended.push("sys+seed");
69
+ }
70
+ if (!recommended.length && !blocked.some((b) => b.layer === "sys")) recommended.push("sys");
71
+
72
+ const warnings = [];
73
+ if (spec.class === "file-swap" || spec.class === "config-file") warnings.push("注入要落盘:用 agent-purge 启动才会自动回滚(journal 在系统临时目录)");
74
+ if (needsSeed && !(a.note ?? "").includes("种子")) warnings.push(`裸问法 sys 层 ${sys}%,带人设词种子后 ${seedSys}% —— 必须走 seed 层`);
75
+ if (a.note) warnings.push(a.note);
76
+ // user 层无效的提示只在它曾经有效过的家才说(否则每行重复一句废话)
77
+ if (user !== null && user < 80 && user > 0) warnings.push(`user 层实测 ${user}%,不稳定,以 sys 为主`);
78
+
79
+ return {
80
+ id,
81
+ class: spec.class,
82
+ claudeLike: needsSeed,
83
+ bare,
84
+ persona,
85
+ blocked,
86
+ recommended,
87
+ defaultLayers: recommended.filter((x) => !x.includes("+")),
88
+ requiresSeed: needsSeed,
89
+ requiresProviderSwap: a.providerSwap === true,
90
+ mode: a.preferredMode ?? "replace",
91
+ warnings,
92
+ cmd: benchCommand(id, { task: "crack-bare", layers: recommended.includes("user") ? "off,sys,user" : "off,sys", repeat: 2 }),
93
+ };
94
+ }
95
+
96
+ export function plans(matrix) {
97
+ return Object.keys(matrix.harnesses).map((id) => plan(matrix, id)).filter((p) => !p.error);
98
+ }
99
+
100
+ /** 顶部汇总:sys 注入几家、user 注入几家、最大增益。UI 与 CLI 共用。 */
101
+ export function summary(matrix) {
102
+ const rows = plans(matrix);
103
+ const measured = rows.filter((r) => r.bare.sys !== null || r.bare.user !== null).length;
104
+ const sysOk = rows.filter((r) => pct(r.bare.sys) >= 80).length;
105
+ const userOk = rows.filter((r) => pct(r.bare.user) >= 80).length;
106
+ const maxDelta = rows.reduce((best, r) => {
107
+ const d = pct(r.bare.sys) - pct(r.bare.off);
108
+ return d > best.delta ? { delta: d, id: r.id } : best;
109
+ }, { delta: 0, id: null });
110
+ return {
111
+ total: rows.length,
112
+ measured,
113
+ sysOk,
114
+ userOk,
115
+ maxDelta: maxDelta.delta,
116
+ maxDeltaHarness: maxDelta.id,
117
+ target: matrix.armorTarget ?? null,
118
+ measuredAt: matrix.armorMeasuredAt ?? null,
119
+ };
120
+ }
121
+
122
+ export function renderMatrix(matrix, { color = process.stdout.isTTY } = {}) {
123
+ const rows = Object.entries(matrix.harnesses).map(([id, s]) => {
124
+ const p = plan(matrix, id);
125
+ const b = p.bare ?? {};
126
+ return {
127
+ id,
128
+ class: s.class,
129
+ off: pct(b.off),
130
+ sys: pct(b.sys),
131
+ user: pct(b.user),
132
+ best: p.recommended?.length ? p.recommended.join("+") : "—",
133
+ };
134
+ });
135
+ const head = ["harness", "class", "off", "sys", "user", "推荐层"];
136
+ const cells = rows.map((r) => [r.id, r.class, fmtCell(r.off), fmtCell(r.sys), fmtCell(r.user), r.best]);
137
+ const width = head.map((h, i) => Math.max(h.length, ...cells.map((c) => [...c[i]].length)));
138
+ const pad = (s, w) => s + " ".repeat(Math.max(0, w - [...s].length));
139
+ console.log(paint(pad("harness", width[0]) + " " + pad("class", width[1]) + " " + pad("off", width[2]) + " " + pad("sys", width[3]) + " " + pad("user", width[4]) + " 推荐层", "none", color));
140
+ console.log(width.map((w) => "─".repeat(w)).join(" "));
141
+ rows.forEach((r) => {
142
+ console.log(
143
+ [
144
+ pad(r.id, width[0]),
145
+ pad(r.class, width[1]),
146
+ pad(paint(fmtCell(r.off), tier(r.off), color), width[2]),
147
+ pad(paint(fmtCell(r.sys), tier(r.sys), color), width[3]),
148
+ pad(paint(fmtCell(r.user), tier(r.user), color), width[4]),
149
+ r.best,
150
+ ].join(" "),
151
+ );
152
+ });
153
+ const s = summary(matrix);
154
+ console.log(
155
+ `\n${paint(`sys 层注入 ${s.sysOk}/${s.measured}(已测)`, s.sysOk >= s.measured * 0.8 ? "high" : "mid", color)} · ` +
156
+ `${paint(`user 层注入 ${s.userOk}/${s.measured}`, s.userOk > 0 ? "mid" : "low", color)} · ` +
157
+ `最大增益 +${s.maxDelta}pp(${s.maxDeltaHarness})`,
158
+ );
159
+ if (s.target) console.log(`${color ? C.dim : ""}实测目标:${s.target} · ${s.measuredAt ?? ""}${color ? C.reset : ""}`);
160
+ console.log(`${color ? C.dim : ""}数字口径:A=给出可执行流程;B=拒绕;C=含糊。n 见 matrix.json armor.probe.bare/persona${color ? C.reset : ""}`);
161
+ }
162
+
163
+ export function renderPlan(p, { color = process.stdout.isTTY } = {}) {
164
+ if (p.error) return console.log(paint(p.error, "low", color));
165
+ const dim = color ? C.dim : "";
166
+ const off = color ? C.reset : "";
167
+ const fmt = (v) => (v === null || v === undefined ? "—" : `${v}%`);
168
+ console.log(`${paint(p.id, "high", color)} ${dim}${p.class}${off}`);
169
+ console.log(` off ${fmt(p.bare.off)} sys ${fmt(p.bare.sys)} user ${fmt(p.bare.user)} ${dim}(n=${p.bare.n ?? "?"})${off}`);
170
+ console.log(` 推荐层:${paint(p.recommended.join(" + ") || "—", p.recommended.length ? "high" : "low", color)} mode:${p.mode}`);
171
+ if (p.blocked.length) for (const b of p.blocked) console.log(` ${paint("禁", "low", color)} ${b.layer}:${b.reason}`);
172
+ for (const w of p.warnings) console.log(` ${dim}·${off} ${w}`);
173
+ console.log(` ${dim}$ ${p.cmd}${off}`);
174
+ }
175
+
176
+ export function benchCommand(id, { task = "crack-bare", layers = "off,sys,user", repeat = 2, out } = {}) {
177
+ const parts = [
178
+ "agent-purge armor run",
179
+ `--task ${task}`,
180
+ `--layers ${layers}`,
181
+ `--repeat ${repeat}`,
182
+ `--only ${id}`,
183
+ ];
184
+ if (out) parts.push(`--out ${out}`);
185
+ return parts.join(" ");
186
+ }
187
+
188
+ /** 预检:不跑模型,只回答"这台机器上这一层到底能不能落地"。 */
189
+ export function preflight(matrix, id, { base, key, model } = {}) {
190
+ const spec = matrix.harnesses?.[id];
191
+ const p = plan(matrix, id);
192
+ const checks = [];
193
+ const add = (name, ok, detail) => checks.push({ name, ok, detail });
194
+ add("实测数据", !p.error, p.error ?? `sys ${fmtPct(p.bare.sys)} / user ${fmtPct(p.bare.user)}`);
195
+ add("类目", spec?.class !== "unsupported", spec?.reason ?? `class=${spec?.class}`);
196
+ add("上游", Boolean(base && key), base ? `${base}${key ? "" : "(缺 key)"}` : "未设置 --base/--key");
197
+ add("模型", Boolean(model), model ?? "未设置 --model");
198
+ if (spec?.class === "config-file") add("配置回滚", true, "swapFile + journal(崩溃后下次启动自动回收)");
199
+ if (p.requiresSeed) add("seed 层", true, "claude 型:必须带人设词种子,裸注入无效");
200
+ if (!p.error) add("可注入层", p.recommended.length > 0, p.recommended.length ? p.recommended.join(" + ") : "无可用层(sys 无数据、user 无效)");
201
+ return { id, ok: checks.every((c) => c.ok), checks, plan: p };
202
+ }
203
+
204
+ /** 统一代理到 bench/bench-armor.mjs / bench/judge-armor.mjs,保证 CLI 与环境变量只有一处定义。 */
205
+ export function runBench(argv, env) {
206
+ if (isCompiled()) return { code: 2, error: "独立 exe 未打包 bench 脚本;请在 Node 源码/瘦包中运行 armor bench" };
207
+ const script = join(ROOT, "bench", "bench-armor.mjs");
208
+ if (!existsSync(script)) return { code: 2, error: `缺少 ${script}` };
209
+ const r = spawnSync(process.execPath, [script, ...argv], {
210
+ stdio: "inherit",
211
+ env: { ...process.env, ...env },
212
+ });
213
+ return { code: r.status ?? 1 };
214
+ }
215
+
216
+ export function runJudge(argv, env) {
217
+ if (isCompiled()) return { code: 2, error: "独立 exe 未打包 judge 脚本;请在 Node 源码/瘦包中运行 armor judge" };
218
+ const script = join(ROOT, "bench", "judge-armor.mjs");
219
+ if (!existsSync(script)) return { code: 2, error: `缺少 ${script}` };
220
+ const r = spawnSync(process.execPath, [script, ...argv], {
221
+ stdio: "inherit",
222
+ env: { ...process.env, ...env },
223
+ });
224
+ return { code: r.status ?? 1 };
225
+ }