pi-verdict 0.3.0 → 0.4.0

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/README.md CHANGED
@@ -37,6 +37,7 @@ pi --extension ./extensions/auto-mode.ts
37
37
  - `/automode` — show current status: on/off + shadow-cache stats for the session
38
38
  - `/automode on`
39
39
  - `/automode off`
40
+ - `ctrl+shift+a` — toggle the master switch silently (the always-on footer is the only feedback; rebind or disable via `toggleShortcut`)
40
41
  - footer always shows `auto mode on` (highlighted) / `auto mode off` (dimmed)
41
42
 
42
43
  | Option | Default | Description |
@@ -54,7 +55,8 @@ pi --extension ./extensions/auto-mode.ts
54
55
  "allow": ["^ls\\b", "^git (status|log|diff)\\b"],
55
56
  "deny": ["rm ", "docker ", "^/etc/"],
56
57
  "builtinDenyFloor": true,
57
- "classifierModel": null
58
+ "classifierModel": null,
59
+ "toggleShortcut": "ctrl+shift+a"
58
60
  }
59
61
  ```
60
62
 
@@ -63,9 +65,10 @@ pi --extension ./extensions/auto-mode.ts
63
65
  - `builtinDenyFloor: false` turns the built-in danger/path floor off entirely (risk accepted by you; the classifier and your rules remain — the self-protection layer below always stays on)
64
66
  - `classifierModel: "provider/model-id"` sets the classifier model (e.g. a fast flash-class model); precedence is flag > env > config > session model (self-reflection); an invalid value falls back to the session model with a one-time warning
65
67
  - the spec accepts pi's native `--model` thinking suffix: `"zai/glm-5.3-flash:low"` sets classifier thinking to effort low (default without suffix: thinking explicitly off — the [measured](research/thinking-param-blackhole.md) default)
68
+ - `toggleShortcut` rebinds the master-switch toggle key (any pi key combo, e.g. `ctrl+shift+x`; `null` or empty disables the shortcut; an invalid combo warns once at session start and skips registration). The toggle is semantically identical to `/automode on|off` — works mid-run, no confirmation, never persisted (persistence stays with the `--no-auto-mode` flag; the extension never writes its own protected config)
66
69
  - first run generates a template at `~/.pi/agent/config/pi-verdict.json` (honors `PI_CODING_AGENT_DIR`); changes apply to new sessions
67
70
 
68
- Why no built-in allowlist? A third-party security audit ([`research/rule-layer-security-audit.md`](research/rule-layer-security-audit.md)) showed that allowlist soundness requires shell AST analysis — every built-in "always allow" would be a security claim maintained by the author. The built-in layer only makes **deny** claims (the sound direction); allow claims are yours.
71
+ Why no built-in allowlist? Bypass testing of the rule layer ([writeup](research/rule-layer-security-audit.md)) showed that allowlist soundness requires shell AST analysis — every built-in "always allow" would be a security claim maintained by the author. The built-in layer only makes **deny** claims (the sound direction); allow claims are yours.
69
72
 
70
73
  ### Self-protection (the gate guards itself — [ADR-0001](docs/adr/0001-self-protection-layer.md))
71
74
 
@@ -141,7 +144,7 @@ Design decisions here are settled by measurement, and the lab notes ship with th
141
144
  - [`research/thinking-param-blackhole.md`](research/thinking-param-blackhole.md) — three-layer forensic root-cause of thinking models burning the classifier budget; why the fix is `thinkingEnabled: false`
142
145
  - [`research/rule-engine-sim`](research/rule-engine-sim/README.md) — measured a tree-sitter rule-engine port against 746 real bash calls (**absorbs 0 gray calls**) and rejected it
143
146
  - [`research/pi-permission-landscape.md`](research/pi-permission-landscape.md) — the competitive landscape this README's positioning is checked against
144
- - [`research/rule-layer-security-audit.md`](research/rule-layer-security-audit.md) — third-party audit of the rule layer (8/8 reproduced → fixed architecturally in 0.2.0)
147
+ - [`research/rule-layer-security-audit.md`](research/rule-layer-security-audit.md) — rule-layer bypass testing (8/8 reproduced → fixed architecturally in 0.2.0)
145
148
  - [`research/pi-automode-convergence.md`](research/pi-automode-convergence.md) — where this project genuinely converges with pi-automode, and what remains distinct
146
149
  - [`research/claude-code-classifier-prompts.md`](research/claude-code-classifier-prompts.md) — structural reconstruction of Claude Code's classifier design (via self-hosted Langfuse observations) that this extension's transcript contract descends from
147
150
 
@@ -149,7 +152,7 @@ Design decisions here are settled by measurement, and the lab notes ship with th
149
152
 
150
153
  Prototype quality — usable, not hardened:
151
154
 
152
- - no built-in allowlist by design (see the [security audit](research/rule-layer-security-audit.md)); with an empty `allow` config most commands go to the classifier — point `--auto-mode-model` at a fast model if per-call latency matters
155
+ - no built-in allowlist by design (see the [bypass writeup](research/rule-layer-security-audit.md)); with an empty `allow` config most commands go to the classifier — point `--auto-mode-model` at a fast model if per-call latency matters
153
156
  - AGENTS.md is not passed to the classifier as downweighted intent evidence (Claude Code does this)
154
157
  - parallel gray-zone calls are adjudicated serially
155
158
  - self-reflection means the session model adjudicates — point `--auto-mode-model` at a lighter model if verdict latency/cost matters (open question tracked in the issue tracker)
@@ -166,7 +169,7 @@ The name: the three-state **verdict** is the core concept. The UX keeps `/automo
166
169
  ```bash
167
170
  bun install
168
171
  bun run typecheck
169
- bun test # 61 offline stub tests: self-protection, tamper detection, deny floor, user rules, audit regression, classifier retry, shadow cache, commands
172
+ bun test # 69 offline stub tests: self-protection, tamper detection, deny floor, user rules, bypass regression, classifier retry, shadow cache, commands, toggle shortcut
170
173
  ```
