dsh-rule-engine 0.5.12 → 0.5.14

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.
@@ -1,150 +1,221 @@
1
- // understander.js - 规则理解器(模式库兜底版)。
2
- // 输入 parser 解析出的规则,输出结构化执行配置。
3
- // LLM 理解器可在后续版本接入 ctx.llm,当前先保证确定性与可测试性。
4
- import { extractElements } from "./parser.js";
5
- import {
6
- BOM_WRITE,
7
- DESTRUCTIVE_CMD,
8
- DSH_KEYWORDS_RE,
9
- INLINE_CMD,
10
- SENSITIVE_CMD,
11
- TIME_WORDS,
12
- PROMISE_WORDS,
13
- URL_RE,
14
- SOURCE_MARK
15
- } from "./patterns.js";
16
-
17
- const HANDLER_BY_RULE = {
18
- 1: "rule1-retry",
19
- 2: "rule2-time",
20
- 5: "rule5-source",
21
- 7: "rule7-promise",
22
- 9: "rule9-inline-bom",
23
- 11: "rule11-language",
24
- "12A": "rule12a-approval",
25
- "12B": "rule12b-skill",
26
- "12C": "rule12c-network",
27
- "13A": "rule13a-backup",
28
- // 13B 已于 2026-08-24 外移至手册(规则正文删除,映射一并清理,防死映射)
29
- // 14 为纯 D 级自证规则,无对应 handler(0.5.11:删除空转映射 registry)
30
- 18: "rule18-manual-first",
31
- 21: "rule21-meta",
32
- 22: "rule22-7-direct",
33
- 23: "rule23-runtime-verify",
34
- 24: "rule24-assembly-type",
35
- 26: "rule26-release-asset",
36
- 27: "rule27-mount-audit"
37
- };
38
-
39
- /**
40
- * 覆盖自省(一致性预防机制):
41
- * - dead:HANDLER_BY_RULE 映射了但 AGENTS.md 无此规则 ID(死映射,应清理)
42
- * - uncovered:A/C/M(deny/ask/meta)级但无 handler 的规则(规则声称会被机器执行,实际不会)
43
- * /guard status 与一致性测试/脚本中使用;规则 21④/19⑦ 的体检触发条件之一。
44
- */
45
- export function analyzeCoverage(configs) {
46
- const dead = new Map(); // ruleId -> handler(映射存在但规则不存在)
47
- const uncovered = []; // A/C/M 级规则但引擎无执行 handler
48
- const present = new Set();
49
- for (const cfg of configs || []) {
50
- if (cfg && cfg.ruleId !== undefined && cfg.ruleId !== null) present.add(String(cfg.ruleId));
51
- }
52
- for (const [id, handler] of Object.entries(HANDLER_BY_RULE)) {
53
- if (!present.has(String(id))) dead.set(String(id), handler);
54
- }
55
- for (const cfg of configs || []) {
56
- if (cfg.disabled) continue;
57
- const acts = cfg.actions || [];
58
- const hard = acts.some((a) => a === "deny" || a === "ask" || a === "meta");
59
- if (hard && !cfg.handler) {
60
- uncovered.push({ ruleId: String(cfg.ruleId), title: cfg.title, actions: acts });
61
- }
62
- }
63
- return { dead, uncovered };
64
- }
65
-
66
- /** 根据执行等级推导动作(容忍空白/强弱/连接符:如 "B + D"、"A 弱 + D"、"D 强") */
67
- export function actionsForLevel(level) {
68
- const s = String(level || "").toUpperCase().replace(/\s+/g, "");
69
- const actions = [];
70
- if (s.includes("A")) actions.push("deny");
71
- if (s.includes("B")) actions.push("correct");
72
- if (s.includes("C")) actions.push("ask");
73
- if (s.includes("D")) actions.push("self-certify");
74
- if (s.includes("M")) actions.push("meta");
75
- if (actions.length === 0) actions.push("self-certify");
76
- return [...new Set(actions)];
77
- }
78
-
79
- function splitKeywords(text) {
80
- if (!text) return [];
81
- return text
82
- .split(/[,。;、,\n;::/\\()()]+/)
83
- .map((s) => s.trim())
84
- .filter((s) => s.length >= 2 && s.length <= 20);
85
- }
86
-
87
- /** 从检查文本提取机器可用的提示词 */
88
- function hintPatterns(checkText) {
89
- const hints = [];
90
- if (/node\s+-e|node\s+-p|pwsh\s+-c|--eval|--print|-Command\b/i.test(checkText)) hints.push("inline-command");
91
- if (/set-content|out-file|add-content|writealltext|utf8bom/i.test(checkText)) hints.push("bom-write");
92
- if (/ask_user_question|授权|弹框/i.test(checkText)) hints.push("ask");
93
- if (/get-date|时间词|昨天|今天/i.test(checkText)) hints.push("time");
94
- if (/skill|技能/i.test(checkText)) hints.push("skill");
95
- if (/git\s+(push|commit)|敏感操作|授权/i.test(checkText)) hints.push("sensitive");
96
- if (/删除|覆盖|备份|验证/i.test(checkText)) hints.push("backup");
97
- if (/手册/i.test(checkText)) hints.push("manual");
98
- if (/重试|连续失败|第\s*3\s*次/i.test(checkText)) hints.push("retry");
99
- if (/运行时验证|mock|启动|实测/i.test(checkText)) hints.push("runtime-verify");
100
- if (/URL|来源|出处|引用/i.test(checkText)) hints.push("source");
101
- if (/中文|英文|语言/i.test(checkText)) hints.push("language");
102
- return [...new Set(hints)];
103
- }
104
-
105
- /**
106
- * 理解一条规则。
107
- * @param {object} rule parser 输出
108
- * @returns {object} 执行配置
109
- */
110
- export function understandRule(rule) {
111
- const elems = extractElements(rule.body || "");
112
- const level = rule.level || "";
113
- const actions = actionsForLevel(level);
114
- const hints = hintPatterns(elems.check);
115
- const triggerKeywords = splitKeywords(elems.trigger);
116
- const confidence = level && elems.trigger && elems.check && (elems.action || level.toUpperCase().includes("D")) ? "high" : level ? "medium" : "low";
117
- const handler = HANDLER_BY_RULE[rule.index] || "";
118
- const config = {
119
- ruleId: rule.index,
120
- title: rule.title,
121
- section: rule.section,
122
- level,
123
- actions,
124
- confidence,
125
- handler,
126
- triggerKeywords,
127
- hints,
128
- elements: elems,
129
- disabled: false
130
- };
131
- return config;
132
- }
133
-
134
- /** 批量理解 */
135
- export function understandAll(rules) {
136
- return rules.map(understandRule);
137
- }
138
-
139
- /** 导出常用正则供测试/调试 */
140
- export const REGEX = {
141
- INLINE_CMD,
142
- BOM_WRITE,
143
- DESTRUCTIVE_CMD,
144
- SENSITIVE_CMD,
145
- TIME_WORDS,
146
- PROMISE_WORDS,
147
- URL_RE,
148
- SOURCE_MARK,
149
- DSH_KEYWORDS_RE
150
- };
1
+ // understander.js - 规则理解器(模式库兜底版)。
2
+ // 输入 parser 解析出的规则,输出结构化执行配置。
3
+ // LLM 理解器可在后续版本接入 ctx.llm,当前先保证确定性与可测试性。
4
+ import { extractElements, levelFromTitle } from "./parser.js";
5
+ import {
6
+ BOM_WRITE,
7
+ DESTRUCTIVE_CMD,
8
+ DSH_KEYWORDS_RE,
9
+ INLINE_CMD,
10
+ SENSITIVE_CMD,
11
+ TIME_WORDS,
12
+ PROMISE_WORDS,
13
+ URL_RE,
14
+ SOURCE_MARK
15
+ } from "./patterns.js";
16
+
17
+ // 2026-08-31(残余1 剥离,用户定稿):本机默认偏好表从代码下沉到本机配置——
18
+ // rule-engine.json 的 handlerDefaultMap(可选键,可整体替换/清空)。代码层默认表为空
19
+ // (通用版=零本机编号);本机偏好经 state 注入(understandRule/understandAll 的 opts.defaultMap)。
20
+
21
+ /**
22
+ * 覆盖自省(一致性预防机制):
23
+ * - dead:defaultMap 映射了但 AGENTS.md 无此规则 ID(死映射,应清理)
24
+ * - uncovered:A/C/M(deny/ask/meta)级但无 handler 的规则(规则声称会被机器执行,实际不会)
25
+ * 在 /guard status 与一致性测试/脚本中使用;规则 21④/19⑦ 的体检触发条件之一。
26
+ * 剥离后 defaultMap 由调用方传入(本机=配置表;通用=空表);缺省空表 → dead 恒 0。
27
+ * @param {object} configs 理解产物
28
+ * @param {object} [defaultMap] 生效的默认偏好表({ruleId: handlerName})
29
+ */
30
+ export function analyzeCoverage(configs, defaultMap = {}) {
31
+ const dead = new Map(); // ruleId -> handler(映射存在但规则不存在)
32
+ const uncovered = []; // A/C/M 级规则但引擎无执行 handler
33
+ const present = new Set();
34
+ for (const cfg of configs || []) {
35
+ if (cfg && cfg.ruleId !== undefined && cfg.ruleId !== null) present.add(String(cfg.ruleId));
36
+ }
37
+ for (const [id, handler] of Object.entries(defaultMap || {})) {
38
+ if (!present.has(String(id))) dead.set(String(id), handler);
39
+ }
40
+ for (const cfg of configs || []) {
41
+ if (cfg.disabled) continue;
42
+ const acts = cfg.actions || [];
43
+ const hard = acts.some((a) => a === "deny" || a === "ask" || a === "meta");
44
+ if (hard && !cfg.handler) {
45
+ uncovered.push({ ruleId: String(cfg.ruleId), title: cfg.title, actions: acts });
46
+ }
47
+ }
48
+ return { dead, uncovered };
49
+ }
50
+
51
+ /** 根据执行等级推导动作(容忍空白/强弱/连接符:如 "B + D"、"A 弱 + D"、"D 强") */
52
+ export function actionsForLevel(level) {
53
+ const s = String(level || "").toUpperCase().replace(/\s+/g, "");
54
+ const actions = [];
55
+ if (s.includes("A")) actions.push("deny");
56
+ if (s.includes("B")) actions.push("correct");
57
+ if (s.includes("C")) actions.push("ask");
58
+ if (s.includes("D")) actions.push("self-certify");
59
+ if (s.includes("M")) actions.push("meta");
60
+ if (actions.length === 0) actions.push("self-certify");
61
+ return [...new Set(actions)];
62
+ }
63
+
64
+ function splitKeywords(text) {
65
+ if (!text) return [];
66
+ return text
67
+ .split(/[,。;、,\n;::/\\()()]+/)
68
+ .map((s) => s.trim())
69
+ .filter((s) => s.length >= 2 && s.length <= 20);
70
+ }
71
+
72
+ /** 从检查文本提取机器可用的提示词 */
73
+ function hintPatterns(checkText) {
74
+ const hints = [];
75
+ if (/node\s+-e|node\s+-p|pwsh\s+-c|--eval|--print|-Command\b/i.test(checkText)) hints.push("inline-command");
76
+ if (/set-content|out-file|add-content|writealltext|utf8bom/i.test(checkText)) hints.push("bom-write");
77
+ if (/ask_user_question|授权|弹框/i.test(checkText)) hints.push("ask");
78
+ if (/get-date|时间词|昨天|今天/i.test(checkText)) hints.push("time");
79
+ if (/skill|技能/i.test(checkText)) hints.push("skill");
80
+ if (/git\s+(push|commit)|敏感操作|授权/i.test(checkText)) hints.push("sensitive");
81
+ if (/删除|覆盖|备份|验证/i.test(checkText)) hints.push("backup");
82
+ if (/手册/i.test(checkText)) hints.push("manual");
83
+ if (/重试|连续失败|第\s*3\s*次/i.test(checkText)) hints.push("retry");
84
+ if (/运行时验证|mock|启动|实测/i.test(checkText)) hints.push("runtime-verify");
85
+ if (/URL|来源|出处|引用/i.test(checkText)) hints.push("source");
86
+ if (/中文|英文|语言/i.test(checkText)) hints.push("language");
87
+ return [...new Set(hints)];
88
+ }
89
+
90
+ /** 规则正文内联 handler 声明(T3):`<!-- handler: rule22-7-direct -->` */
91
+ const HANDLER_DECL_RE = /<!--\s*handler\s*:\s*([a-z0-9][a-z0-9-]*)\s*-->/i;
92
+
93
+ /**
94
+ * 执行器语义别名(残余3 通用化,2026-08-31):
95
+ * 内部执行器名(rule12a-approval 等)带规则编号影子——陌生用户声明时不应见到。
96
+ * 语义名 → 内部名映射:声明/配置写语义名即归一;直接写内部名不受影响(别名表无该键)。
97
+ * 归一发生在 resolveHandler 末端;guard-core 分支(按内部名)零改动。
98
+ */
99
+ const HANDLER_ALIASES = {
100
+ retry: "rule1-retry",
101
+ time: "rule2-time",
102
+ source: "rule5-source",
103
+ promise: "rule7-promise",
104
+ "inline-command": "rule9-inline-bom",
105
+ language: "rule11-language",
106
+ approval: "rule12a-approval",
107
+ "skill-auth": "rule12b-skill",
108
+ network: "rule12c-network",
109
+ backup: "rule13a-backup",
110
+ "manual-first": "rule18-manual-first",
111
+ meta: "rule21-meta",
112
+ "intent-direct": "rule22-7-direct",
113
+ "runtime-verify": "rule23-runtime-verify",
114
+ assembly: "rule24-assembly-type",
115
+ "release-asset": "rule26-release-asset",
116
+ "mount-audit": "rule27-mount-audit"
117
+ };
118
+
119
+ /** 语义名归一(内部名原样返回;未知名保持原样——由覆盖自省提示未覆盖) */
120
+ export function normalizeHandlerName(name) {
121
+ const s = String(name || "");
122
+ return HANDLER_ALIASES[s] || s;
123
+ }
124
+
125
+ /**
126
+ * handler 绑定解析(T3 通用化,2026-08-31)——优先级:
127
+ * ① opts.handlerOverrides[ruleId] (rule-engine.json 配置覆盖,集中管理)
128
+ * ② 规则正文内联声明 <!-- handler: xxx --> (随规则走,可声明任意已实现执行器)
129
+ * ③ opts.defaultMap || HANDLER_BY_RULE (本机默认偏好表;defaultMap 可整体替换)
130
+ * ④ "" (未绑定=纯自证规则,README「局限 4」分流——规则仍参与匹配/自证,不参与硬拦)
131
+ * 所有来源经 normalizeHandlerName 归一(语义名/内部名均可;本机零行为变化)。
132
+ */
133
+ export function resolveHandler(rule, opts = {}) {
134
+ const id = String(rule?.index ?? "");
135
+ const overrides = opts?.handlerOverrides;
136
+ if (overrides && typeof overrides === "object" && overrides[id] !== undefined) return normalizeHandlerName(overrides[id]);
137
+ const body = String(rule?.body || "");
138
+ const decl = body.match(HANDLER_DECL_RE);
139
+ if (decl) return normalizeHandlerName(decl[1]);
140
+ // 剥离后(2026-08-31):代码层无默认表;defaultMap 由调用方注入(本机=配置 handlerDefaultMap)
141
+ const map = opts?.defaultMap || {};
142
+ return normalizeHandlerName(map[rule?.index] || "");
143
+ }
144
+
145
+ /**
146
+ * 理解一条规则。
147
+ * @param {object} rule parser 输出
148
+ * @param {object} [opts] 绑定选项(handlerOverrides/defaultMap,见 resolveHandler)
149
+ * @returns {object} 执行配置
150
+ */
151
+ export function understandRule(rule, opts = {}) {
152
+ const elems = extractElements(rule.body || "");
153
+ const level = rule.level || "";
154
+ const actions = actionsForLevel(level);
155
+ const hints = hintPatterns(elems.check);
156
+ const triggerKeywords = splitKeywords(elems.trigger);
157
+ const confidence = level && elems.trigger && elems.check && (elems.action || level.toUpperCase().includes("D")) ? "high" : level ? "medium" : "low";
158
+ const handler = resolveHandler(rule, opts);
159
+ const config = {
160
+ ruleId: rule.index,
161
+ title: rule.title,
162
+ section: rule.section,
163
+ level,
164
+ actions,
165
+ confidence,
166
+ handler,
167
+ triggerKeywords,
168
+ hints,
169
+ elements: elems,
170
+ disabled: false
171
+ };
172
+ return config;
173
+ }
174
+
175
+ /** 批量理解。opts:
176
+ * - disabledEntries: dsh-rules-manager 禁用存档条目(数组)。正文已移走、不在 rules 中的
177
+ * 禁用规则用存档正文重建占位 cfg(disabled=true)——禁用=存在但休眠,恢复自动生效。
178
+ * 在理解层重建保证所有调用方(state/consistency-live/运行时)口径一致。
179
+ * - handlerOverrides: { ruleId: handlerName } 声明式绑定覆盖(T3,通用化通道)。
180
+ * - defaultMap: 覆盖 HANDLER_BY_RULE 默认偏好表(T3;缺省=本机默认表)。
181
+ */
182
+ export function understandAll(rules, opts = {}) {
183
+ const configs = (rules || []).map((r) => understandRule(r, opts));
184
+ const entries = opts?.disabledEntries;
185
+ if (Array.isArray(entries) && entries.length) {
186
+ const disabledIds = new Set(entries.map((d) => String(d.index)).filter(Boolean));
187
+ // 在场禁用规则:只标 disabled(保留 AGENTS.md 正文,不被存档覆盖)
188
+ for (const cfg of configs) {
189
+ if (disabledIds.has(String(cfg.ruleId))) cfg.disabled = true;
190
+ }
191
+ // 缺场禁用规则:用存档正文重建占位(正文已移走的规则不再"消失")
192
+ const present = new Set(configs.map((c) => String(c.ruleId)));
193
+ for (const entry of entries) {
194
+ const key = String(entry.index);
195
+ if (present.has(key)) continue;
196
+ const cfg = understandRule({
197
+ index: entry.index,
198
+ title: entry.title || "",
199
+ section: entry.section || "",
200
+ level: levelFromTitle(entry.header || entry.title || ""),
201
+ body: entry.body || ""
202
+ }, opts);
203
+ cfg.disabled = true;
204
+ configs.push(cfg);
205
+ }
206
+ }
207
+ return configs;
208
+ }
209
+
210
+ /** 导出常用正则供测试/调试 */
211
+ export const REGEX = {
212
+ INLINE_CMD,
213
+ BOM_WRITE,
214
+ DESTRUCTIVE_CMD,
215
+ SENSITIVE_CMD,
216
+ TIME_WORDS,
217
+ PROMISE_WORDS,
218
+ URL_RE,
219
+ SOURCE_MARK,
220
+ DSH_KEYWORDS_RE
221
+ };
package/lib/index.js CHANGED
@@ -101,6 +101,11 @@ export const inject = ["tools", "commands", "agents", "workspaceRegistry", "skil
101
101
  const pluginConfig = loadPluginConfig();
102
102
  state.enabled = pluginConfig.enabled;
103
103
  applyTaskContractConfig(state, pluginConfig);
104
+ // T3 通用化(2026-08-31):声明式绑定覆盖表注入(rule-engine.json 可选键 handlerOverrides;缺省 {})
105
+ state.handlerOverrides = pluginConfig?.handlerOverrides || {};
106
+ // 残余1 剥离(2026-08-31):本机默认偏好表注入(rule-engine.json 可选键 handlerDefaultMap;
107
+ // 通用部署=空表——代码零本机编号,本机偏好不随包走、升级不丢)
108
+ state.handlerDefaultMap = pluginConfig?.handlerDefaultMap || {};
104
109
  // reloadRules 内部已统一刷新理解产物(P0-3),此处不再重复写
105
110
  reloadRules(state);
106
111
 
@@ -157,6 +162,23 @@ function sessionIdOfExec(exec) {
157
162
  return "global";
158
163
  }
159
164
 
165
+ /**
166
+ * 从 /guard 命令 invocation 取会话 id(2026-08-31 会话寻址修复)。
167
+ * 官方 CommandInvocation 无 session 字段(只有 commandId/agent/rawInput/attachments/signal),
168
+ * 旧代码 invocation?.session?.id 恒为 undefined → 永远落到 "global"(仅模板),而当前会话的
169
+ * contract 在创建时已快照、不随 global 变化 —— 这是"/guard mode change 放行无效"的根因。
170
+ * 官方类型:Agent.id 与 session.id 是同一身份("The single identity shared with session"),
171
+ * 收到命令的代理 = 命令发起的会话;裁决侧 sessionIdOfExec 取 exec.agent.session.id —— 两者对齐。
172
+ */
173
+ export function sessionIdOfInvocation(invocation) {
174
+ const agent = invocation?.agent;
175
+ if (!agent) return "global";
176
+ if (typeof agent.id === "string" && agent.id) return agent.id;
177
+ if (typeof agent.session === "object" && agent.session?.id) return agent.session.id;
178
+ if (typeof agent.session === "string") return agent.session;
179
+ return "global";
180
+ }
181
+
160
182
  /**
161
183
  * 从 user/message 事件提取用户文本。
162
184
  * 2026-08-24 根因修复:官方结构(@deepseek-ai/dsh-session/surface)中
@@ -617,6 +639,8 @@ export function handleSessionEvent(ctx, session, event) {
617
639
  }
618
640
  }
619
641
  if (isManualReadTool(toolName, args)) s.manualReadSeen = true;
642
+ // 规则 5/31 扩展(2026-09-01):查询类工具调用记下回合号("近 3 回合有据"判定)
643
+ if (isReadOnlyTool(toolName, args)) s.lastQueryTurn = s.turn.number;
620
644
  if (toolName === "skill" && args?.name) s.turn.skillNames.push(args.name);
621
645
  s.turn.toolNames.push(toolName);
622
646
  if (state.taskContract?.taskContractEnabled) {
@@ -1078,23 +1102,30 @@ export function handleSessionEvent(ctx, session, event) {
1078
1102
 
1079
1103
  // ── /guard 命令 ─────────────────────────────────────────────────────────────
1080
1104
 
1105
+ // ── /guard 子命令单一真源(0.5.13:hint 手写漏 tools 修复)──
1106
+ // hint(输入框提示)与 USAGE(完整帮助)均由此派生:未来新增子命令只改这一个数组。
1107
+ const COMMAND_SPECS = [
1108
+ { name: "status", args: "", desc: "引擎状态" },
1109
+ { name: "rules", args: "", desc: "规则清单 + 理解产物" },
1110
+ { name: "active", args: "", desc: "最近激活的规则" },
1111
+ { name: "log", args: "[N]", desc: "最近 N 条审计(默认 10)" },
1112
+ { name: "unlock", args: "[N]", desc: "解锁配置写保护 N 分钟(默认 10,仅用户)" },
1113
+ { name: "bypass", args: "[N]", desc: "临时整体放行 N 分钟(默认 5,仅用户)" },
1114
+ { name: "lock", args: "", desc: "立即恢复全部守卫(取消解锁/放行)" },
1115
+ { name: "revoke", args: "", desc: "撤销全部授权记录" },
1116
+ { name: "tools", args: "", desc: "工具放行白名单(永久+本会话,含时间/来源会话)" },
1117
+ { name: "tools revoke", args: "<工具名>", desc: "撤销白名单条目(持久化+会话集)" },
1118
+ { name: "reload", args: "", desc: "强制重解析 AGENTS.md" },
1119
+ { name: "mode", args: "<模式>", desc: "设置任务契约模式(review/answer/change/monitor/watch/off)" },
1120
+ { name: "budget", args: "...", desc: "设置预算(agents=N files=... deps=allow hash=allow)" },
1121
+ { name: "contract", args: "", desc: "查看当前任务契约" },
1122
+ { name: "contract categories", args: "...", desc: "设定契约类别白名单(0.5.12)" },
1123
+ { name: "label", args: "<id> <label>", desc: "给审计记录打标(correct/incorrect/inconclusive)" }
1124
+ ];
1125
+
1081
1126
  const USAGE = [
1082
1127
  "用法:",
1083
- " /guard status 引擎状态",
1084
- " /guard rules 规则清单 + 理解产物",
1085
- " /guard active 最近激活的规则",
1086
- " /guard log [N] 最近 N 条审计(默认 10)",
1087
- " /guard unlock [N] 解锁配置写保护 N 分钟(默认 10,仅用户)",
1088
- " /guard bypass [N] 临时整体放行 N 分钟(默认 5,仅用户)",
1089
- " /guard lock 立即恢复全部守卫(取消解锁/放行)",
1090
- " /guard revoke 撤销全部授权记录",
1091
- " /guard tools 工具放行白名单(永久+本会话,含时间/来源会话)",
1092
- " /guard tools revoke <工具名> 撤销白名单条目(持久化+会话集)",
1093
- " /guard reload 强制重解析 AGENTS.md",
1094
- " /guard mode <模式> 设置任务契约模式(review/answer/change/monitor/watch/off)",
1095
- " /guard budget ... 设置预算(agents=N files=... deps=allow hash=allow)",
1096
- " /guard contract 查看当前任务契约",
1097
- " /guard label <id> <label> 给审计记录打标(correct/incorrect/inconclusive)",
1128
+ ...COMMAND_SPECS.map((s) => ` /guard ${s.name}${s.args ? " " + s.args : ""} ${s.desc}`),
1098
1129
  "",
1099
1130
  "说明:",
1100
1131
  " - 守卫 = 硬拦截:违反规则的工具调用直接拒绝,模型无法自行绕过;",
@@ -1102,6 +1133,10 @@ const USAGE = [
1102
1133
  " - 每次拦截/纠察都会记录到 " + auditFilePath() + "(/guard log 可查)。"
1103
1134
  ].join("\n");
1104
1135
 
1136
+ // 输入框提示:由子命令清单自动派生(顶层命令名去重,新子命令/别名自动同步)
1137
+ // eslint-disable-next-line no-unused-vars
1138
+ const GUARD_HINT = "[" + [...new Set(COMMAND_SPECS.map((s) => s.name.split(" ")[0]))].join("|") + "]";
1139
+
1105
1140
  function parseCommand(rawInput) {
1106
1141
  const text = (rawInput || "").trim();
1107
1142
  if (!text || /^(status|state)$/i.test(text)) return { kind: "status" };
@@ -1156,11 +1191,14 @@ async function executeGuard(ctx, invocation) {
1156
1191
  const medium = state.configs.filter((c) => c.confidence === "medium").length;
1157
1192
  const low = state.configs.filter((c) => c.confidence === "low").length;
1158
1193
  // 覆盖自省(一致性预防):dead = 映射有但规则无;uncovered = 硬等级规则但无 handler
1159
- const { dead, uncovered } = analyzeCoverage(state.configs);
1194
+ const { dead, uncovered } = analyzeCoverage(state.configs, state.handlerDefaultMap || {});
1160
1195
  const parts = [
1161
1196
  "【规则引擎状态】",
1162
1197
  ` 总开关:${state.enabled ? "开启" : "已关闭"}`,
1163
1198
  ` 任务契约:${state.taskContract?.taskContractEnabled ? `开启(模式 ${state.taskContract.taskContractMode}|ask ${state.taskContract.askEnabled ? "开" : "关"})` : "关闭"}`,
1199
+ // 2026-08-31:本会话真实契约(mode/level/预算)——档位(面板)≠ 本会话模式,两者分开展示
1200
+ // (旧版只显示档位:用户 "armed" 误以为已放行,实际裁决按会话 mode 走)
1201
+ ` 本会话契约:${state.taskContract?.taskContractEnabled ? `${contractSummary(getSessionState(state, sessionIdOfInvocation(invocation)).contract)}(会话 ${sessionIdOfInvocation(invocation)})` : "关闭(总开关未开,命令不生效)"}`,
1164
1202
  ` 规则容器:${state.configOk ? `正常(${state.configs.length} 条规则)` : "⚠ " + state.configError}`,
1165
1203
  ` 理解置信度:high ${high} / medium ${medium} / low ${low}`,
1166
1204
  ` 覆盖自省:${dead.size === 0 && uncovered.length === 0 ? "无缺口" : (dead.size > 0 ? `死映射 ${[...dead.keys()].join("、")}(引擎映射存在但规则已删除,建议清理)` : "") + (uncovered.length > 0 ? `;未覆盖 ${uncovered.map((u) => u.ruleId).join("、")}(${uncovered.map((u) => u.actions.join("+")).join(";")} 但无 handler,规则声明强制但引擎不执行,建议降 D 级或补实现)` : "")}`,
@@ -1199,7 +1237,7 @@ async function executeGuard(ctx, invocation) {
1199
1237
  const minutes = Math.min(Math.max(1, command.minutes), 60);
1200
1238
  state.unlockUntil = Date.now() + minutes * 60000;
1201
1239
  // 逃生门请求审计(机制批 M1):用途留痕,供规则 1⑤ 事前说明核对
1202
- audit({ kind: "guard-command", rule: "__escape-gate", name: "逃生门请求", event: "command", reason: `/guard unlock ${minutes} 分钟(配置写保护)`, session: invocation?.session?.id || "global" });
1240
+ audit({ kind: "guard-command", rule: "__escape-gate", name: "逃生门请求", event: "command", reason: `/guard unlock ${minutes} 分钟(配置写保护)`, session: sessionIdOfInvocation(invocation) });
1203
1241
  return {
1204
1242
  kind: "success",
1205
1243
  text: `已解锁「配置写保护」${minutes} 分钟。现在可以让助手修改 rule-engine.json / rule-understanding.json / AGENTS.md;改完请执行 /guard lock 或等待自动恢复。`
@@ -1209,7 +1247,7 @@ async function executeGuard(ctx, invocation) {
1209
1247
  const minutes = Math.min(Math.max(1, command.minutes), 60);
1210
1248
  state.bypassUntil = Date.now() + minutes * 60000;
1211
1249
  // 逃生门请求审计(机制批 M1)
1212
- audit({ kind: "guard-command", rule: "__escape-gate", name: "逃生门请求", event: "command", reason: `/guard bypass ${minutes} 分钟(全部守卫)`, session: invocation?.session?.id || "global" });
1250
+ audit({ kind: "guard-command", rule: "__escape-gate", name: "逃生门请求", event: "command", reason: `/guard bypass ${minutes} 分钟(全部守卫)`, session: sessionIdOfInvocation(invocation) });
1213
1251
  return {
1214
1252
  kind: "success",
1215
1253
  text: `已临时放行全部守卫 ${minutes} 分钟。到期自动恢复,也可 /guard reload 后立即恢复。`
@@ -1230,7 +1268,7 @@ async function executeGuard(ctx, invocation) {
1230
1268
  name: "撤销全部授权",
1231
1269
  event: "command",
1232
1270
  reason: `撤销 ${count} 条授权/范围(session + turn.scopes + global + askRejections 全清)`,
1233
- session: invocation?.session?.id || "global"
1271
+ session: sessionIdOfInvocation(invocation)
1234
1272
  });
1235
1273
  return { kind: "success", text: `已撤销全部授权与范围(共 ${count} 条,含全局池与 ask 拒绝记录;不再有残留)。` };
1236
1274
  }
@@ -1275,7 +1313,7 @@ async function executeGuard(ctx, invocation) {
1275
1313
  name: "白名单撤销",
1276
1314
  event: "command",
1277
1315
  reason: `/guard tools revoke ${target}(持久化+会话集同步移除)`,
1278
- session: invocation?.session?.id || "global"
1316
+ session: sessionIdOfInvocation(invocation)
1279
1317
  });
1280
1318
  return { kind: "success", text: `已撤销 ${target}${existed ? "" : "(原不在白名单)"}。` };
1281
1319
  }
@@ -1296,7 +1334,7 @@ async function executeGuard(ctx, invocation) {
1296
1334
  }
1297
1335
  case "mode": {
1298
1336
  if (!state.taskContract?.taskContractEnabled) return { kind: "error", text: "任务契约未启用:请先在规则引擎设置页开启总开关。" };
1299
- const sid = invocation?.session?.id || "global";
1337
+ const sid = sessionIdOfInvocation(invocation);
1300
1338
  const s = getSessionState(state, sid);
1301
1339
  const res = applyContract(s.contract, { mode: command.mode, level: command.level, source: "guard-command" });
1302
1340
  if (res.changed) {
@@ -1307,7 +1345,7 @@ async function executeGuard(ctx, invocation) {
1307
1345
  }
1308
1346
  case "budget": {
1309
1347
  if (!state.taskContract?.taskContractEnabled) return { kind: "error", text: "任务契约未启用:请先在规则引擎设置页开启总开关。" };
1310
- const sid = invocation?.session?.id || "global";
1348
+ const sid = sessionIdOfInvocation(invocation);
1311
1349
  const s = getSessionState(state, sid);
1312
1350
  const res = applyContract(s.contract, { ...command.patch, source: "guard-command" });
1313
1351
  if (res.changed) {
@@ -1317,14 +1355,14 @@ async function executeGuard(ctx, invocation) {
1317
1355
  return { kind: "success", text: `任务契约:${contractSummary(s.contract)}` };
1318
1356
  }
1319
1357
  case "contract": {
1320
- const sid = invocation?.session?.id || "global";
1358
+ const sid = sessionIdOfInvocation(invocation);
1321
1359
  const s = getSessionState(state, sid);
1322
1360
  const enabled = state.taskContract?.taskContractEnabled ? "开启" : "关闭(总开关未开启,命令不生效)";
1323
1361
  return { kind: "success", text: `任务契约(总开关:${enabled})\n${contractSummary(s.contract)}` };
1324
1362
  }
1325
1363
  case "contract-categories": {
1326
1364
  if (!state.taskContract?.taskContractEnabled) return { kind: "error", text: "任务契约未启用:请先在规则引擎设置页开启总开关。" };
1327
- const sid = invocation?.session?.id || "global";
1365
+ const sid = sessionIdOfInvocation(invocation);
1328
1366
  const s = getSessionState(state, sid);
1329
1367
  const cats = command.categories;
1330
1368
  if (cats.length === 0) {
@@ -1360,7 +1398,7 @@ async function executeGuard(ctx, invocation) {
1360
1398
  try {
1361
1399
  const fp = labelFingerprint(JSON.parse(found.args || "{}").command || "");
1362
1400
  if (fp) {
1363
- const sid = invocation?.session?.id || "global";
1401
+ const sid = sessionIdOfInvocation(invocation);
1364
1402
  state.labelRows = upsertLabel(state.labelRows || [], labelEntry(fp, "incorrect", sid));
1365
1403
  saveLabelsToDisk(labelsFilePath(dshHome()), state.labelRows);
1366
1404
  audit({ kind: "task-label", rule: "__task-contract", name: "审计人工标注", event: "command", reason: `${found.eventId} = ${command.label}(来源 ERR-${found.errId || "?"})+ 命令指纹放行已记录:${fp.slice(0, 100)}`, session: sid });
@@ -1370,7 +1408,7 @@ async function executeGuard(ctx, invocation) {
1370
1408
  // 指纹解析失败不阻断打标(保守:只记单条,不记指纹)
1371
1409
  }
1372
1410
  }
1373
- audit({ kind: "task-label", rule: "__task-contract", name: "审计人工标注", event: "command", reason: `${found.eventId} = ${command.label}(来源 ERR-${found.errId || "?"})`, session: invocation?.session?.id || "global" });
1411
+ audit({ kind: "task-label", rule: "__task-contract", name: "审计人工标注", event: "command", reason: `${found.eventId} = ${command.label}(来源 ERR-${found.errId || "?"})`, session: sessionIdOfInvocation(invocation) });
1374
1412
  return { kind: "success", text: `已标注 ${found.eventId} = ${command.label}` };
1375
1413
  }
1376
1414
  case "label-clear": {
@@ -1378,7 +1416,7 @@ async function executeGuard(ctx, invocation) {
1378
1416
  const before = (state.labelRows || []).length;
1379
1417
  state.labelRows = (state.labelRows || []).filter((r) => r.fingerprint !== fp);
1380
1418
  saveLabelsToDisk(labelsFilePath(dshHome()), state.labelRows);
1381
- audit({ kind: "task-label", rule: "__task-contract", name: "打标指纹撤销", event: "command", reason: `撤销命令指纹:${fp}(${before} → ${state.labelRows.length} 条)`, session: invocation?.session?.id || "global" });
1419
+ audit({ kind: "task-label", rule: "__task-contract", name: "打标指纹撤销", event: "command", reason: `撤销命令指纹:${fp}(${before} → ${state.labelRows.length} 条)`, session: sessionIdOfInvocation(invocation) });
1382
1420
  return { kind: "success", text: `已撤销命令指纹 ${fp}(剩余 ${state.labelRows.length} 条)` };
1383
1421
  }
1384
1422
  default:
@@ -1732,7 +1770,7 @@ export function apply(ctx) {
1732
1770
  yield ctx.commands.register({
1733
1771
  name: "guard",
1734
1772
  description: "规则执行引擎:查看状态/规则/激活/审计,解锁配置修改,临时放行,强制重载",
1735
- input: { hint: "[status|rules|active|log <N>|unlock <分钟>|bypass <分钟>|reload]" },
1773
+ input: { hint: GUARD_HINT },
1736
1774
  handler: async (invocation) => {
1737
1775
  try {
1738
1776
  return await executeGuard(ctx, invocation);