pi-verdict 0.3.1 → 0.4.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/README.md CHANGED
@@ -6,12 +6,12 @@
6
6
  [![npm](https://img.shields.io/npm/v/pi-verdict)](https://www.npmjs.com/package/pi-verdict)
7
7
  [![pi extension](https://img.shields.io/badge/pi-extension-blueviolet)](https://pi.dev)
8
8
 
9
- **pi-verdict is a permission gate for [pi](https://pi.dev) in the style of Claude Code's auto mode: every tool call gets checked before it runs — allow, deny, or ask you first.**
9
+ **pi-verdict is a minimal permission gate for [pi](https://pi.dev) in the style of Claude Code's auto mode: every tool call gets checked before it runs — allow, deny, or ask you first.**
10
10
 
11
+ - Minimal — just a few hundred lines of code
11
12
  - Built-in danger rules and your own allow/deny rules settle the clear cases first, at zero latency
12
13
  - Everything else goes to a model classifier that sees the conversation context
13
14
  - Any uncertainty or failure fails closed; nothing ever runs silently
14
- - Minimal — just a few hundred lines of code
15
15
 
16
16
  ## The problem
17
17
 
@@ -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 |
@@ -47,14 +48,15 @@ pi --extension ./extensions/auto-mode.ts
47
48
  | `PI_AUTO_MODE_MODEL` | — | env form of the model flag |
48
49
  | `PI_AUTO_MODE_DEBUG=1` | off | env form of debug (flag wins) |
49
50
 
50
- ### User rules (`config/pi-verdict.json`)
51
+ ### User rules (`~/.pi/agent/config/pi-verdict.json`)
51
52
 
52
53
  ```json
53
54
  {
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? 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.
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
 
@@ -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, bypass 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
@@ -6,12 +6,12 @@
6
6
  [![npm](https://img.shields.io/npm/v/pi-verdict)](https://www.npmjs.com/package/pi-verdict)
7
7
  [![pi extension](https://img.shields.io/badge/pi-extension-blueviolet)](https://pi.dev)
8
8
 
9
- **pi-verdict 是 [pi](https://pi.dev) 的 Claude Code 的 Auto mode 式的权限门禁:每次工具调用执行前先过检查——放行、拦截,或先问你。**
9
+ **pi-verdict 是 [pi](https://pi.dev) 的 Claude Code 的 Auto mode 式的极简权限门禁:每次工具调用执行前先过检查——放行、拦截,或先问你。**
10
10
 
11
+ - 只有几百行的极简代码
11
12
  - 内置危险规则与你的 allow/deny 规则以零延迟先行裁决明确情形
12
13
  - 其余交给携带会话上下文的模型分类器
13
14
  - 任何不确定或失败一律 fail-closed, 绝不静默放行
14
- - 只有几百行的极简代码
15
15
 
16
16
  ## 问题
17
17
 
@@ -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
  | 配置 | 默认 | 说明 |
@@ -47,14 +48,15 @@ pi --extension ./extensions/auto-mode.ts
47
48
  | `PI_AUTO_MODE_MODEL` | — | 模型配置的环境变量形式 |
48
49
  | `PI_AUTO_MODE_DEBUG=1` | 关 | 调试的环境变量形式(flag 优先) |
49
50
 
50
- ### 用户自定义规则(`config/pi-verdict.json`)
51
+ ### 用户自定义规则(`~/.pi/agent/config/pi-verdict.json`)
51
52
 
52
53
  ```json
53
54
  {
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
 
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-verdict",
3
- "version": "0.3.1",
4
- "description": "A permission gate for Pi in the style of Claude Code's auto mode",
3
+ "version": "0.4.1",
4
+ "description": "A minimal 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",
7
7
  "main": "extensions/auto-mode.ts",