171
174
 
172
175
  Issue tracker and decision records live in the GitHub issues ("map" issue #1 indexes them).
package/README.zh-CN.md CHANGED
@@ -37,6 +37,7 @@ pi --extension ./extensions/auto-mode.ts
37
37
  - `/automode` —— 显示当前状态:开/关 + 本会话影子缓存统计
38
38
  - `/automode on`
39
39
  - `/automode off`
40
+ - `ctrl+shift+a` —— 静默切换主开关(footer 始终显示为唯一反馈;键位可经 `toggleShortcut` 重绑或禁用)
40
41
  - footer 恒显 `auto mode on`(高亮)/ `auto mode off`(暗色)
41
42
 
42
43
  | 配置 | 默认 | 说明 |
@@ -54,7 +55,8 @@ pi --extension ./extensions/auto-mode.ts
54
55
  "allow": ["^ls\\b", "^git (status|log|diff)\\b"],
55
56
  "deny": ["rm ", "docker ", "^/etc/"],
56
57
  "builtinDenyFloor": true,
57
- "classifierModel": null
58
+ "classifierModel": null,
59
+ "toggleShortcut": "ctrl+shift+a"
58
60
  }
59
61
  ```
60
62
 
@@ -63,9 +65,10 @@ pi --extension ./extensions/auto-mode.ts
63
65
  - `builtinDenyFloor: false` 可整体关闭内置危险/路径拦截(风险自担;分类器与你的规则仍在——下方自保护层永远开启)
64
66
  - `classifierModel: "provider/model-id"` 指定分类器模型(如轻量 flash 类);优先级 flag > env > config > 自省;无效值回退会话模型并一次性警告
65
67
  - spec 支持 pi 原生 `--model` 思考级别后缀:`"zai/glm-5.3-flash:low"` 将分类器思考设为 effort low(无后缀缺省 = 显式关思考,[实测](research/thinking-param-blackhole.md)背书的默认)
68
+ - `toggleShortcut` 重绑主开关快捷键(任意 pi 键组合,如 `ctrl+shift+x`;`null` 或空串禁用;非法组合在会话启动时一次性警告并跳过注册)。快捷键与 `/automode on|off` **语义等价**——运行中生效、无确认弹窗、不持久化(持久需求由 `--no-auto-mode` flag 承担;扩展运行时从不写自己的受保护配置)
66
69
  - 首次运行自动生成模板 `~/.pi/agent/config/pi-verdict.json`(尊重 `PI_CODING_AGENT_DIR`);修改后新会话生效
67
70
 
68
- 为什么没有内置白名单?第三方安全审计(见 [`research/rule-layer-security-audit.md`](research/rule-layer-security-audit.md))证明白名单的健全性需要 shell AST 分析——每条内置「永远放行」都是作者维护的安全声明。因此内置层只做 **deny** 声明(方向健全),allow 声明归你。
71
+ 为什么没有内置白名单?对规则层的绕过测试(见 [`research/rule-layer-security-audit.md`](research/rule-layer-security-audit.md))证明白名单的健全性需要 shell AST 分析——每条内置「永远放行」都是作者维护的安全声明。因此内置层只做 **deny** 声明(方向健全),allow 声明归你。
69
72
 
70
73
  ### 自保护(门禁守护自身——[ADR-0001](docs/adr/0001-self-protection-layer.md))
71
74
 
@@ -140,7 +143,7 @@ tool_call
140
143
  - [`research/thinking-param-blackhole.md`](research/thinking-param-blackhole.md) —— 思考模型烧尽分类器预算的三层取证,以及为什么修复是 `thinkingEnabled: false`
141
144
  - [`research/rule-engine-sim`](research/rule-engine-sim/README.md) —— 用 746 条真实 bash 调用实测 tree-sitter 规则引擎移植(**灰区吸收 0 条**)并否决
142
145
  - [`research/pi-permission-landscape.md`](research/pi-permission-landscape.md) —— 本 README 定位所对照的竞品全景
143
- - [`research/rule-layer-security-audit.md`](research/rule-layer-security-audit.md) —— 规则层第三方安全审计(8/8 复现 → 0.2.0 架构性修复)
146
+ - [`research/rule-layer-security-audit.md`](research/rule-layer-security-audit.md) —— 规则层绕过测试(8/8 复现 → 0.2.0 架构性修复)
144
147
  - [`research/pi-automode-convergence.md`](research/pi-automode-convergence.md) —— 与 pi-automode 何处真正收敛、何处仍然不同
145
148
  - [`research/claude-code-classifier-prompts.md`](research/claude-code-classifier-prompts.md) —— Claude Code 分类器设计的结构化还原(基于自托管 Langfuse 观测),本扩展 transcript 契约的血统来源
146
149
 
@@ -148,7 +151,7 @@ tool_call
148
151
 
149
152
  原型质量 —— 可用,未硬化:
150
153
 
151
- - 设计上无内置白名单(见[安全审计](research/rule-layer-security-audit.md)与[用户规则](#用户规则configpi-verdictjson));allow 配置为空时大多数命令进分类器 —— 延迟敏感可 `--auto-mode-model` 指向轻量模型
154
+ - 设计上无内置白名单(见[绕过测试](research/rule-layer-security-audit.md)与[用户规则](#用户规则configpi-verdictjson));allow 配置为空时大多数命令进分类器 —— 延迟敏感可 `--auto-mode-model` 指向轻量模型
152
155
  - AGENTS.md 未作为降权意图证据传入分类器(Claude Code 有此设计)
153
156
  - 并行灰区调用串行裁决
154
157
  - 自省意味着会话模型亲自裁决 —— 若延迟/成本敏感,用 `--auto-mode-model` 指向轻量模型(开放问题见 issue tracker)
@@ -165,7 +168,7 @@ tool_call
165
168
  ```bash
166
169
  bun install
167
170
  bun run typecheck
168
- bun test # 61 个离线桩测试:自保护 / 变更检测 / deny floor / 用户规则 / 审计回归 / 分类器重试 / 影子缓存 / 命令
171
+ bun test # 69 个离线桩测试:自保护 / 变更检测 / deny floor / 用户规则 / 绕过回归 / 分类器重试 / 影子缓存 / 命令 / toggle 快捷键
169
172
  ```
170
173
 
171
174
  Issue tracker 与决策记录在 GitHub issues(「地图」issue #1 为索引)。
@@ -28,13 +28,15 @@
28
28
  *
29
29
  * 配置:
30
30
  * --auto-mode / --no-auto-mode CLI flag,总开关(默认开)
31
+ * ctrl+shift+a 主开关 toggle 快捷键(默认键位;静默切换,footer 始终显示为
32
+ * 唯一反馈;config 的 toggleShortcut 可改/null 禁用,新会话生效)
31
33
  * --auto-mode-model provider/id[:thinking] 分类器模型 + 可选思考级别后缀
32
34
  * (pi 原生 --model 语法;缺省 off = 显式关思考)
33
35
  * PI_AUTO_MODE_MODEL 同上的环境变量形式
34
36
  * --auto-mode-debug 所有裁决(含放行)都弹通知;影子缓存标注同步开启
35
37
  * PI_AUTO_MODE_DEBUG=1 同上的环境变量形式(兼容保留)
36
38
  * <agentDir>/config/pi-verdict.json 用户规则:{ allow: [regex], deny: [regex],
37
- * builtinDenyFloor, classifierModel }
39
+ * builtinDenyFloor, classifierModel, toggleShortcut }
38
40
  * 匹配:bash=完整命令串 / 文件工具=绝对路径;新会话生效;
39
41
  * 受自保护层保护(agent 不可改,仅用户手工编辑)
40
42
  *
@@ -105,6 +107,48 @@ function classifyBash(command: string, floorOn: boolean): RuleResult {
105
107
  // 非法正则跳过并通知(配置错误不导致扩展失效);新会话生效。
106
108
  // ============================================================================
107
109
 
110
+ // ============================================================================
111
+ // 主开关 toggle 快捷键(#15)
112
+ //
113
+ // 与 /automode 命令语义等价:同一翻转入口,不因操作面引入额外规则
114
+ // (运行中生效 / 无确认弹窗 / 无持久化写回——写回会模糊 ADR-0001 的「仅用户手编」边界)。
115
+ // 反馈静默:footer 始终显示(auto-mode 双态)是唯一反馈,不 notify。
116
+ // 键位:config 的 toggleShortcut 字段,缺省 ctrl+shift+a(与 pi 全部默认键位无冲突,
117
+ // 双修饰降误触,避开依赖 Kitty 协议的 super);null/空串禁用;新会话生效。
118
+ // ============================================================================
119
+
120
+ /** toggle 快捷键默认键位:主编辑器上下文空闲、语义好记(A for Auto)、不易误触 */
121
+ const DEFAULT_TOGGLE_SHORTCUT = "ctrl+shift+a";
122
+
123
+ /** 键名词表(功能键与特殊键;词表对齐 pi keybindings 文档) */
124
+ const KEY_NAME_ALT = "f(?:[1-9]|1[0-2])|escape|esc|enter|return|tab|space|backspace|delete|insert|clear|home|end|pageup|pagedown|up|down|left|right";
125
+ const KEY_PRINTABLE = "[a-z0-9]|[-=`\\[\\];',./!@#$%^&*()_+|~{}:<>?]";
126
+ /**
127
+ * key 组合格式校验:修饰键 ≥1(modifier+任意键),或裸键为功能/特殊键——
128
+ * 裸可打印字符(如 "a")拒绝,会劫持正常文本输入。词表对齐 pi keybindings 文档,
129
+ * 零依赖约束下不引入 pi 内部校验 API;pi 侧另有兜底:与内置键冲突自动跳过并提示。
130
+ */
131
+ const KEY_COMBO_RE = new RegExp(`^(?:(?:ctrl|shift|alt|super)\\+)+(?:${KEY_NAME_ALT}|${KEY_PRINTABLE})$|^(?:${KEY_NAME_ALT})$`, "i");
132
+
133
+ /**
134
+ * 解析配置 toggleShortcut:缺省 → 默认键位;null/空白/类型错误 → 禁用;
135
+ * 非法格式 → 禁用 + 警告文案(session_start 经 ctx 发出,对齐 skipped 正则的模式;
136
+ * 配置错误不静默失效,但也不阻止扩展其余部分工作)。
137
+ */
138
+ function resolveToggleShortcut(raw: unknown): { key: string | null; warning: string | null } {
139
+ if (raw === undefined) return { key: DEFAULT_TOGGLE_SHORTCUT, warning: null };
140
+ if (raw === null) return { key: null, warning: null };
141
+ if (typeof raw !== "string") {
142
+ return { key: null, warning: `toggleShortcut must be a pi key combo string (e.g. "${DEFAULT_TOGGLE_SHORTCUT}"), or null/empty to disable — got ${JSON.stringify(raw)}` };
143
+ }
144
+ const s = raw.trim();
145
+ if (!s) return { key: null, warning: null };
146
+ if (!KEY_COMBO_RE.test(s)) {
147
+ return { key: null, warning: `toggleShortcut "${raw}" is not a valid pi key combo (modifier+key, e.g. "${DEFAULT_TOGGLE_SHORTCUT}") — shortcut not registered; fix config/pi-verdict.json` };
148
+ }
149
+ return { key: s, warning: null };
150
+ }
151
+
108
152
  interface UserRules {
109
153
  allow: RegExp[];
110
154
  deny: RegExp[];
@@ -112,9 +156,11 @@ interface UserRules {
112
156
  builtinDenyFloor: boolean;
113
157
  /** 分类器模型 spec(provider/id);null = 未配置(自省继承会话模型) */
114
158
  classifierModel: string | null;
159
+ /** 主开关 toggle 快捷键键位(#15);null = 禁用;缺省 DEFAULT_TOGGLE_SHORTCUT */
160
+ toggleShortcut: string | null;
115
161
  }
116
162
 
117
- const EMPTY_RULES: UserRules = { allow: [], deny: [], builtinDenyFloor: true, classifierModel: null };
163
+ const EMPTY_RULES: UserRules = { allow: [], deny: [], builtinDenyFloor: true, classifierModel: null, toggleShortcut: DEFAULT_TOGGLE_SHORTCUT };
118
164
 
119
165
  function agentDirPath(): string {
120
166
  return process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
@@ -125,18 +171,20 @@ function userConfigPath(): string {
125
171
  }
126
172
 
127
173
  const USER_CONFIG_TEMPLATE = `${JSON.stringify({
128
- _hint: "pi-verdict user rules. allow/deny are JS regex arrays; deny wins over allow. Match target: bash = full command string, file tools = absolute path. builtinDenyFloor=false disables the built-in danger/path floor (at your own risk; the self-protection layer always stays on and cannot be turned off by any config). classifierModel persistently sets the classifier model (provider/id, e.g. zai/glm-5.3-flash; accepts a pi-native thinking suffix, e.g. zai/glm-5.3-flash:low; empty = self-reflection, inherit session model). This file is part of the permission gate itself: pi-verdict denies any agent-side modification of it — edit it manually outside pi. Changes apply to new sessions.",
174
+ _hint: "pi-verdict user rules. allow/deny are JS regex arrays; deny wins over allow. Match target: bash = full command string, file tools = absolute path. builtinDenyFloor=false disables the built-in danger/path floor (at your own risk; the self-protection layer always stays on and cannot be turned off by any config). classifierModel persistently sets the classifier model (provider/id, e.g. zai/glm-5.3-flash; accepts a pi-native thinking suffix, e.g. zai/glm-5.3-flash:low; empty = self-reflection, inherit session model). toggleShortcut sets the master-switch toggle key (pi key combo, e.g. ctrl+shift+a; null or empty disables the shortcut). This file is part of the permission gate itself: pi-verdict denies any agent-side modification of it — edit it manually outside pi. Changes apply to new sessions.",
129
175
  allow: ["^ls\\b"],
130
176
  deny: [],
131
177
  builtinDenyFloor: true,
132
178
  classifierModel: null,
179
+ toggleShortcut: DEFAULT_TOGGLE_SHORTCUT,
133
180
  }, null, 2)}\n`;
134
181
 
135
182
  /**
136
183
  * 加载用户规则。首启生成带注释模板(allow 内示例默认仅 ^ls\b 可用,其余为说明占位);
137
- * 配置缺失/损坏/字段非法一律回退空规则(安全默认,不失效),非法正则收集回报。
184
+ * 配置缺失/损坏/字段非法一律回退空规则(安全默认,不失效),非法正则收集回报,
185
+ * 非法 toggleShortcut 收集警告文案(与 skipped 同经 session_start 发出)。
138
186
  */
139
- function loadUserRules(): { rules: UserRules; skipped: string[] } {
187
+ function loadUserRules(): { rules: UserRules; skipped: string[]; shortcutWarning: string | null } {
140
188
  try {
141
189
  const p = userConfigPath();
142
190
  if (!fs.existsSync(p)) {
@@ -144,9 +192,9 @@ function loadUserRules(): { rules: UserRules; skipped: string[] } {
144
192
  fs.mkdirSync(path.dirname(p), { recursive: true });
145
193
  fs.writeFileSync(p, USER_CONFIG_TEMPLATE);
146
194
  } catch { /* 只读环境静默跳过 */ }
147
- return { rules: EMPTY_RULES, skipped: [] };
195
+ return { rules: EMPTY_RULES, skipped: [], shortcutWarning: null };
148
196
  }
149
- const raw = JSON.parse(fs.readFileSync(p, "utf8")) as { allow?: unknown; deny?: unknown; builtinDenyFloor?: unknown; classifierModel?: unknown };
197
+ const raw = JSON.parse(fs.readFileSync(p, "utf8")) as { allow?: unknown; deny?: unknown; builtinDenyFloor?: unknown; classifierModel?: unknown; toggleShortcut?: unknown };
150
198
  const skipped: string[] = [];
151
199
  const compile = (list: unknown): RegExp[] =>
152
200
  (Array.isArray(list) ? list : []).filter((x): x is string => typeof x === "string").flatMap((src) => {
@@ -157,17 +205,20 @@ function loadUserRules(): { rules: UserRules; skipped: string[] } {
157
205
  return [];
158
206
  }
159
207
  });
208
+ const shortcut = resolveToggleShortcut(raw.toggleShortcut);
160
209
  return {
161
210
  rules: {
162
211
  allow: compile(raw.allow),
163
212
  deny: compile(raw.deny),
164
213
  builtinDenyFloor: raw.builtinDenyFloor !== false,
165
214
  classifierModel: typeof raw.classifierModel === "string" && raw.classifierModel.trim() ? raw.classifierModel.trim() : null,
215
+ toggleShortcut: shortcut.key,
166
216
  },
167
217
  skipped,
218
+ shortcutWarning: shortcut.warning,
168
219
  };
169
220
  } catch {
170
- return { rules: EMPTY_RULES, skipped: [] };
221
+ return { rules: EMPTY_RULES, skipped: [], shortcutWarning: null };
171
222
  }
172
223
  }
173
224
 
@@ -798,6 +849,12 @@ export default function autoMode(pi: ExtensionAPI) {
798
849
  ctx.ui.setStatus("auto-mode", ctx.ui.theme.fg(enabled ? "accent" : "dim", enabled ? "auto mode on" : "auto mode off"));
799
850
  }
800
851
 
852
+ /** 主开关设定(共用,#15):/automode 命令与 toggle 快捷键同一入口,不因操作面引入额外规则 */
853
+ function setMasterSwitch(next: boolean, ctx: ExtensionContext) {
854
+ enabled = next;
855
+ refreshStatus(ctx);
856
+ }
857
+
801
858
  // session_start:重置影子缓存(会话内存态,#5 定案)+ 重载用户规则(配置改动新会话生效)
802
859
  // + 重建自保护基线(ADR-0001:受保护文件的会话启动快照)
803
860
  pi.on("session_start", async (_event, ctx) => {
@@ -809,24 +866,40 @@ export default function autoMode(pi: ExtensionAPI) {
809
866
  if (loaded.skipped.length > 0) {
810
867
  ctx.ui.notify(`pi-verdict: skipped ${loaded.skipped.length} invalid regex(es) in config (${userConfigPath()})`, "warning");
811
868
  }
869
+ if (loaded.shortcutWarning) ctx.ui.notify(`pi-verdict: ${loaded.shortcutWarning}`, "warning");
812
870
  refreshStatus(ctx);
813
871
  });
814
872
 
873
+ // 主开关 toggle 快捷键(#15):键位取首次加载的用户规则(会话内固定——改配置后
874
+ // /reload 重载扩展或新会话生效);handler 与 /automode 语义等价,静默切换,
875
+ // footer 始终显示是唯一反馈
876
+ const registeredToggleKey = userRules.toggleShortcut;
877
+ if (registeredToggleKey) {
878
+ // KeyId 是 pi 的编译期联合类型(运行时即 string);用户配置键位经 KEY_COMBO_RE
879
+ // 运行时校验后断言转入,零依赖约束下不引入 pi 内部类型路径
880
+ type PiShortcutKey = Parameters<ExtensionAPI["registerShortcut"]>[0];
881
+ pi.registerShortcut(registeredToggleKey as PiShortcutKey, {
882
+ description: "Toggle Auto Mode (pi-verdict)",
883
+ handler: (ctx) => setMasterSwitch(!enabled, ctx),
884
+ });
885
+ }
886
+ /** Usage 行的 toggle 提示(#15):无注册键位时不显示;显示注册时固定的键 */
887
+ const toggleHint = () => (registeredToggleKey ? ` · toggle: ${registeredToggleKey}` : "");
888
+
815
889
  pi.registerCommand("automode", {
816
890
  description: "Show Auto Mode status and shadow-cache stats, or set it: /automode on|off",
817
891
  handler: async (args, ctx) => {
818
892
  const arg = args.trim().toLowerCase();
819
893
  // 裸调用:只读状态展示,无副作用(含影子缓存统计行)
820
894
  if (arg === "") {
821
- ctx.ui.notify(`${enabled ? "🛡️ Auto Mode: on" : "Auto Mode: off"}\n${shadow.summary()}\nUsage: /automode on|off`, "info");
895
+ ctx.ui.notify(`${enabled ? "🛡️ Auto Mode: on" : "Auto Mode: off"}\n${shadow.summary()}\nUsage: /automode on|off${toggleHint()}`, "info");
822
896
  return;
823
897
  }
824
898
  // 幂等设定:与现值相同不翻转,仅确认
825
899
  if (arg === "on" || arg === "off") {
826
900
  const next = arg === "on";
827
901
  const changed = next !== enabled;
828
- enabled = next;
829
- refreshStatus(ctx);
902
+ setMasterSwitch(next, ctx);
830
903
  const head = next
831
904
  ? `🛡️ Auto Mode enabled${changed ? "" : " (unchanged)"}: tool calls adjudicated by rules + classifier`
832
905
  : `Auto Mode disabled${changed ? "" : " (unchanged)"}: tool calls execute directly`;
@@ -834,7 +907,7 @@ export default function autoMode(pi: ExtensionAPI) {
834
907
  return;
835
908
  }
836
909
  // 未知参数:严格拒绝并列出用法(大小写已归一化)
837
- ctx.ui.notify(`unknown argument: ${arg}\nUsage: /automode (status) | /automode on | /automode off`, "warning");
910
+ ctx.ui.notify(`unknown argument: ${arg}\nUsage: /automode (status) | /automode on | /automode off${toggleHint()}`, "warning");
838
911
  },
839
912
  });
840
913
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-verdict",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A permission gate for Pi in the style of Claude Code's auto mode",
5
5
  "author": "Jesset (https://github.com/jesset)",
6
6
  "type": "module",