@shidesheng0218/agentguard 0.8.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +403 -0
  3. package/dist/cli.js +2857 -0
  4. package/package.json +60 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2857 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import fs10 from "fs";
5
+ import { Command } from "commander";
6
+
7
+ // src/version.ts
8
+ import { createRequire } from "module";
9
+ var require2 = createRequire(import.meta.url);
10
+ var pkg = require2("../package.json");
11
+ var version = pkg.version;
12
+
13
+ // src/config.ts
14
+ import fs2 from "fs";
15
+ import { parse as parseToml } from "smol-toml";
16
+
17
+ // src/paths.ts
18
+ import fs from "fs";
19
+ import os from "os";
20
+ import path from "path";
21
+ function guardHome() {
22
+ const env = process.env.AGENT_GUARD_HOME ?? process.env.KIMI_GUARD_HOME;
23
+ if (env && env.trim()) return path.resolve(env);
24
+ const fresh = path.join(os.homedir(), ".agent-guard");
25
+ const legacy = path.join(os.homedir(), ".kimi-guard");
26
+ if (!fs.existsSync(fresh) && fs.existsSync(legacy)) return legacy;
27
+ return fresh;
28
+ }
29
+ function claudeSettingsPath() {
30
+ const env = process.env.CLAUDE_SETTINGS_PATH;
31
+ if (env && env.trim()) return path.resolve(env);
32
+ return path.join(os.homedir(), ".claude", "settings.json");
33
+ }
34
+ function claudeDetected() {
35
+ return fs.existsSync(claudeSettingsPath()) || fs.existsSync(path.join(os.homedir(), ".claude"));
36
+ }
37
+ function stateDbPath() {
38
+ return path.join(guardHome(), "state.db");
39
+ }
40
+ function probeLogPath() {
41
+ return path.join(guardHome(), "probe.jsonl");
42
+ }
43
+ function userConfigPath() {
44
+ return path.join(guardHome(), "config.toml");
45
+ }
46
+ function detectKimiConfig() {
47
+ const env = process.env.KIMI_CONFIG_PATH;
48
+ if (env && env.trim()) {
49
+ const p = path.resolve(env);
50
+ return { path: p, exists: fs.existsSync(p) };
51
+ }
52
+ const candidates = [
53
+ path.join(os.homedir(), ".kimi-code", "config.toml"),
54
+ path.join(os.homedir(), ".kimi", "config.toml")
55
+ ];
56
+ for (const p of candidates) {
57
+ if (fs.existsSync(p)) return { path: p, exists: true };
58
+ }
59
+ return { path: candidates[0], exists: false };
60
+ }
61
+
62
+ // src/toolsets.ts
63
+ var DEFAULT_EDIT_TOOLS = ["WriteFile", "StrReplaceFile", "Edit", "Write", "MultiEdit", "NotebookEdit"];
64
+ var DEFAULT_READ_TOOLS = ["ReadFile", "Read"];
65
+ var DEFAULT_SEARCH_TOOLS = ["Grep", "Glob"];
66
+ var DEFAULT_SHELL_TOOLS = ["Shell", "Bash"];
67
+ var CLAUDE_EDIT_TOOLS = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
68
+ var CLAUDE_READ_TOOLS = ["Read"];
69
+ var CLAUDE_SEARCH_TOOLS = ["Grep", "Glob"];
70
+ var CLAUDE_SHELL_TOOLS = ["Bash"];
71
+ function toolDefaultsFor(harness) {
72
+ return harness === "claude" ? { edit: [...CLAUDE_EDIT_TOOLS], read: [...CLAUDE_READ_TOOLS], search: [...CLAUDE_SEARCH_TOOLS], shell: [...CLAUDE_SHELL_TOOLS] } : { edit: [...DEFAULT_EDIT_TOOLS], read: [...DEFAULT_READ_TOOLS], search: [...DEFAULT_SEARCH_TOOLS], shell: [...DEFAULT_SHELL_TOOLS] };
73
+ }
74
+ function resolve(list, fallback) {
75
+ return new Set(list.length > 0 ? list : fallback);
76
+ }
77
+ function editTools(cfg) {
78
+ return resolve(cfg.tools.edit, DEFAULT_EDIT_TOOLS);
79
+ }
80
+ function readTools(cfg) {
81
+ return resolve(cfg.tools.read, DEFAULT_READ_TOOLS);
82
+ }
83
+ function searchTools(cfg) {
84
+ return resolve(cfg.tools.search, DEFAULT_SEARCH_TOOLS);
85
+ }
86
+ function shellTools(cfg) {
87
+ return resolve(cfg.tools.shell, DEFAULT_SHELL_TOOLS);
88
+ }
89
+
90
+ // src/config.ts
91
+ var defaultConfig = {
92
+ harness: "kimi",
93
+ tools: toolDefaultsFor("kimi"),
94
+ repeat: {
95
+ enabled: true,
96
+ maxRepeats: 3,
97
+ warnAt: 2,
98
+ windowMinutes: 30,
99
+ watch: ["Grep", "Glob", "Shell", "Bash", "FetchURL", "SearchWeb", "ReadFile"],
100
+ thresholds: { ReadFile: 5 },
101
+ exemptPatterns: []
102
+ },
103
+ cycle: { enabled: true, windowMinutes: 30 },
104
+ noProgress: { enabled: true, windowMinutes: 30, warnAt: 15, blockAt: 25 },
105
+ nearRepeat: { enabled: true, windowMinutes: 30, warnAt: 6, blockAt: 10 },
106
+ explore: { enabled: true, windowMinutes: 30, warnAt: 10, blockAt: 15 },
107
+ verify: {
108
+ enabled: true,
109
+ blockOnNoEvidence: false,
110
+ evidenceWindowMinutes: 60,
111
+ claimPatterns: [],
112
+ evidencePatterns: [],
113
+ shellTools: ["Shell", "Bash"],
114
+ veto: {
115
+ enabled: false,
116
+ model: "kimi-k3",
117
+ baseUrl: "https://api.moonshot.cn/v1",
118
+ maxCallsPerSession: 3,
119
+ timeoutMs: 1e4
120
+ }
121
+ },
122
+ thinking: { enabled: true, minThinkChars: 2e4, maxTextRatio: 0.1 },
123
+ anchor: { enabled: true, everyNPrompts: 5, maxChars: 1e3 },
124
+ context: { enabled: true, warnPercent: 85 },
125
+ noGain: { enabled: true, windowMinutes: 30, warnAt: 3, blockAt: 4 },
126
+ churn: {
127
+ enabled: true,
128
+ windowMinutes: 30,
129
+ warnAt: 5,
130
+ blockAt: 10,
131
+ tools: ["WriteFile", "StrReplaceFile", "Edit", "Write", "MultiEdit", "NotebookEdit"]
132
+ },
133
+ policy: { killSwitch: true, maxBlocksPerSession: 5, blockWindowMinutes: 60 },
134
+ budget: {
135
+ enabled: true,
136
+ plan: "tier1",
137
+ weekly: 0,
138
+ fiveHour: 0,
139
+ dispatchTools: ["Task", "Agent"],
140
+ reservePercent: 10,
141
+ subagentWeight: 5,
142
+ warnPercent: 80,
143
+ precise: false,
144
+ preciseUrl: "",
145
+ preciseCacheSeconds: 300
146
+ },
147
+ probe: false
148
+ };
149
+ var CONFIG_TEMPLATE = `# agent-guard configuration
150
+ # Docs: https://github.com/shidesheng0218/kimi-guard
151
+
152
+ [tools] # canonical tool-name taxonomy \u2014 if your CLI version renames tools, fix it HERE
153
+ edit = ["WriteFile", "StrReplaceFile", "Edit", "Write", "MultiEdit", "NotebookEdit"]
154
+ read = ["ReadFile", "Read"]
155
+ search = ["Grep", "Glob"]
156
+ shell = ["Shell", "Bash"]
157
+
158
+ [repeat]
159
+ enabled = true
160
+ maxRepeats = 3 # identical (tool, args) calls allowed per window
161
+ warnAt = 2 # soft context warning before the hard block
162
+ windowMinutes = 30
163
+ watch = ["Grep", "Glob", "Shell", "Bash", "FetchURL", "SearchWeb", "ReadFile"]
164
+ # exemptPatterns = ["git status"] # regexes over JSON-serialized args; matching calls are never repeat-blocked (polling commands like git status, sleep)
165
+
166
+ [repeat.thresholds] # per-tool overrides
167
+ ReadFile = 5
168
+
169
+ [cycle] # A->B->A->B oscillation detection
170
+ enabled = true
171
+ windowMinutes = 30
172
+
173
+ [noProgress] # long stretch of calls with no successful edit
174
+ enabled = true
175
+ windowMinutes = 30
176
+ warnAt = 15
177
+ blockAt = 25
178
+
179
+ [nearRepeat] # fuzzy near-duplicates (punctuation/case/order differences)
180
+ enabled = true
181
+ windowMinutes = 30
182
+ warnAt = 6
183
+ blockAt = 10
184
+
185
+ [explore] # pure-exploration streak: reads/searches with no action in between
186
+ enabled = true
187
+ windowMinutes = 30
188
+ warnAt = 10
189
+ blockAt = 15
190
+
191
+ [verify] # completion-claim gate: "tests pass" must be backed by a real run
192
+ enabled = true
193
+ blockOnNoEvidence = false # hooks path: block Stop when edits landed but nothing was verified
194
+ evidenceWindowMinutes = 60
195
+ # deprecated: shell tool names now live in [tools] shell (this key still works)
196
+
197
+ [verify.veto] # optional LLM veto vote to suppress false positives (self-critic style)
198
+ enabled = false # requires KIMI_GUARD_VETO_API_KEY in the environment
199
+ model = "kimi-k3" # use a cheap fast model \u2014 the LLM only votes, never authors
200
+ baseUrl = "https://api.moonshot.cn/v1"
201
+ maxCallsPerSession = 3 # anti "vote-laundering" cap: the model cannot retry its way out
202
+ timeoutMs = 10000
203
+
204
+ [thinking] # thinking-dominance (pure-reasoning turns), Wire mode only
205
+ enabled = true
206
+ minThinkChars = 20000
207
+ maxTextRatio = 0.1
208
+
209
+ [anchor] # goal anchoring: re-inject the original task periodically
210
+ enabled = true
211
+ everyNPrompts = 5 # re-inject the goal every N prompts / steps
212
+ maxChars = 1000
213
+
214
+ [context] # context-fill gate (Wire mode reads StatusUpdate.context_usage)
215
+ enabled = true
216
+ warnPercent = 85 # steer a wrap-up warning when context is this full
217
+
218
+ [noGain] # different args, byte-identical output
219
+ enabled = true
220
+ windowMinutes = 30
221
+ warnAt = 3
222
+ blockAt = 4
223
+
224
+ [churn] # same file edited over and over
225
+ enabled = true
226
+ windowMinutes = 30
227
+ warnAt = 5
228
+ blockAt = 10
229
+ # deprecated: edit tool names now live in [tools] edit (this key still works)
230
+
231
+ [policy]
232
+ killSwitch = true # after maxBlocksPerSession interventions, block ALL tools
233
+ maxBlocksPerSession = 5
234
+ blockWindowMinutes = 60
235
+
236
+ [budget] # request accounting for Kimi Coding Plans
237
+ enabled = true
238
+ plan = "tier1" # tier1: 1024/week | tier2: 2048 | tier3: 7168 (200 per 5h)
239
+ weekly = 0 # override weekly requests (0 = use plan preset)
240
+ fiveHour = 0 # override 5h requests (0 = use plan preset)
241
+ dispatchTools = ["Task", "Agent"]
242
+ reservePercent = 10 # keep this much headroom for you, not the agent
243
+ subagentWeight = 5 # ~requests each dispatched subagent costs
244
+ warnPercent = 80
245
+ precise = false # poll the official Kimi usage API for exact windows (needs KIMI_API_KEY, sk-kimi-...)
246
+ preciseUrl = "" # default https://api.kimi.com/coding/v1
247
+ preciseCacheSeconds = 300 # the API is rate-limited; cache aggressively. Falls back to event-based on any error
248
+
249
+ [probe]
250
+ enabled = false
251
+ `;
252
+ function applyClaudeDefaults(cfg) {
253
+ cfg.tools = toolDefaultsFor("claude");
254
+ cfg.repeat.watch = ["Grep", "Glob", "Bash", "Read", "WebFetch", "WebSearch"];
255
+ cfg.repeat.thresholds = { Read: 5 };
256
+ cfg.budget.dispatchTools = ["Task"];
257
+ }
258
+ function num(v, fallback) {
259
+ return typeof v === "number" && Number.isFinite(v) ? v : fallback;
260
+ }
261
+ function bool(v, fallback) {
262
+ return typeof v === "boolean" ? v : fallback;
263
+ }
264
+ function strArr(v, fallback) {
265
+ return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length > 0 ? v : fallback;
266
+ }
267
+ function strArrOrNull(v) {
268
+ return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length > 0 ? v : null;
269
+ }
270
+ function loadConfig(configPath = userConfigPath(), harness = "kimi") {
271
+ const cfg = structuredClone(defaultConfig);
272
+ cfg.harness = harness;
273
+ if (harness === "claude") applyClaudeDefaults(cfg);
274
+ let raw;
275
+ try {
276
+ raw = fs2.readFileSync(configPath, "utf8");
277
+ } catch {
278
+ return cfg;
279
+ }
280
+ let data;
281
+ try {
282
+ data = parseToml(raw);
283
+ } catch (err) {
284
+ process.stderr.write(`[agent-guard] failed to parse ${configPath}: ${err.message}
285
+ `);
286
+ return cfg;
287
+ }
288
+ const section = (name) => data[name] ?? {};
289
+ const tools = section("tools");
290
+ const toolsEdit = strArrOrNull(tools["edit"]);
291
+ const toolsRead = strArrOrNull(tools["read"]);
292
+ const toolsSearch = strArrOrNull(tools["search"]);
293
+ const toolsShell = strArrOrNull(tools["shell"]);
294
+ if (toolsEdit) cfg.tools.edit = toolsEdit;
295
+ if (toolsRead) cfg.tools.read = toolsRead;
296
+ if (toolsSearch) cfg.tools.search = toolsSearch;
297
+ if (toolsShell) cfg.tools.shell = toolsShell;
298
+ const repeat = section("repeat");
299
+ cfg.repeat.enabled = bool(repeat["enabled"], cfg.repeat.enabled);
300
+ cfg.repeat.maxRepeats = num(repeat["maxRepeats"], cfg.repeat.maxRepeats);
301
+ cfg.repeat.warnAt = num(repeat["warnAt"], cfg.repeat.warnAt);
302
+ cfg.repeat.windowMinutes = num(repeat["windowMinutes"], cfg.repeat.windowMinutes);
303
+ cfg.repeat.watch = strArr(repeat["watch"], cfg.repeat.watch);
304
+ const exempt = repeat["exemptPatterns"];
305
+ if (Array.isArray(exempt)) cfg.repeat.exemptPatterns = exempt.filter((p) => typeof p === "string");
306
+ const th = repeat["thresholds"];
307
+ if (th) {
308
+ for (const [k, v] of Object.entries(th)) if (typeof v === "number") cfg.repeat.thresholds[k] = v;
309
+ }
310
+ const cycle = section("cycle");
311
+ cfg.cycle.enabled = bool(cycle["enabled"], cfg.cycle.enabled);
312
+ cfg.cycle.windowMinutes = num(cycle["windowMinutes"], cfg.cycle.windowMinutes);
313
+ const noProgress = section("noProgress");
314
+ cfg.noProgress.enabled = bool(noProgress["enabled"], cfg.noProgress.enabled);
315
+ cfg.noProgress.windowMinutes = num(noProgress["windowMinutes"], cfg.noProgress.windowMinutes);
316
+ cfg.noProgress.warnAt = num(noProgress["warnAt"], cfg.noProgress.warnAt);
317
+ cfg.noProgress.blockAt = num(noProgress["blockAt"], cfg.noProgress.blockAt);
318
+ const nearRepeat = section("nearRepeat");
319
+ cfg.nearRepeat.enabled = bool(nearRepeat["enabled"], cfg.nearRepeat.enabled);
320
+ cfg.nearRepeat.windowMinutes = num(nearRepeat["windowMinutes"], cfg.nearRepeat.windowMinutes);
321
+ cfg.nearRepeat.warnAt = num(nearRepeat["warnAt"], cfg.nearRepeat.warnAt);
322
+ cfg.nearRepeat.blockAt = num(nearRepeat["blockAt"], cfg.nearRepeat.blockAt);
323
+ const explore = section("explore");
324
+ cfg.explore.enabled = bool(explore["enabled"], cfg.explore.enabled);
325
+ cfg.explore.windowMinutes = num(explore["windowMinutes"], cfg.explore.windowMinutes);
326
+ cfg.explore.warnAt = num(explore["warnAt"], cfg.explore.warnAt);
327
+ cfg.explore.blockAt = num(explore["blockAt"], cfg.explore.blockAt);
328
+ const verify = section("verify");
329
+ cfg.verify.enabled = bool(verify["enabled"], cfg.verify.enabled);
330
+ cfg.verify.blockOnNoEvidence = bool(verify["blockOnNoEvidence"], cfg.verify.blockOnNoEvidence);
331
+ cfg.verify.evidenceWindowMinutes = num(verify["evidenceWindowMinutes"], cfg.verify.evidenceWindowMinutes);
332
+ const claims = verify["claimPatterns"];
333
+ if (Array.isArray(claims)) cfg.verify.claimPatterns = claims.filter((c) => typeof c === "string");
334
+ const evidence = verify["evidencePatterns"];
335
+ if (Array.isArray(evidence)) cfg.verify.evidencePatterns = evidence.filter((c) => typeof c === "string");
336
+ cfg.verify.shellTools = strArr(verify["shellTools"], cfg.verify.shellTools);
337
+ const legacyShellTools = strArrOrNull(verify["shellTools"]);
338
+ if (legacyShellTools && !toolsShell) cfg.tools.shell = legacyShellTools;
339
+ const veto = verify["veto"];
340
+ if (veto) {
341
+ cfg.verify.veto.enabled = bool(veto["enabled"], cfg.verify.veto.enabled);
342
+ cfg.verify.veto.model = typeof veto["model"] === "string" ? veto["model"] : cfg.verify.veto.model;
343
+ cfg.verify.veto.baseUrl = typeof veto["baseUrl"] === "string" ? veto["baseUrl"] : cfg.verify.veto.baseUrl;
344
+ cfg.verify.veto.maxCallsPerSession = num(veto["maxCallsPerSession"], cfg.verify.veto.maxCallsPerSession);
345
+ cfg.verify.veto.timeoutMs = num(veto["timeoutMs"], cfg.verify.veto.timeoutMs);
346
+ }
347
+ const thinking = section("thinking");
348
+ cfg.thinking.enabled = bool(thinking["enabled"], cfg.thinking.enabled);
349
+ cfg.thinking.minThinkChars = num(thinking["minThinkChars"], cfg.thinking.minThinkChars);
350
+ cfg.thinking.maxTextRatio = num(thinking["maxTextRatio"], cfg.thinking.maxTextRatio);
351
+ const anchor = section("anchor");
352
+ cfg.anchor.enabled = bool(anchor["enabled"], cfg.anchor.enabled);
353
+ cfg.anchor.everyNPrompts = num(anchor["everyNPrompts"], cfg.anchor.everyNPrompts);
354
+ cfg.anchor.maxChars = num(anchor["maxChars"], cfg.anchor.maxChars);
355
+ const context = section("context");
356
+ cfg.context.enabled = bool(context["enabled"], cfg.context.enabled);
357
+ cfg.context.warnPercent = num(context["warnPercent"], cfg.context.warnPercent);
358
+ const noGain = section("noGain");
359
+ cfg.noGain.enabled = bool(noGain["enabled"], cfg.noGain.enabled);
360
+ cfg.noGain.windowMinutes = num(noGain["windowMinutes"], cfg.noGain.windowMinutes);
361
+ cfg.noGain.warnAt = num(noGain["warnAt"], cfg.noGain.warnAt);
362
+ cfg.noGain.blockAt = num(noGain["blockAt"], cfg.noGain.blockAt);
363
+ const churn = section("churn");
364
+ cfg.churn.enabled = bool(churn["enabled"], cfg.churn.enabled);
365
+ cfg.churn.windowMinutes = num(churn["windowMinutes"], cfg.churn.windowMinutes);
366
+ cfg.churn.warnAt = num(churn["warnAt"], cfg.churn.warnAt);
367
+ cfg.churn.blockAt = num(churn["blockAt"], cfg.churn.blockAt);
368
+ cfg.churn.tools = strArr(churn["tools"], cfg.churn.tools);
369
+ const legacyChurnTools = strArrOrNull(churn["tools"]);
370
+ if (legacyChurnTools && !toolsEdit) cfg.tools.edit = legacyChurnTools;
371
+ const policy = section("policy");
372
+ cfg.policy.killSwitch = bool(policy["killSwitch"], cfg.policy.killSwitch);
373
+ cfg.policy.maxBlocksPerSession = num(policy["maxBlocksPerSession"], cfg.policy.maxBlocksPerSession);
374
+ cfg.policy.blockWindowMinutes = num(policy["blockWindowMinutes"], cfg.policy.blockWindowMinutes);
375
+ const budget = section("budget");
376
+ cfg.budget.enabled = bool(budget["enabled"], cfg.budget.enabled);
377
+ cfg.budget.plan = typeof budget["plan"] === "string" ? budget["plan"] : cfg.budget.plan;
378
+ cfg.budget.weekly = num(budget["weekly"], cfg.budget.weekly);
379
+ cfg.budget.fiveHour = num(budget["fiveHour"], cfg.budget.fiveHour);
380
+ cfg.budget.dispatchTools = strArr(budget["dispatchTools"], cfg.budget.dispatchTools);
381
+ cfg.budget.reservePercent = num(budget["reservePercent"], cfg.budget.reservePercent);
382
+ cfg.budget.subagentWeight = num(budget["subagentWeight"], cfg.budget.subagentWeight);
383
+ cfg.budget.warnPercent = num(budget["warnPercent"], cfg.budget.warnPercent);
384
+ cfg.budget.precise = bool(budget["precise"], cfg.budget.precise);
385
+ cfg.budget.preciseUrl = typeof budget["preciseUrl"] === "string" ? budget["preciseUrl"] : cfg.budget.preciseUrl;
386
+ cfg.budget.preciseCacheSeconds = num(budget["preciseCacheSeconds"], cfg.budget.preciseCacheSeconds);
387
+ cfg.probe = bool(section("probe")["enabled"], cfg.probe);
388
+ cfg.verify.shellTools = cfg.tools.shell;
389
+ cfg.churn.tools = cfg.tools.edit;
390
+ return cfg;
391
+ }
392
+ function writeConfigTemplate(configPath = userConfigPath()) {
393
+ if (fs2.existsSync(configPath)) return false;
394
+ fs2.mkdirSync(configPath.replace(/[/\\][^/\\]+$/, ""), { recursive: true });
395
+ fs2.writeFileSync(configPath, CONFIG_TEMPLATE, "utf8");
396
+ return true;
397
+ }
398
+
399
+ // src/installer.ts
400
+ import fs3 from "fs";
401
+ import path2 from "path";
402
+ var MANAGED_BEGIN = "# >>> kimi-guard managed >>> DO NOT EDIT";
403
+ var MANAGED_END = "# <<< kimi-guard <<<";
404
+ function hookRules(commandName = "kguard", compat = false) {
405
+ const c = (event) => `${commandName} hook ${event}`;
406
+ const base = [
407
+ { event: "PreToolUse", command: c("PreToolUse"), timeout: 5 },
408
+ { event: "PostToolUse", command: c("PostToolUse"), timeout: 5 },
409
+ { event: "PostToolUseFailure", command: c("PostToolUseFailure"), timeout: 5 }
410
+ ];
411
+ if (compat) return base;
412
+ return [
413
+ ...base,
414
+ { event: "Stop", command: c("Stop"), timeout: 5 },
415
+ { event: "TurnStarted", command: c("TurnStarted"), timeout: 5 },
416
+ { event: "SubagentStart", command: c("SubagentStart"), timeout: 5 },
417
+ { event: "StopFailure", command: c("StopFailure"), timeout: 5 },
418
+ { event: "Interrupt", command: c("Interrupt"), timeout: 5 },
419
+ { event: "SessionEnd", command: c("SessionEnd"), timeout: 5 },
420
+ { event: "PreCompact", command: c("PreCompact"), timeout: 5 },
421
+ { event: "PostCompact", command: c("PostCompact"), timeout: 5 }
422
+ ];
423
+ }
424
+ function tomlStr(s) {
425
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
426
+ }
427
+ function hookToToml(h) {
428
+ const lines = ["[[hooks]]", `event = "${tomlStr(h.event)}"`, `command = "${tomlStr(h.command)}"`];
429
+ if (h.matcher) lines.push(`matcher = "${tomlStr(h.matcher)}"`);
430
+ lines.push(`timeout = ${h.timeout}`);
431
+ return lines.join("\n");
432
+ }
433
+ function managedBlock(commandName = "kguard", compat = false) {
434
+ return [MANAGED_BEGIN, ...hookRules(commandName, compat).map(hookToToml), MANAGED_END].join("\n");
435
+ }
436
+ function findManagedBlock(text) {
437
+ const begin = text.indexOf(MANAGED_BEGIN);
438
+ if (begin < 0) return void 0;
439
+ const end = text.indexOf(MANAGED_END, begin);
440
+ if (end < 0) return void 0;
441
+ return { start: begin, end: end + MANAGED_END.length };
442
+ }
443
+ function installHooks(commandName = "kguard", compat = false) {
444
+ const { path: configPath, exists } = detectKimiConfig();
445
+ const created = !exists;
446
+ let text = exists ? fs3.readFileSync(configPath, "utf8") : "";
447
+ const block = findManagedBlock(text);
448
+ const replaced = block !== void 0;
449
+ if (exists && !replaced) {
450
+ const backupPath = `${configPath}.kimi-guard.bak`;
451
+ fs3.writeFileSync(backupPath, text, "utf8");
452
+ }
453
+ const newBlock = managedBlock(commandName, compat);
454
+ if (block) {
455
+ text = text.slice(0, block.start) + newBlock + text.slice(block.end);
456
+ } else {
457
+ const prefix = text.length === 0 ? "" : text.endsWith("\n") ? "\n" : "\n\n";
458
+ text = text + prefix + newBlock + "\n";
459
+ }
460
+ fs3.mkdirSync(path2.dirname(configPath), { recursive: true });
461
+ fs3.writeFileSync(configPath, text, "utf8");
462
+ return {
463
+ configPath,
464
+ created,
465
+ replaced,
466
+ backupPath: exists && !replaced ? `${configPath}.kimi-guard.bak` : void 0
467
+ };
468
+ }
469
+ function uninstallHooks() {
470
+ const { path: configPath, exists } = detectKimiConfig();
471
+ if (!exists) return { configPath, removed: false };
472
+ const text = fs3.readFileSync(configPath, "utf8");
473
+ const block = findManagedBlock(text);
474
+ if (!block) return { configPath, removed: false };
475
+ let out = text.slice(0, block.start) + text.slice(block.end);
476
+ out = out.replace(/^\n{2,}/, "\n");
477
+ fs3.writeFileSync(configPath, out, "utf8");
478
+ return { configPath, removed: true };
479
+ }
480
+ function hooksInstalled(configPath) {
481
+ const p = configPath ?? detectKimiConfig().path;
482
+ try {
483
+ return fs3.readFileSync(p, "utf8").includes(MANAGED_BEGIN);
484
+ } catch {
485
+ return false;
486
+ }
487
+ }
488
+
489
+ // src/harness/claude.ts
490
+ import fs4 from "fs";
491
+ import path3 from "path";
492
+ var CLAUDE_COMMAND_MARKER = "agentguard hook";
493
+ var CLAUDE_EVENTS = [
494
+ "PreToolUse",
495
+ "PostToolUse",
496
+ "PostToolUseFailure",
497
+ "UserPromptSubmit",
498
+ "Stop",
499
+ "SubagentStart",
500
+ "SessionStart",
501
+ "SessionEnd",
502
+ "PreCompact",
503
+ "PostCompact",
504
+ "StopFailure"
505
+ ];
506
+ var CLAUDE_EVENTS_COMPAT = ["PreToolUse", "PostToolUse", "PostToolUseFailure"];
507
+ function isOurs(group) {
508
+ return (group.hooks ?? []).some((h) => typeof h.command === "string" && h.command.includes(CLAUDE_COMMAND_MARKER));
509
+ }
510
+ function ourGroup(event, bin) {
511
+ return {
512
+ matcher: "",
513
+ hooks: [{ type: "command", command: `${bin} hook ${event} --harness claude`, timeout: 5 }]
514
+ };
515
+ }
516
+ function installClaudeHooks(bin = "agentguard", compat = false) {
517
+ const configPath = claudeSettingsPath();
518
+ const created = !fs4.existsSync(configPath);
519
+ let settings = {};
520
+ let backupPath;
521
+ if (!created) {
522
+ const raw = fs4.readFileSync(configPath, "utf8");
523
+ try {
524
+ settings = JSON.parse(raw);
525
+ } catch {
526
+ backupPath = `${configPath}.agentguard.bak`;
527
+ fs4.copyFileSync(configPath, backupPath);
528
+ settings = {};
529
+ }
530
+ if (!backupPath) {
531
+ backupPath = `${configPath}.agentguard.bak`;
532
+ fs4.writeFileSync(backupPath, raw, "utf8");
533
+ }
534
+ }
535
+ const events = compat ? CLAUDE_EVENTS_COMPAT : CLAUDE_EVENTS;
536
+ const hooks = settings.hooks ??= {};
537
+ let updated = false;
538
+ for (const event of events) {
539
+ const groups = (hooks[event] ?? []).filter((g) => !isOurs(g));
540
+ const before = JSON.stringify(hooks[event] ?? []);
541
+ groups.push(ourGroup(event, bin));
542
+ hooks[event] = groups;
543
+ if (JSON.stringify(groups) !== before) updated = true;
544
+ }
545
+ fs4.mkdirSync(path3.dirname(configPath), { recursive: true });
546
+ fs4.writeFileSync(configPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
547
+ return { configPath, created, updated: updated || created, backupPath: created ? void 0 : backupPath };
548
+ }
549
+ function uninstallClaudeHooks() {
550
+ const configPath = claudeSettingsPath();
551
+ if (!fs4.existsSync(configPath)) return { configPath, removed: false };
552
+ let settings;
553
+ try {
554
+ settings = JSON.parse(fs4.readFileSync(configPath, "utf8"));
555
+ } catch {
556
+ return { configPath, removed: false };
557
+ }
558
+ const hooks = settings.hooks;
559
+ if (!hooks) return { configPath, removed: false };
560
+ let removed = false;
561
+ for (const event of Object.keys(hooks)) {
562
+ const kept = hooks[event].filter((g) => !isOurs(g));
563
+ if (kept.length !== hooks[event].length) removed = true;
564
+ if (kept.length === 0) delete hooks[event];
565
+ else hooks[event] = kept;
566
+ }
567
+ if (removed) fs4.writeFileSync(configPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
568
+ return { configPath, removed };
569
+ }
570
+ function claudeHooksInstalled(configPath = claudeSettingsPath()) {
571
+ try {
572
+ const settings = JSON.parse(fs4.readFileSync(configPath, "utf8"));
573
+ return Object.values(settings.hooks ?? {}).some((groups) => groups.some(isOurs));
574
+ } catch {
575
+ return false;
576
+ }
577
+ }
578
+
579
+ // src/guard.ts
580
+ import fs7 from "fs";
581
+
582
+ // src/store.ts
583
+ import fs5 from "fs";
584
+ import path4 from "path";
585
+ import { createRequire as createRequire2 } from "module";
586
+ var nodeRequire = createRequire2(import.meta.url);
587
+ function sqliteCtor() {
588
+ return nodeRequire("node:sqlite").DatabaseSync;
589
+ }
590
+ var SCHEMA_VERSION = 3;
591
+ var SCHEMA = `
592
+ CREATE TABLE IF NOT EXISTS calls (
593
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
594
+ session_id TEXT NOT NULL,
595
+ tool_name TEXT NOT NULL,
596
+ args_hash TEXT NOT NULL,
597
+ args_json TEXT NOT NULL,
598
+ output_hash TEXT,
599
+ file_path TEXT,
600
+ status TEXT NOT NULL,
601
+ ts INTEGER NOT NULL
602
+ );
603
+ CREATE INDEX IF NOT EXISTS idx_calls_session ON calls(session_id, ts);
604
+ CREATE INDEX IF NOT EXISTS idx_calls_sig ON calls(session_id, tool_name, args_hash, ts);
605
+ CREATE INDEX IF NOT EXISTS idx_calls_out ON calls(session_id, tool_name, output_hash, ts);
606
+ CREATE INDEX IF NOT EXISTS idx_calls_time ON calls(ts);
607
+ CREATE TABLE IF NOT EXISTS events (
608
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
609
+ session_id TEXT NOT NULL,
610
+ kind TEXT NOT NULL,
611
+ meta_json TEXT NOT NULL,
612
+ ts INTEGER NOT NULL
613
+ );
614
+ CREATE INDEX IF NOT EXISTS idx_events_kind ON events(session_id, kind, ts);
615
+ CREATE INDEX IF NOT EXISTS idx_events_time ON events(ts);
616
+ CREATE TABLE IF NOT EXISTS blocks (
617
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
618
+ session_id TEXT NOT NULL,
619
+ tool_name TEXT NOT NULL,
620
+ kind TEXT NOT NULL,
621
+ ts INTEGER NOT NULL,
622
+ feedback TEXT
623
+ );
624
+ CREATE INDEX IF NOT EXISTS idx_blocks_session ON blocks(session_id, ts);
625
+ CREATE TABLE IF NOT EXISTS meta (
626
+ k TEXT PRIMARY KEY,
627
+ v TEXT NOT NULL
628
+ );
629
+ `;
630
+ var db = null;
631
+ function openDb() {
632
+ if (db) return db;
633
+ const file = stateDbPath();
634
+ fs5.mkdirSync(path4.dirname(file), { recursive: true });
635
+ const d = new (sqliteCtor())(file);
636
+ d.exec("PRAGMA journal_mode = WAL;");
637
+ migrate(d);
638
+ db = d;
639
+ return db;
640
+ }
641
+ function migrate(d) {
642
+ d.exec("CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL);");
643
+ const row = d.prepare("SELECT v FROM meta WHERE k = 'schema_version'").get();
644
+ const version2 = row ? Number(row.v) : 0;
645
+ if (version2 === SCHEMA_VERSION) {
646
+ d.exec(SCHEMA);
647
+ return;
648
+ }
649
+ if (version2 === 2) {
650
+ d.exec(SCHEMA);
651
+ try {
652
+ d.exec("ALTER TABLE blocks ADD COLUMN feedback TEXT");
653
+ } catch {
654
+ }
655
+ } else {
656
+ d.exec("DROP TABLE IF EXISTS calls; DROP TABLE IF EXISTS events; DROP TABLE IF EXISTS blocks;");
657
+ d.exec(SCHEMA);
658
+ }
659
+ d.prepare(
660
+ "INSERT INTO meta (k, v) VALUES ('schema_version', ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v"
661
+ ).run(String(SCHEMA_VERSION));
662
+ }
663
+ function recordCall(call) {
664
+ openDb().prepare(
665
+ "INSERT INTO calls (session_id, tool_name, args_hash, args_json, output_hash, file_path, status, ts) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
666
+ ).run(
667
+ call.sessionId,
668
+ call.toolName,
669
+ call.argsHash,
670
+ call.argsJson,
671
+ call.outputHash,
672
+ call.filePath,
673
+ call.status,
674
+ call.ts ?? Date.now()
675
+ );
676
+ }
677
+ function callsSince(sessionId, sinceTs, limit = 500) {
678
+ return openDb().prepare(
679
+ "SELECT tool_name, args_hash, args_json, output_hash, file_path, status, ts FROM calls WHERE session_id = ? AND ts >= ? ORDER BY ts ASC LIMIT ?"
680
+ ).all(sessionId, sinceTs, limit);
681
+ }
682
+ function recordEvent(sessionId, kind, meta, ts = Date.now()) {
683
+ openDb().prepare("INSERT INTO events (session_id, kind, meta_json, ts) VALUES (?, ?, ?, ?)").run(sessionId, kind, JSON.stringify(meta), ts);
684
+ }
685
+ function countEvents(sessionId, kinds, sinceTs) {
686
+ const placeholders = kinds.map(() => "?").join(",");
687
+ const row = openDb().prepare(
688
+ `SELECT COUNT(*) AS n FROM events WHERE session_id = ? AND kind IN (${placeholders}) AND ts >= ?`
689
+ ).get(sessionId, ...kinds, sinceTs);
690
+ return Number(row?.n ?? 0);
691
+ }
692
+ function oldestEventTs(sessionId, kinds, sinceTs) {
693
+ const placeholders = kinds.map(() => "?").join(",");
694
+ const row = openDb().prepare(
695
+ `SELECT MIN(ts) AS m FROM events WHERE session_id = ? AND kind IN (${placeholders}) AND ts >= ?`
696
+ ).get(sessionId, ...kinds, sinceTs);
697
+ return row?.m ?? null;
698
+ }
699
+ function recordBlock(sessionId, toolName, kind, ts = Date.now()) {
700
+ const info = openDb().prepare("INSERT INTO blocks (session_id, tool_name, kind, ts) VALUES (?, ?, ?, ?)").run(sessionId, toolName, kind, ts);
701
+ return Number(info.lastInsertRowid);
702
+ }
703
+ function listBlocks(limit = 20) {
704
+ return openDb().prepare("SELECT id, session_id, tool_name, kind, ts, feedback FROM blocks ORDER BY id DESC LIMIT ?").all(limit);
705
+ }
706
+ function setBlockFeedback(id, verdict) {
707
+ const info = openDb().prepare("UPDATE blocks SET feedback = ? WHERE id = ?").run(verdict, id);
708
+ return Number(info.changes) > 0;
709
+ }
710
+ function blockKindStats() {
711
+ const rows = openDb().prepare(
712
+ `SELECT kind, COUNT(*) AS n,
713
+ SUM(CASE WHEN feedback = 'fp' THEN 1 ELSE 0 END) AS fp,
714
+ SUM(CASE WHEN feedback = 'tp' THEN 1 ELSE 0 END) AS tp
715
+ FROM blocks GROUP BY kind ORDER BY n DESC`
716
+ ).all();
717
+ return rows.map((r) => ({ kind: r.kind, n: Number(r.n), fp: Number(r.fp), tp: Number(r.tp) }));
718
+ }
719
+ function countBlocks(sessionId, sinceTs) {
720
+ const row = openDb().prepare("SELECT COUNT(*) AS n FROM blocks WHERE session_id = ? AND ts >= ?").get(sessionId, sinceTs);
721
+ return Number(row?.n ?? 0);
722
+ }
723
+ function getMeta(key) {
724
+ const row = openDb().prepare("SELECT v FROM meta WHERE k = ?").get(key);
725
+ return row?.v;
726
+ }
727
+ function setMeta(key, value) {
728
+ openDb().prepare("INSERT INTO meta (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v").run(key, value);
729
+ }
730
+ function knownSessions(limit = 5) {
731
+ return openDb().prepare(
732
+ `SELECT session_id, MAX(last_ts) AS last_ts, SUM(n) AS n FROM (
733
+ SELECT session_id, MAX(ts) AS last_ts, COUNT(*) AS n FROM calls GROUP BY session_id
734
+ UNION ALL
735
+ SELECT session_id, MAX(ts) AS last_ts, COUNT(*) AS n FROM events GROUP BY session_id
736
+ ) GROUP BY session_id ORDER BY last_ts DESC LIMIT ?`
737
+ ).all(limit);
738
+ }
739
+ function buildStatus() {
740
+ const d = openDb();
741
+ const now = Date.now();
742
+ const day = now - 864e5;
743
+ const calls24h = Number(d.prepare("SELECT COUNT(*) AS n FROM calls WHERE ts >= ?").get(day).n);
744
+ const blocks24h = d.prepare("SELECT kind, COUNT(*) AS n FROM blocks WHERE ts >= ? GROUP BY kind").all(day).map((r) => ({ kind: r.kind, n: Number(r.n) }));
745
+ const topRepeated = d.prepare(
746
+ "SELECT tool_name, args_hash, COUNT(*) AS n FROM calls WHERE ts >= ? GROUP BY session_id, tool_name, args_hash ORDER BY n DESC LIMIT 5"
747
+ ).all(day).map((r) => ({ tool_name: r.tool_name, args_hash: r.args_hash, n: Number(r.n) }));
748
+ const noGainPairs = d.prepare(
749
+ "SELECT tool_name, COUNT(DISTINCT session_id || ':' || output_hash) AS n FROM calls WHERE ts >= ? AND output_hash IS NOT NULL GROUP BY tool_name ORDER BY n DESC LIMIT 5"
750
+ ).all(day).map((r) => ({ tool_name: r.tool_name, n: Number(r.n) }));
751
+ const events24h = d.prepare("SELECT kind, COUNT(*) AS n FROM events WHERE ts >= ? GROUP BY kind").all(day).map((r) => ({ kind: r.kind, n: Number(r.n) }));
752
+ const lastActivity = d.prepare("SELECT MAX(m) AS m FROM (SELECT MAX(ts) AS m FROM calls UNION ALL SELECT MAX(ts) AS m FROM events)").get();
753
+ return {
754
+ calls24h,
755
+ blocks24h,
756
+ topRepeated,
757
+ noGainPairs,
758
+ events24h,
759
+ lastActivityTs: lastActivity?.m ?? null
760
+ };
761
+ }
762
+
763
+ // src/events.ts
764
+ import { createHash } from "crypto";
765
+ function pickString(payload, keys) {
766
+ for (const k of keys) {
767
+ const v = payload[k];
768
+ if (typeof v === "string" && v) return v;
769
+ }
770
+ return "";
771
+ }
772
+ function pickField(payload, keys) {
773
+ for (const k of keys) {
774
+ if (payload[k] !== void 0) return payload[k];
775
+ }
776
+ return void 0;
777
+ }
778
+ function collapseWs(s) {
779
+ return s.replace(/\s+/g, " ").trim();
780
+ }
781
+ function sortDeep(value, collapseStrings) {
782
+ if (Array.isArray(value)) return value.map((x) => sortDeep(x, collapseStrings));
783
+ if (value !== null && typeof value === "object") {
784
+ const out = {};
785
+ for (const k of Object.keys(value).sort()) {
786
+ out[k] = sortDeep(value[k], collapseStrings);
787
+ }
788
+ return out;
789
+ }
790
+ if (value === void 0) return null;
791
+ if (typeof value === "string") return collapseStrings ? collapseWs(value) : value;
792
+ return value;
793
+ }
794
+ var WHITESPACE_SENSITIVE = /* @__PURE__ */ new Set(["Shell", "Bash", "Grep", "Glob", "FetchURL", "SearchWeb", "ReadFile", "WriteFile", "StrReplaceFile", "Edit", "Write", "MultiEdit"]);
795
+ function fingerprint(tool, args) {
796
+ let v = args ?? {};
797
+ if (v === null || typeof v !== "object") v = { value: v ?? null };
798
+ const normalized = sortDeep(v, !WHITESPACE_SENSITIVE.has(tool));
799
+ return createHash("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 16);
800
+ }
801
+ function hashOutput(output) {
802
+ if (output === void 0 || output === null) return null;
803
+ let s;
804
+ if (typeof output === "string") s = output;
805
+ else {
806
+ try {
807
+ s = JSON.stringify(sortDeep(output, false));
808
+ } catch {
809
+ s = String(output);
810
+ }
811
+ }
812
+ s = collapseWs(s).slice(0, 4096);
813
+ if (!s) return null;
814
+ return createHash("sha256").update(s).digest("hex").slice(0, 16);
815
+ }
816
+ var FILE_KEYS = ["file_path", "filePath", "path", "file", "filename", "notebook_path", "target"];
817
+ function extractFile(args) {
818
+ if (args === null || typeof args !== "object") return null;
819
+ const obj = args;
820
+ for (const k of FILE_KEYS) {
821
+ const v = obj[k];
822
+ if (typeof v === "string" && v) return v;
823
+ }
824
+ return null;
825
+ }
826
+ function normalizeCall(payload, event, ts = Date.now()) {
827
+ const sessionId = pickString(payload, ["session_id", "sessionId", "session", "sessionID"]) || "unknown";
828
+ const tool = pickString(payload, ["tool_name", "toolName", "tool"]);
829
+ if (!tool) return null;
830
+ const args = pickField(payload, ["tool_input", "toolInput", "input"]) ?? {};
831
+ const outputKeys = ["tool_output", "toolOutput", "tool_response", "output", "result"];
832
+ const output = event === "PostToolUse" ? pickField(payload, outputKeys) : event === "PostToolUseFailure" ? pickField(payload, ["error", "error_message", ...outputKeys]) : void 0;
833
+ const argsJson = JSON.stringify(args).slice(0, 2048);
834
+ return {
835
+ sessionId,
836
+ tool,
837
+ args,
838
+ argsHash: fingerprint(tool, args),
839
+ argsJson,
840
+ outputHash: output !== void 0 ? hashOutput(output) : null,
841
+ filePath: extractFile(args),
842
+ status: event === "PostToolUseFailure" ? "failure" : "ok",
843
+ ts
844
+ };
845
+ }
846
+
847
+ // src/analysis.ts
848
+ var allow = [];
849
+ function isRepeatExempt(proposed, cfg) {
850
+ if (cfg.repeat.exemptPatterns.length === 0) return false;
851
+ let text;
852
+ try {
853
+ text = JSON.stringify(proposed.args ?? {});
854
+ } catch {
855
+ text = String(proposed.args);
856
+ }
857
+ for (const p of cfg.repeat.exemptPatterns) {
858
+ try {
859
+ if (new RegExp(p).test(text)) return true;
860
+ } catch {
861
+ }
862
+ }
863
+ return false;
864
+ }
865
+ function analyzeRepetition(history, proposed, cfg, now) {
866
+ if (!cfg.repeat.enabled) return allow;
867
+ const watched = cfg.repeat.watch.includes(proposed.tool) || proposed.tool in cfg.repeat.thresholds;
868
+ if (!watched) return allow;
869
+ if (isRepeatExempt(proposed, cfg)) return allow;
870
+ const threshold = cfg.repeat.thresholds[proposed.tool] ?? cfg.repeat.maxRepeats;
871
+ const since = now - cfg.repeat.windowMinutes * 6e4;
872
+ const n = history.filter(
873
+ (r) => r.tool_name === proposed.tool && r.args_hash === proposed.argsHash && r.ts >= since
874
+ ).length;
875
+ if (n >= threshold) {
876
+ return [
877
+ {
878
+ kind: "repeat",
879
+ severity: "block",
880
+ tool: proposed.tool,
881
+ message: `"${proposed.tool}" has already been called ${n} times with identical arguments in the last ${cfg.repeat.windowMinutes} minutes. The previous results are already in context \u2014 use them instead of re-running. If a retry is genuinely required, change the arguments or state why the previous result is insufficient.`,
882
+ evidence: `signature count=${n}, threshold=${threshold}`
883
+ }
884
+ ];
885
+ }
886
+ if (cfg.repeat.warnAt > 0 && n >= cfg.repeat.warnAt) {
887
+ return [
888
+ {
889
+ kind: "repeat",
890
+ severity: "warn",
891
+ tool: proposed.tool,
892
+ message: `"${proposed.tool}" has been called ${n} times with identical arguments \u2014 the result is already in context. Identical calls are blocked at ${threshold}; make sure any retry adds new information.`,
893
+ evidence: `signature count=${n}, warnAt=${cfg.repeat.warnAt}`
894
+ }
895
+ ];
896
+ }
897
+ return allow;
898
+ }
899
+ function analyzeCycles(history, cfg, now) {
900
+ if (!cfg.cycle.enabled) return allow;
901
+ const since = now - cfg.cycle.windowMinutes * 6e4;
902
+ const recent = history.filter((r) => r.ts >= since).slice(-16).map((r) => `${r.tool_name}:${r.args_hash}`);
903
+ if (recent.length < 8) return allow;
904
+ const findings = [];
905
+ for (let period = 1; period <= 3; period++) {
906
+ const minReps = period === 1 ? 5 : 3;
907
+ const needed = period * minReps;
908
+ const tail = recent.slice(-needed);
909
+ if (tail.length < needed) continue;
910
+ const base = tail.slice(0, period);
911
+ let isCycle = true;
912
+ for (let i = period; i < tail.length; i++) {
913
+ if (tail[i] !== base[i % period]) {
914
+ isCycle = false;
915
+ break;
916
+ }
917
+ }
918
+ if (isCycle) {
919
+ const desc = period === 1 ? `the same call (${base[0]})` : `a ${period}-step cycle (${base.map((s) => s.split(":")[0]).join(" \u2192 ")})`;
920
+ findings.push({
921
+ kind: "cycle",
922
+ severity: "block",
923
+ tool: base[0]?.split(":")[0],
924
+ message: `Loop detected: the agent has repeated ${desc} ${minReps}+ times in a row without progress. Stop re-running this sequence. Re-read the results already in context, reassess the approach, and either proceed differently or end the turn with a summary.`,
925
+ evidence: `period=${period}, reps>=${minReps}`
926
+ });
927
+ break;
928
+ }
929
+ }
930
+ return findings;
931
+ }
932
+ function analyzeNoGain(history, cfg, now) {
933
+ if (!cfg.noGain.enabled) return allow;
934
+ const since = now - cfg.noGain.windowMinutes * 6e4;
935
+ const byPair = /* @__PURE__ */ new Map();
936
+ for (const r of history) {
937
+ if (r.ts < since || !r.output_hash) continue;
938
+ const key = `${r.tool_name}:${r.output_hash}`;
939
+ byPair.set(key, (byPair.get(key) ?? 0) + 1);
940
+ }
941
+ const findings = [];
942
+ for (const [key, n] of byPair) {
943
+ if (n < cfg.noGain.warnAt) continue;
944
+ const tool = key.split(":")[0];
945
+ if (n >= cfg.noGain.blockAt) {
946
+ findings.push({
947
+ kind: "noGain",
948
+ severity: "block",
949
+ tool,
950
+ message: `No-progress loop: ${tool} returned the exact same output ${n} times despite different arguments. You are not gaining new information. Stop calling ${tool}, analyze the result you already have, change strategy, or report your findings.`,
951
+ evidence: `tool=${tool} identical_output_count=${n}`
952
+ });
953
+ } else {
954
+ findings.push({
955
+ kind: "noGain",
956
+ severity: "warn",
957
+ tool,
958
+ message: `${tool} has returned the same output ${n} times \u2014 verify you are not repeating work.`,
959
+ evidence: `tool=${tool} identical_output_count=${n}`
960
+ });
961
+ }
962
+ }
963
+ return findings.slice(0, 2);
964
+ }
965
+ function editToolSet(cfg) {
966
+ return editTools(cfg);
967
+ }
968
+ function analyzeNoProgress(history, proposed, cfg, now) {
969
+ if (!cfg.noProgress.enabled) return allow;
970
+ if (editToolSet(cfg).has(proposed.tool)) return allow;
971
+ const since = now - cfg.noProgress.windowMinutes * 6e4;
972
+ const tools = editToolSet(cfg);
973
+ let lastEditTs = -1;
974
+ for (const r of history) {
975
+ if (r.ts < since) continue;
976
+ if (tools.has(r.tool_name) && r.status === "ok") lastEditTs = Math.max(lastEditTs, r.ts);
977
+ }
978
+ const stretch = history.filter((r) => r.ts >= since && r.ts > lastEditTs).length;
979
+ if (stretch < cfg.noProgress.warnAt) return allow;
980
+ if (stretch >= cfg.noProgress.blockAt) {
981
+ return [
982
+ {
983
+ kind: "noProgress",
984
+ severity: "block",
985
+ tool: proposed.tool,
986
+ message: `No progress: ${stretch} tool calls in the last ${cfg.noProgress.windowMinutes} minutes with no successful file edit landing. You are circling, not converging. Stop, pick the single most valuable next change, make it deliberately, or report what is blocking you.`,
987
+ evidence: `stretch=${stretch} warnAt=${cfg.noProgress.warnAt} blockAt=${cfg.noProgress.blockAt}`
988
+ }
989
+ ];
990
+ }
991
+ return [
992
+ {
993
+ kind: "noProgress",
994
+ severity: "warn",
995
+ tool: proposed.tool,
996
+ message: `${stretch} calls without a landed edit recently \u2014 make sure the next step actually produces a change.`,
997
+ evidence: `stretch=${stretch}`
998
+ }
999
+ ];
1000
+ }
1001
+ function analyzeChurn(history, cfg, now) {
1002
+ if (!cfg.churn.enabled) return allow;
1003
+ const since = now - cfg.churn.windowMinutes * 6e4;
1004
+ const tools = editTools(cfg);
1005
+ const byFile = /* @__PURE__ */ new Map();
1006
+ for (const r of history) {
1007
+ if (r.ts < since || !r.file_path || !tools.has(r.tool_name)) continue;
1008
+ byFile.set(r.file_path, (byFile.get(r.file_path) ?? 0) + 1);
1009
+ }
1010
+ const findings = [];
1011
+ for (const [file, n] of byFile) {
1012
+ if (n >= cfg.churn.blockAt) {
1013
+ findings.push({
1014
+ kind: "churn",
1015
+ severity: "block",
1016
+ tool: "edit",
1017
+ message: `Edit churn: ${file} has been modified ${n} times in the last ${cfg.churn.windowMinutes} minutes without converging. Stop editing. Re-read the file and the error output, form an explicit hypothesis about why previous fixes failed, then make a single deliberate change \u2014 or ask the user for help.`,
1018
+ evidence: `file=${file} edits=${n}`
1019
+ });
1020
+ } else if (n >= cfg.churn.warnAt) {
1021
+ findings.push({
1022
+ kind: "churn",
1023
+ severity: "warn",
1024
+ tool: "edit",
1025
+ message: `${file} has been edited ${n} times recently \u2014 step back and verify your approach before editing again.`,
1026
+ evidence: `file=${file} edits=${n}`
1027
+ });
1028
+ }
1029
+ }
1030
+ return findings.sort((a, b) => a.severity === b.severity ? 0 : a.severity === "block" ? -1 : 1).slice(0, 1);
1031
+ }
1032
+ function analyzeExplore(history, proposed, cfg, now) {
1033
+ if (!cfg.explore.enabled) return allow;
1034
+ const passive = /* @__PURE__ */ new Set([...readTools(cfg), ...searchTools(cfg)]);
1035
+ if (!passive.has(proposed.tool)) return allow;
1036
+ const since = now - cfg.explore.windowMinutes * 6e4;
1037
+ let streak = 0;
1038
+ for (let i = history.length - 1; i >= 0; i--) {
1039
+ const r = history[i];
1040
+ if (r.ts < since || !passive.has(r.tool_name)) break;
1041
+ streak++;
1042
+ }
1043
+ if (streak < cfg.explore.warnAt) return allow;
1044
+ if (streak >= cfg.explore.blockAt) {
1045
+ return [
1046
+ {
1047
+ kind: "explore",
1048
+ severity: "block",
1049
+ tool: proposed.tool,
1050
+ message: `Exploration without implementation: ${streak} consecutive read/search calls with no action in the last ${cfg.explore.windowMinutes} minutes. You have gathered enough \u2014 pick the most valuable thing you learned and act on it (edit, run, or write). If nothing is actionable, summarize what you found and say so.`,
1051
+ evidence: `streak=${streak} blockAt=${cfg.explore.blockAt}`
1052
+ }
1053
+ ];
1054
+ }
1055
+ return [
1056
+ {
1057
+ kind: "explore",
1058
+ severity: "warn",
1059
+ tool: proposed.tool,
1060
+ message: `${streak} consecutive read/search calls \u2014 make sure the next step acts on what you already learned.`,
1061
+ evidence: `streak=${streak}`
1062
+ }
1063
+ ];
1064
+ }
1065
+ function fuzzyKey(tool, argsJson) {
1066
+ let text = argsJson;
1067
+ try {
1068
+ const obj = JSON.parse(argsJson);
1069
+ const parts = [];
1070
+ for (const v of Object.values(obj)) {
1071
+ if (typeof v === "string") parts.push(v);
1072
+ else if (v !== null && v !== void 0) parts.push(JSON.stringify(v));
1073
+ }
1074
+ if (parts.length > 0) text = parts.join("|");
1075
+ } catch {
1076
+ }
1077
+ return `${tool}:${text.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]/g, "")}`;
1078
+ }
1079
+ function analyzeNearRepeat(history, cfg, now = Date.now()) {
1080
+ if (!cfg.nearRepeat.enabled) return allow;
1081
+ const since = now - cfg.nearRepeat.windowMinutes * 6e4;
1082
+ const byKey = /* @__PURE__ */ new Map();
1083
+ for (const r of history) {
1084
+ if (r.ts < since) continue;
1085
+ const key = fuzzyKey(r.tool_name, r.args_json);
1086
+ const cur = byKey.get(key) ?? { n: 0, tool: r.tool_name };
1087
+ cur.n++;
1088
+ byKey.set(key, cur);
1089
+ }
1090
+ const findings = [];
1091
+ for (const [, v] of byKey) {
1092
+ if (v.n >= cfg.nearRepeat.blockAt) {
1093
+ findings.push({
1094
+ kind: "nearRepeat",
1095
+ severity: "block",
1096
+ tool: v.tool,
1097
+ message: `Near-duplicate loop: ${v.tool} has been called ${v.n} times with arguments that differ only trivially (punctuation, case, spacing, order). You are not trying anything new. Use the results already in context or change the approach substantially.`,
1098
+ evidence: `fuzzy_count=${v.n} blockAt=${cfg.nearRepeat.blockAt}`
1099
+ });
1100
+ } else if (v.n >= cfg.nearRepeat.warnAt) {
1101
+ findings.push({
1102
+ kind: "nearRepeat",
1103
+ severity: "warn",
1104
+ tool: v.tool,
1105
+ message: `${v.tool} has ${v.n} near-identical calls recently \u2014 verify these calls differ meaningfully.`,
1106
+ evidence: `fuzzy_count=${v.n}`
1107
+ });
1108
+ }
1109
+ }
1110
+ return findings.sort((a, b) => a.severity === b.severity ? 0 : a.severity === "block" ? -1 : 1).slice(0, 1);
1111
+ }
1112
+ function analyzeCall(history, proposed, cfg, now = Date.now()) {
1113
+ const findings = [
1114
+ ...analyzeRepetition(history, proposed, cfg, now),
1115
+ ...analyzeCycles(history, cfg, now),
1116
+ ...analyzeNoGain(history, cfg, now),
1117
+ ...analyzeChurn(history, cfg, now),
1118
+ ...analyzeNoProgress(history, proposed, cfg, now),
1119
+ ...analyzeNearRepeat(history, cfg, now),
1120
+ ...analyzeExplore(history, proposed, cfg, now)
1121
+ ];
1122
+ const rank = { block: 0, warn: 1 };
1123
+ findings.sort((a, b) => rank[a.severity] - rank[b.severity]);
1124
+ return { findings: findings.slice(0, 2) };
1125
+ }
1126
+
1127
+ // src/policy.ts
1128
+ var KILL_SWITCH_MESSAGE = "[agent-guard] CIRCUIT BREAK: this session has hit the intervention limit. Stop making tool calls immediately. Do not attempt to work around this guard. Summarize what you have learned so far, state what remains blocked and why, and end your turn so the user can review the situation.";
1129
+ function isKillSwitchTripped(blocksInSession, cfg) {
1130
+ return cfg.killSwitch && cfg.maxBlocksPerSession > 0 && blocksInSession >= cfg.maxBlocksPerSession;
1131
+ }
1132
+ function killSwitchDecision() {
1133
+ return { action: "block", blockReason: KILL_SWITCH_MESSAGE };
1134
+ }
1135
+ function resolveFindings(findings, ctx) {
1136
+ if (isKillSwitchTripped(ctx.blocksInSession, ctx.cfg)) {
1137
+ return killSwitchDecision();
1138
+ }
1139
+ const block = findings.find((f) => f.severity === "block");
1140
+ if (block) {
1141
+ return { action: "block", blockReason: `[agent-guard] Blocked (${block.kind}): ${block.message}` };
1142
+ }
1143
+ const warns = findings.filter((f) => f.severity === "warn");
1144
+ if (warns.length > 0) {
1145
+ const hint = warns.map((f) => `[agent-guard] note (${f.kind}): ${f.message}`).join(" | ");
1146
+ return { action: "warn", contextHint: hint };
1147
+ }
1148
+ return { action: "allow" };
1149
+ }
1150
+
1151
+ // src/precise.ts
1152
+ var DEFAULT_URL = "https://api.kimi.com/coding/v1";
1153
+ var FETCH_TIMEOUT_MS = 3e3;
1154
+ function preciseKeyConfigured(env = process.env) {
1155
+ return Boolean(env.KIMI_API_KEY?.trim());
1156
+ }
1157
+ function cachedPreciseUsage(cfg, now = Date.now()) {
1158
+ if (!cfg.precise) return null;
1159
+ try {
1160
+ const raw = getMeta("precise_usage");
1161
+ const ts = Number(getMeta("precise_usage_ts") ?? "0");
1162
+ if (!raw || ts <= 0) return null;
1163
+ if (now - ts > cfg.preciseCacheSeconds * 1e3) return null;
1164
+ const parsed = JSON.parse(raw);
1165
+ return { ...parsed, fetchedAt: ts };
1166
+ } catch {
1167
+ return null;
1168
+ }
1169
+ }
1170
+ function numOrNull(v) {
1171
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
1172
+ }
1173
+ function pickNum(obj, keys) {
1174
+ for (const k of keys) {
1175
+ const n = numOrNull(obj[k]);
1176
+ if (n !== null) return n;
1177
+ }
1178
+ return null;
1179
+ }
1180
+ function pickResetMs(obj, now) {
1181
+ for (const k of ["resetTime", "reset_at", "reset_time"]) {
1182
+ const v = obj[k];
1183
+ if (typeof v === "number" && Number.isFinite(v)) return v > 1e12 ? v : v * 1e3;
1184
+ if (typeof v === "string") {
1185
+ const parsed = Date.parse(v);
1186
+ if (!Number.isNaN(parsed)) return parsed;
1187
+ }
1188
+ }
1189
+ const inSeconds = numOrNull(obj["reset_in"]);
1190
+ return inSeconds !== null ? now + inSeconds * 1e3 : null;
1191
+ }
1192
+ function windowMinutes(item) {
1193
+ const duration = numOrNull(item["duration"]);
1194
+ const unit = String(item["timeUnit"] ?? item["time_unit"] ?? "").toLowerCase();
1195
+ if (duration === null) return null;
1196
+ if (unit.includes("min")) return duration;
1197
+ if (unit.includes("hour") || unit === "h") return duration * 60;
1198
+ if (unit.includes("day") || unit === "d") return duration * 60 * 24;
1199
+ if (unit.includes("week")) return duration * 60 * 24 * 7;
1200
+ return null;
1201
+ }
1202
+ function toWindow(item, now) {
1203
+ const limit = pickNum(item, ["limit", "limit_amount"]);
1204
+ const usedRaw = pickNum(item, ["used", "used_amount"]);
1205
+ const remaining = pickNum(item, ["remaining"]);
1206
+ const used = usedRaw ?? (limit !== null && remaining !== null ? limit - remaining : null);
1207
+ if (limit === null || used === null) return null;
1208
+ return { used, limit, resetsAt: pickResetMs(item, now) };
1209
+ }
1210
+ function parseUsagePayload(payload, now = Date.now()) {
1211
+ if (payload === null || typeof payload !== "object") return null;
1212
+ const root = payload;
1213
+ const items = [];
1214
+ const push = (v) => {
1215
+ if (Array.isArray(v)) {
1216
+ for (const it of v) if (it !== null && typeof it === "object") items.push(it);
1217
+ } else if (v !== null && typeof v === "object") {
1218
+ items.push(v);
1219
+ }
1220
+ };
1221
+ push(root["data"]);
1222
+ push(root["usage"]);
1223
+ push(root["limits"]);
1224
+ let fiveHour = null;
1225
+ let weekly = null;
1226
+ for (const item of items) {
1227
+ const candidates = [item];
1228
+ if (item["detail"] !== null && typeof item["detail"] === "object") candidates.push(item["detail"]);
1229
+ const isSummary = item["model_name"] === "all" || item["scope"] === "all";
1230
+ for (const c of candidates) {
1231
+ const win = toWindow(c, now);
1232
+ if (!win) continue;
1233
+ const mins = windowMinutes(c);
1234
+ if (mins !== null && Math.abs(mins - 300) <= 5 && !fiveHour) fiveHour = win;
1235
+ else if ((isSummary || mins === null || mins >= 60 * 24) && !weekly) weekly = win;
1236
+ }
1237
+ }
1238
+ if (!fiveHour && !weekly) return null;
1239
+ return { fiveHour, weekly, fetchedAt: now };
1240
+ }
1241
+ async function refreshPreciseUsage(cfg, env = process.env) {
1242
+ if (!cfg.precise || !preciseKeyConfigured(env)) return null;
1243
+ const fresh = cachedPreciseUsage(cfg);
1244
+ if (fresh) return fresh;
1245
+ const base = (cfg.preciseUrl || DEFAULT_URL).replace(/\/+$/, "");
1246
+ const key = env.KIMI_API_KEY.trim();
1247
+ for (const path7 of ["/usages", "/usage"]) {
1248
+ try {
1249
+ const res = await fetch(base + path7, {
1250
+ headers: { Authorization: `Bearer ${key}`, "User-Agent": "KimiCLI/1.6" },
1251
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
1252
+ });
1253
+ if (res.status === 404) continue;
1254
+ if (!res.ok) return null;
1255
+ const parsed = parseUsagePayload(await res.json());
1256
+ if (!parsed) return null;
1257
+ setMeta("precise_usage", JSON.stringify({ fiveHour: parsed.fiveHour, weekly: parsed.weekly }));
1258
+ setMeta("precise_usage_ts", String(parsed.fetchedAt));
1259
+ return parsed;
1260
+ } catch {
1261
+ return null;
1262
+ }
1263
+ }
1264
+ return null;
1265
+ }
1266
+ async function refreshPreciseIfStale(cfg, env = process.env) {
1267
+ try {
1268
+ if (cfg.precise && !cachedPreciseUsage(cfg)) await refreshPreciseUsage(cfg, env);
1269
+ } catch {
1270
+ }
1271
+ }
1272
+
1273
+ // src/meter.ts
1274
+ var PLANS = {
1275
+ tier1: { weekly: 1024, fiveHour: 200 },
1276
+ tier2: { weekly: 2048, fiveHour: 200 },
1277
+ tier3: { weekly: 7168, fiveHour: 200 }
1278
+ };
1279
+ var HOUR = 36e5;
1280
+ var FIVE_HOURS = 5 * HOUR;
1281
+ var WEEK = 7 * 24 * HOUR;
1282
+ function resolveLimits(cfg) {
1283
+ const custom = { weekly: cfg.weekly, fiveHour: cfg.fiveHour };
1284
+ if (custom.weekly > 0 || custom.fiveHour > 0) {
1285
+ const preset = PLANS[cfg.plan] ?? { weekly: 0, fiveHour: 0 };
1286
+ return {
1287
+ weekly: custom.weekly > 0 ? custom.weekly : preset.weekly,
1288
+ fiveHour: custom.fiveHour > 0 ? custom.fiveHour : preset.fiveHour
1289
+ };
1290
+ }
1291
+ return PLANS[cfg.plan] ?? { weekly: 0, fiveHour: 0 };
1292
+ }
1293
+ function budgetSnapshot(sessionId, cfg, now = Date.now()) {
1294
+ const turns = countEvents(sessionId, ["turn"], now - HOUR);
1295
+ const subagents = countEvents(sessionId, ["subagent"], now - HOUR);
1296
+ const used5h = countEvents(sessionId, ["turn"], now - FIVE_HOURS) + countEvents(sessionId, ["subagent"], now - FIVE_HOURS) * cfg.subagentWeight;
1297
+ const usedWeek = countEvents(sessionId, ["turn"], now - WEEK) + countEvents(sessionId, ["subagent"], now - WEEK) * cfg.subagentWeight;
1298
+ const limits = resolveLimits(cfg);
1299
+ const mk = (label, used, limit, span, oldestTs) => ({
1300
+ label,
1301
+ used,
1302
+ limit,
1303
+ percent: limit > 0 ? Math.min(100, Math.round(used / limit * 100)) : 0,
1304
+ resetsInMs: oldestTs !== null ? Math.max(0, oldestTs + span - now) : span
1305
+ });
1306
+ const five = mk("5h", used5h, limits.fiveHour, FIVE_HOURS, oldestEventTs(sessionId, ["turn", "subagent"], now - FIVE_HOURS));
1307
+ const week = mk("weekly", usedWeek, limits.weekly, WEEK, oldestEventTs(sessionId, ["turn", "subagent"], now - WEEK));
1308
+ const precise = cachedPreciseUsage(cfg);
1309
+ if (precise?.fiveHour && precise.fiveHour.limit > 0) {
1310
+ five.used = precise.fiveHour.used;
1311
+ five.limit = precise.fiveHour.limit;
1312
+ five.percent = Math.min(100, Math.round(five.used / five.limit * 100));
1313
+ if (precise.fiveHour.resetsAt !== null) five.resetsInMs = Math.max(0, precise.fiveHour.resetsAt - now);
1314
+ }
1315
+ if (precise?.weekly && precise.weekly.limit > 0) {
1316
+ week.used = precise.weekly.used;
1317
+ week.limit = precise.weekly.limit;
1318
+ week.percent = Math.min(100, Math.round(week.used / week.limit * 100));
1319
+ if (precise.weekly.resetsAt !== null) week.resetsInMs = Math.max(0, precise.weekly.resetsAt - now);
1320
+ }
1321
+ const hoursLeft5h = Math.max(0.25, five.resetsInMs / HOUR);
1322
+ const projected = five.used + turns * hoursLeft5h + subagents * cfg.subagentWeight * hoursLeft5h;
1323
+ return {
1324
+ enabled: cfg.enabled,
1325
+ plan: cfg.plan,
1326
+ fiveHour: five,
1327
+ weekly: week,
1328
+ turnsLastHour: turns,
1329
+ subagentsLastHour: subagents,
1330
+ projectedFiveHour: Math.round(projected),
1331
+ precise: precise !== null
1332
+ };
1333
+ }
1334
+ function evaluateBudgetGate(sessionId, cfg, now = Date.now()) {
1335
+ if (!cfg.enabled) return null;
1336
+ const snap = budgetSnapshot(sessionId, cfg, now);
1337
+ const reserve = Math.max(0, Math.min(50, cfg.reservePercent));
1338
+ for (const w of [snap.fiveHour, snap.weekly]) {
1339
+ if (w.limit <= 0) continue;
1340
+ if (w.percent >= 100 - reserve) {
1341
+ return {
1342
+ kind: "budget",
1343
+ severity: "block",
1344
+ tool: "dispatch",
1345
+ message: `Quota gate: your ${w.label} request window is ${w.percent}% consumed (${w.used}/${w.limit}) and the guard keeps a ${reserve}% reserve. Do not dispatch subagents \u2014 continue the task directly in this session with minimal tool usage, or ask the user to wait for the window reset or raise [budget] limits.`,
1346
+ evidence: `window=${w.label} used=${w.used} limit=${w.limit}`
1347
+ };
1348
+ }
1349
+ }
1350
+ if (snap.fiveHour.limit > 0 && snap.projectedFiveHour > snap.fiveHour.limit * (1 - reserve / 100)) {
1351
+ return {
1352
+ kind: "budget",
1353
+ severity: "warn",
1354
+ tool: "dispatch",
1355
+ message: `Burn rate warning: at the current rate (~${snap.turnsLastHour} req/h + ${snap.subagentsLastHour} subagent/h) you are projected to reach ~${snap.projectedFiveHour} requests in this 5h window (limit ${snap.fiveHour.limit}). Prefer fewer, larger subagent dispatches.`,
1356
+ evidence: `projected=${snap.projectedFiveHour} limit=${snap.fiveHour.limit}`
1357
+ };
1358
+ }
1359
+ if (snap.fiveHour.percent >= cfg.warnPercent || snap.weekly.percent >= cfg.warnPercent) {
1360
+ return {
1361
+ kind: "budget",
1362
+ severity: "warn",
1363
+ tool: "dispatch",
1364
+ message: `Budget: 5h window ${snap.fiveHour.percent}% used, weekly window ${snap.weekly.percent}% used.`,
1365
+ evidence: `5h=${snap.fiveHour.used}/${snap.fiveHour.limit} week=${snap.weekly.used}/${snap.weekly.limit}`
1366
+ };
1367
+ }
1368
+ return null;
1369
+ }
1370
+ function formatSnapshot(snap) {
1371
+ if (!snap.enabled) return "budget guard disabled";
1372
+ const bar = (w) => {
1373
+ if (w.limit <= 0) return `${w.used} used (no limit configured)`;
1374
+ const filled = Math.round(w.percent / 100 * 20);
1375
+ return `[${"\u2588".repeat(filled)}${"\u2591".repeat(20 - filled)}] ${w.percent}% (${w.used}/${w.limit})`;
1376
+ };
1377
+ return [
1378
+ `plan: ${snap.plan}${snap.precise ? " (precise, official API)" : ""}`,
1379
+ `5h: ${bar(snap.fiveHour)} resets in ${Math.round(snap.fiveHour.resetsInMs / HOUR)}h`,
1380
+ `weekly: ${bar(snap.weekly)} resets in ${Math.round(snap.weekly.resetsInMs / (24 * HOUR))}d`,
1381
+ `burn rate: ${snap.turnsLastHour} req + ${snap.subagentsLastHour} subagents in the last hour`,
1382
+ `projected 5h usage at current rate: ~${snap.projectedFiveHour}`
1383
+ ].join("\n");
1384
+ }
1385
+
1386
+ // src/checkpoint.ts
1387
+ import fs6 from "fs";
1388
+ import path5 from "path";
1389
+ function argSummary(argsJson, max = 100) {
1390
+ try {
1391
+ const obj = JSON.parse(argsJson);
1392
+ const parts = [];
1393
+ for (const [k, v] of Object.entries(obj)) {
1394
+ const s = typeof v === "string" ? v : JSON.stringify(v);
1395
+ parts.push(`${k}=${s.length > 60 ? s.slice(0, 57) + "..." : s}`);
1396
+ }
1397
+ const joined = parts.join(", ");
1398
+ return joined.length > max ? joined.slice(0, max - 3) + "..." : joined || "{}";
1399
+ } catch {
1400
+ return argsJson.slice(0, max);
1401
+ }
1402
+ }
1403
+ function buildBrief(sessionId, now = Date.now(), windowMs = 6 * 36e5, cfg = loadConfig()) {
1404
+ const calls = callsSince(sessionId, now - windowMs, 1e3);
1405
+ if (calls.length === 0) return "";
1406
+ const shells = shellTools(cfg);
1407
+ const edits = editTools(cfg);
1408
+ const reads = readTools(cfg);
1409
+ const searchesSet = searchTools(cfg);
1410
+ const files = /* @__PURE__ */ new Map();
1411
+ const commands = [];
1412
+ const searches = [];
1413
+ const failures = [];
1414
+ for (const r of calls) {
1415
+ const summary = argSummary(r.args_json, 90);
1416
+ if (edits.has(r.tool_name) && r.file_path) {
1417
+ const f = files.get(r.file_path) ?? { reads: 0, edits: 0 };
1418
+ f.edits++;
1419
+ files.set(r.file_path, f);
1420
+ } else if (r.file_path && reads.has(r.tool_name)) {
1421
+ const f = files.get(r.file_path) ?? { reads: 0, edits: 0 };
1422
+ f.reads++;
1423
+ files.set(r.file_path, f);
1424
+ }
1425
+ if (shells.has(r.tool_name)) commands.push(summary);
1426
+ if (searchesSet.has(r.tool_name)) searches.push(summary);
1427
+ if (r.status === "failure") failures.push(`${r.tool_name}: ${summary}`);
1428
+ }
1429
+ const lines = [];
1430
+ lines.push("## Observed activity (auto-captured by agent-guard)");
1431
+ lines.push("");
1432
+ if (files.size > 0) {
1433
+ lines.push("### Files touched");
1434
+ for (const [f, c] of [...files.entries()].slice(0, 25)) {
1435
+ lines.push(`- ${f} (read \xD7${c.reads}, edited \xD7${c.edits})`);
1436
+ }
1437
+ lines.push("");
1438
+ }
1439
+ if (commands.length > 0) {
1440
+ lines.push("### Commands run (most recent last)");
1441
+ for (const c of commands.slice(-10)) lines.push(`- ${c}`);
1442
+ lines.push("");
1443
+ }
1444
+ if (searches.length > 0) {
1445
+ lines.push("### Searches performed (results are already known \u2014 do not redo them)");
1446
+ const seen = /* @__PURE__ */ new Set();
1447
+ for (const s of searches.slice(-15)) {
1448
+ if (seen.has(s)) continue;
1449
+ seen.add(s);
1450
+ lines.push(`- ${s}`);
1451
+ }
1452
+ lines.push("");
1453
+ }
1454
+ if (failures.length > 0) {
1455
+ lines.push("### Failed calls (avoid repeating these)");
1456
+ const seen = /* @__PURE__ */ new Set();
1457
+ for (const f of failures.slice(-8)) {
1458
+ if (seen.has(f)) continue;
1459
+ seen.add(f);
1460
+ lines.push(`- ${f}`);
1461
+ }
1462
+ lines.push("");
1463
+ }
1464
+ lines.push(`Total recorded tool calls in window: ${calls.length}`);
1465
+ return lines.join("\n");
1466
+ }
1467
+ function captureCheckpoint(sessionId, reason, now = Date.now(), cfg = loadConfig()) {
1468
+ const brief = buildBrief(sessionId, now, 6 * 36e5, cfg);
1469
+ if (!brief) return null;
1470
+ const dir = path5.join(guardHome(), "checkpoints", sessionId.replace(/[^\w.-]/g, "_"));
1471
+ fs6.mkdirSync(dir, { recursive: true });
1472
+ const file = path5.join(dir, `${now}-${reason.replace(/[^\w-]/g, "_")}.md`);
1473
+ const header = [
1474
+ `# agent-guard checkpoint`,
1475
+ ``,
1476
+ `- session: ${sessionId}`,
1477
+ `- time: ${new Date(now).toISOString()}`,
1478
+ `- reason: ${reason}`,
1479
+ ``
1480
+ ].join("\n");
1481
+ fs6.writeFileSync(file, header + brief + "\n", "utf8");
1482
+ recordEvent(sessionId, "checkpoint", { reason, file }, now);
1483
+ return { sessionId, path: file, brief, reason, ts: now };
1484
+ }
1485
+ function latestSessionId() {
1486
+ const sessions = knownSessions(1);
1487
+ return sessions[0]?.session_id ?? null;
1488
+ }
1489
+ function latestCheckpointFile(sessionId) {
1490
+ const base = path5.join(guardHome(), "checkpoints");
1491
+ if (!fs6.existsSync(base)) return null;
1492
+ let dir = sessionId ? path5.join(base, sessionId.replace(/[^\w.-]/g, "_")) : "";
1493
+ if (!dir || !fs6.existsSync(dir)) {
1494
+ const dirs = fs6.readdirSync(base).map((d) => ({ d, m: fs6.statSync(path5.join(base, d)).mtimeMs })).sort((a, b) => b.m - a.m);
1495
+ if (dirs.length === 0) return null;
1496
+ dir = path5.join(base, dirs[0].d);
1497
+ }
1498
+ const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().reverse();
1499
+ return files[0] ? path5.join(dir, files[0]) : null;
1500
+ }
1501
+ function renderResumeBlock(brief, reason) {
1502
+ return [
1503
+ `<agent-guard-resume reason="${reason}">`,
1504
+ "You are resuming a task that was interrupted. Use the observed state below as verified",
1505
+ "prior knowledge. Do NOT re-explore files you have already read, do NOT redo searches",
1506
+ "listed here, and do NOT repeat failed calls. Continue from the last known state.",
1507
+ "",
1508
+ brief,
1509
+ "</agent-guard-resume>"
1510
+ ].join("\n");
1511
+ }
1512
+
1513
+ // src/verify.ts
1514
+ var DEFAULT_CLAIM_PATTERNS = [
1515
+ /\btests?\b.{0,40}\b(pass(?:ed|ing)?|green)\b/i,
1516
+ /\ball\b.{0,24}\btests?\b.{0,24}\bpass/i,
1517
+ /\bbuild\b.{0,30}\b(succeed(?:ed)?|passed|ok)\b/i,
1518
+ /\bcompil(?:es?|ed)\b.{0,20}\bsuccessfully\b/i,
1519
+ /\blint\b.{0,30}\b(clean|passed|no issues)\b/i,
1520
+ /\bfixed\b.{0,40}\b(all|every)\b/i,
1521
+ /测试(全部|都)?通过/,
1522
+ /全部(测试)?通过/,
1523
+ /构建成功/,
1524
+ /编译通过/,
1525
+ /零(错误|警告)/,
1526
+ /问题已全部解决/
1527
+ ];
1528
+ var DEFAULT_EVIDENCE_PATTERNS = [
1529
+ /\b(test|tests|vitest|jest|mocha|pytest|cargo test|go test|make test)\b/i,
1530
+ /\b(npm|pnpm|yarn)\s+(run\s+)?(test|check)\b/i,
1531
+ /\b(mvn|gradle|sbt|dotnet\s+test)\b/i,
1532
+ /\b(tsc|pyright|mypy|eslint|biome|ruff|flake8|clippy)\b/i,
1533
+ /\b(build|compile|lint|check|verify)\b/i,
1534
+ /\bmake\b/i
1535
+ ];
1536
+ function findClaims(text, cfg) {
1537
+ if (!text) return [];
1538
+ const patterns = cfg.verify.claimPatterns.length > 0 ? cfg.verify.claimPatterns.map((p) => new RegExp(p)) : DEFAULT_CLAIM_PATTERNS;
1539
+ const claims = [];
1540
+ for (const p of patterns) {
1541
+ const m = p.exec(text);
1542
+ if (m) {
1543
+ claims.push({
1544
+ pattern: String(p),
1545
+ snippet: text.slice(Math.max(0, (m.index ?? 0) - 40), (m.index ?? 0) + m[0].length + 40).replace(/\s+/g, " ").trim()
1546
+ });
1547
+ }
1548
+ if (claims.length >= 3) break;
1549
+ }
1550
+ return claims;
1551
+ }
1552
+ function hasEvidence(sessionId, cfg, now = Date.now()) {
1553
+ const vouched = getMeta(`vouched:${sessionId}`) === "1";
1554
+ if (vouched) return true;
1555
+ const since = now - cfg.verify.evidenceWindowMinutes * 6e4;
1556
+ const patterns = cfg.verify.evidencePatterns.length > 0 ? cfg.verify.evidencePatterns.map((p) => new RegExp(p)) : DEFAULT_EVIDENCE_PATTERNS;
1557
+ const shells = shellTools(cfg);
1558
+ const calls = callsSince(sessionId, since, 400);
1559
+ for (const r of calls) {
1560
+ if (r.status !== "ok") continue;
1561
+ if (!shells.has(r.tool_name)) continue;
1562
+ try {
1563
+ const args = JSON.parse(r.args_json);
1564
+ const cmd = args.command ?? "";
1565
+ for (const p of patterns) {
1566
+ if (p.test(cmd)) return true;
1567
+ }
1568
+ } catch {
1569
+ continue;
1570
+ }
1571
+ }
1572
+ return false;
1573
+ }
1574
+ function hasRecentEdits(sessionId, cfg, now = Date.now()) {
1575
+ const since = now - cfg.verify.evidenceWindowMinutes * 6e4;
1576
+ const edits = editTools(cfg);
1577
+ return callsSince(sessionId, since, 400).some(
1578
+ (r) => r.status === "ok" && edits.has(r.tool_name)
1579
+ );
1580
+ }
1581
+ var HOOKS_STOP_BLOCK_REASON = "[agent-guard] Blocked (verify): this turn ended after successful file edits with no successful verification command (test/build/lint) in the session. Run your verification before claiming completion, or state explicitly why it cannot run here.";
1582
+ var WIRE_VERIFY_CORRECTIVE = "[agent-guard verification] Your final message claims tests/build pass, but no successful verification command was recorded in this session. Actually run the verification now and base your claims on real results, then restate the conclusion.";
1583
+
1584
+ // src/guard.ts
1585
+ function probeEnabled() {
1586
+ if (process.env.KIMI_GUARD_PROBE === "1") return true;
1587
+ try {
1588
+ return getMeta("probe_enabled") === "1";
1589
+ } catch {
1590
+ return false;
1591
+ }
1592
+ }
1593
+ function appendProbe(event, payload) {
1594
+ const line = JSON.stringify({ ts: Date.now(), event, payload }) + "\n";
1595
+ fs7.appendFileSync(probeLogPath(), line, "utf8");
1596
+ }
1597
+ async function readStdinJson() {
1598
+ const chunks = [];
1599
+ for await (const chunk of process.stdin) chunks.push(chunk);
1600
+ const raw = Buffer.concat(chunks).toString("utf8");
1601
+ if (!raw.trim()) return {};
1602
+ try {
1603
+ const parsed = JSON.parse(raw);
1604
+ return typeof parsed === "object" && parsed !== null ? parsed : { value: parsed };
1605
+ } catch {
1606
+ return { _unparsed: raw.slice(0, 4096) };
1607
+ }
1608
+ }
1609
+ function processHookEvent(event, cfg, payload, now = Date.now()) {
1610
+ if (probeEnabled()) {
1611
+ try {
1612
+ appendProbe(event, payload);
1613
+ } catch {
1614
+ }
1615
+ }
1616
+ try {
1617
+ setMeta("last_hook_ts", String(now));
1618
+ setMeta("last_hook_event", event);
1619
+ } catch {
1620
+ }
1621
+ const sessionId = typeof payload["session_id"] === "string" && payload["session_id"] || typeof payload["sessionId"] === "string" && payload["sessionId"] || typeof payload["session"] === "string" && payload["session"] || "unknown";
1622
+ switch (event) {
1623
+ case "PreToolUse":
1624
+ return handlePreToolUse(event, cfg, payload, sessionId, now);
1625
+ case "PostToolUse":
1626
+ case "PostToolUseFailure":
1627
+ return handlePostTool(event, cfg, payload, sessionId, now);
1628
+ case "Stop": {
1629
+ if (cfg.harness === "claude") recordEvent(sessionId, "turn", { origin: "stop" }, now);
1630
+ if (!cfg.verify.enabled || !cfg.verify.blockOnNoEvidence) return { code: 0 };
1631
+ if (!hasRecentEdits(sessionId, cfg, now)) return { code: 0 };
1632
+ if (hasEvidence(sessionId, cfg, now)) return { code: 0 };
1633
+ const id = recordBlock(sessionId, "Stop", "verify", now);
1634
+ return { code: 2, stderr: HOOKS_STOP_BLOCK_REASON + feedbackHint(id) };
1635
+ }
1636
+ case "UserPromptSubmit": {
1637
+ return handleGoalAnchor(payload, sessionId, cfg, now);
1638
+ }
1639
+ case "PreCompact":
1640
+ recordEvent(sessionId, "compaction", { phase: "pre" }, now);
1641
+ setSessionMeta(sessionId, "last_compact_ts", String(now));
1642
+ captureCheckpoint(sessionId, "pre-compact", now, cfg);
1643
+ return { code: 0 };
1644
+ case "PostCompact":
1645
+ recordEvent(sessionId, "compaction", { phase: "post" }, now);
1646
+ return { code: 0 };
1647
+ case "TurnStarted":
1648
+ recordEvent(sessionId, "turn", { origin: payload["origin_kind"] ?? payload["origin"] ?? "" }, now);
1649
+ return { code: 0 };
1650
+ case "SubagentStart":
1651
+ recordEvent(sessionId, "subagent", { agent: payload["agent_name"] ?? "" }, now);
1652
+ return { code: 0 };
1653
+ case "StopFailure":
1654
+ recordEvent(sessionId, "stop_failure", { error: String(payload["error_message"] ?? payload["error_type"] ?? "") }, now);
1655
+ captureCheckpoint(sessionId, "stop-failure", now, cfg);
1656
+ return { code: 0 };
1657
+ case "Interrupt":
1658
+ recordEvent(sessionId, "interrupt", { reason: String(payload["reason"] ?? "") }, now);
1659
+ captureCheckpoint(sessionId, "interrupt", now, cfg);
1660
+ return { code: 0 };
1661
+ case "SessionEnd":
1662
+ captureCheckpoint(sessionId, "session-end", now, cfg);
1663
+ return { code: 0 };
1664
+ default:
1665
+ return { code: 0 };
1666
+ }
1667
+ }
1668
+ function feedbackHint(id) {
1669
+ return `
1670
+ [agent-guard] block #${id} recorded. If this was a false positive, run: kguard feedback fp ${id}`;
1671
+ }
1672
+ function noteNormalizeMiss(now) {
1673
+ try {
1674
+ const misses = Number(getMeta("normalize_misses") ?? "0") + 1;
1675
+ setMeta("normalize_misses", String(misses));
1676
+ setMeta("last_normalize_miss_ts", String(now));
1677
+ } catch {
1678
+ }
1679
+ }
1680
+ function handlePreToolUse(event, cfg, payload, sessionId, now) {
1681
+ const call = normalizeCall(payload, event, now);
1682
+ if (!call) {
1683
+ if (Object.keys(payload).length > 0) noteNormalizeMiss(now);
1684
+ return { code: 0 };
1685
+ }
1686
+ const since = now - Math.max(cfg.repeat.windowMinutes, cfg.cycle.windowMinutes, cfg.policy.blockWindowMinutes) * 6e4;
1687
+ const history = callsSince(sessionId, since);
1688
+ const analysis = analyzeCall(history, { tool: call.tool, argsHash: call.argsHash, args: call.args }, cfg, now);
1689
+ if (cfg.budget.dispatchTools.includes(call.tool)) {
1690
+ const budgetFinding = evaluateBudgetGate(sessionId, cfg.budget, now);
1691
+ if (budgetFinding) analysis.findings.unshift(budgetFinding);
1692
+ }
1693
+ const blocksInSession = countBlocks(sessionId, now - cfg.policy.blockWindowMinutes * 6e4);
1694
+ const decision = resolveFindings(analysis.findings, { blocksInSession, cfg: cfg.policy });
1695
+ if (decision.action === "block") {
1696
+ const kind = isKillSwitchTripped(blocksInSession, cfg.policy) ? "killSwitch" : analysis.findings.find((f) => f.severity === "block")?.kind ?? "unknown";
1697
+ const id = recordBlock(sessionId, call.tool, kind, now);
1698
+ return { code: 2, stderr: decision.blockReason + feedbackHint(id) };
1699
+ }
1700
+ if (decision.action === "warn" && decision.contextHint) {
1701
+ return { code: 0, stdout: decision.contextHint };
1702
+ }
1703
+ return { code: 0 };
1704
+ }
1705
+ function sessionKey(sessionId, key) {
1706
+ return `${key}:${sessionId}`;
1707
+ }
1708
+ function getSessionMeta(sessionId, key) {
1709
+ return getMeta(sessionKey(sessionId, key));
1710
+ }
1711
+ function setSessionMeta(sessionId, key, value) {
1712
+ setMeta(sessionKey(sessionId, key), value);
1713
+ }
1714
+ function handleGoalAnchor(payload, sessionId, cfg, now = Date.now()) {
1715
+ if (!cfg.anchor.enabled) return { code: 0 };
1716
+ const promptText = typeof payload["prompt"] === "string" && payload["prompt"] || typeof payload["user_input"] === "string" && payload["user_input"] || "";
1717
+ if (!promptText) return { code: 0 };
1718
+ const count = Number(getSessionMeta(sessionId, "anchor_count") ?? "0") + 1;
1719
+ setSessionMeta(sessionId, "anchor_count", String(count));
1720
+ recordEvent(sessionId, "prompt", { chars: promptText.length }, now);
1721
+ if (count === 1) {
1722
+ setSessionMeta(sessionId, "goal", promptText.slice(0, cfg.anchor.maxChars));
1723
+ setSessionMeta(sessionId, "last_anchor_ts", String(now));
1724
+ return { code: 0 };
1725
+ }
1726
+ const lastCompact = Number(getSessionMeta(sessionId, "last_compact_ts") ?? "0");
1727
+ const lastAnchor = Number(getSessionMeta(sessionId, "last_anchor_ts") ?? "0");
1728
+ const afterCompaction = lastCompact > lastAnchor;
1729
+ const periodic = count % cfg.anchor.everyNPrompts === 0;
1730
+ if (!afterCompaction && !periodic) return { code: 0 };
1731
+ const goal = getSessionMeta(sessionId, "goal") ?? "";
1732
+ if (!goal) return { code: 0 };
1733
+ setSessionMeta(sessionId, "last_anchor_ts", String(now));
1734
+ return {
1735
+ code: 0,
1736
+ stdout: `[agent-guard] goal anchor (injected ${afterCompaction ? "after compaction" : `every ${cfg.anchor.everyNPrompts} prompts`}): the user's task for this session, verbatim: "${goal}". Re-evaluate: does the current work still serve this goal? If you have drifted, get back on target; if the goal is already met, stop and summarize.`
1737
+ };
1738
+ }
1739
+ function handlePostTool(event, cfg, payload, sessionId, now) {
1740
+ void cfg;
1741
+ const call = normalizeCall(payload, event, now);
1742
+ if (!call) return { code: 0 };
1743
+ recordCall({
1744
+ sessionId,
1745
+ toolName: call.tool,
1746
+ argsHash: call.argsHash,
1747
+ argsJson: call.argsJson,
1748
+ outputHash: call.outputHash,
1749
+ filePath: call.filePath,
1750
+ status: call.status,
1751
+ ts: now
1752
+ });
1753
+ return { code: 0 };
1754
+ }
1755
+
1756
+ // src/hook.ts
1757
+ function encodeHint(event, harness, text) {
1758
+ if (harness === "claude") {
1759
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: text } });
1760
+ }
1761
+ return text;
1762
+ }
1763
+ async function runHook(event, harness = "kimi") {
1764
+ const payload = await readStdinJson();
1765
+ let outcome;
1766
+ try {
1767
+ const cfg = loadConfig(void 0, harness);
1768
+ if (event === "PreToolUse" && cfg.budget.precise) {
1769
+ const tool = typeof payload["tool_name"] === "string" && payload["tool_name"] || typeof payload["toolName"] === "string" && payload["toolName"] || typeof payload["tool"] === "string" && payload["tool"] || "";
1770
+ if (cfg.budget.dispatchTools.includes(tool)) await refreshPreciseIfStale(cfg.budget);
1771
+ }
1772
+ outcome = processHookEvent(event, cfg, payload);
1773
+ } catch (err) {
1774
+ process.stderr.write(`[agent-guard] guard error (fail-open): ${err.message}
1775
+ `);
1776
+ return 0;
1777
+ }
1778
+ if (outcome.stdout) process.stdout.write(encodeHint(event, harness, outcome.stdout) + "\n");
1779
+ if (outcome.stderr) process.stderr.write(outcome.stderr + "\n");
1780
+ return outcome.code;
1781
+ }
1782
+
1783
+ // src/status.ts
1784
+ import fs8 from "fs";
1785
+ import { spawnSync } from "child_process";
1786
+ import pc from "picocolors";
1787
+
1788
+ // src/veto.ts
1789
+ function vetoKeyConfigured(env = process.env) {
1790
+ return Boolean(env.KIMI_GUARD_VETO_API_KEY?.trim());
1791
+ }
1792
+ function vetoBaseUrls(cfg, env = process.env) {
1793
+ return {
1794
+ baseUrl: env.KIMI_GUARD_VETO_BASE_URL?.trim() || cfg.baseUrl,
1795
+ model: env.KIMI_GUARD_VETO_MODEL?.trim() || cfg.model
1796
+ };
1797
+ }
1798
+ function collectVetoContext(sessionId, cfg, now = Date.now()) {
1799
+ const since = now - cfg.verify.evidenceWindowMinutes * 6e4;
1800
+ const calls = callsSince(sessionId, since, 400);
1801
+ const shells = shellTools(cfg);
1802
+ const recentCommands = [];
1803
+ const editedFiles = [];
1804
+ for (const r of calls.slice(-40)) {
1805
+ if (shells.has(r.tool_name)) {
1806
+ try {
1807
+ const args = JSON.parse(r.args_json);
1808
+ if (args.command) recentCommands.push(args.command.slice(0, 120));
1809
+ } catch {
1810
+ }
1811
+ }
1812
+ if (r.file_path && editedFiles.length < 10) editedFiles.push(r.file_path);
1813
+ }
1814
+ return { sessionId, claims: [], goal: "", recentCommands: recentCommands.slice(-5), editedFiles };
1815
+ }
1816
+ var PROMPT_HEADER = "You are a false-positive detector for an AI-agent guardrail. An agent just finished its turn claiming completion, but the session's recorded command history contains NO successful verification command (test/build/lint). Decide whether blocking would be a FALSE POSITIVE \u2014 i.e. the agent has a legitimate reason why verification cannot run in this session.\nRules: base your vote ONLY on the facts below. A claim that verification is unnecessary or happens elsewhere is NOT by itself a reason to veto. Answer with EXACTLY one line:\nVETO: yes (false positive \u2014 allow the completion)\nVETO: no (block stands \u2014 the agent must actually run verification)\nDo not write anything else.\n\nFacts:\n";
1817
+ function buildVetoPrompt(ctx) {
1818
+ const lines = [];
1819
+ lines.push(`- user goal: ${ctx.goal.slice(0, 300) || "(unknown)"}`);
1820
+ lines.push(`- claims made by the agent:`);
1821
+ for (const c of ctx.claims.slice(0, 3)) lines.push(` "${c.snippet}"`);
1822
+ lines.push(`- recent commands the agent ran: ${ctx.recentCommands.length > 0 ? ctx.recentCommands.join(" ; ") : "(none)"}`);
1823
+ lines.push(`- files the agent edited: ${ctx.editedFiles.length > 0 ? ctx.editedFiles.join(", ") : "(none)"}`);
1824
+ lines.push("- recorded successful verification commands in session history: none");
1825
+ return PROMPT_HEADER + lines.join("\n");
1826
+ }
1827
+ async function castVetoVote(ctx, cfg, env = process.env) {
1828
+ if (!cfg.enabled || !vetoKeyConfigured(env)) return { vetoed: false, error: "veto disabled" };
1829
+ const calls = Number(getMeta(`veto_calls:${ctx.sessionId}`) ?? "0");
1830
+ if (calls >= cfg.maxCallsPerSession) return { vetoed: false, error: "session vote budget exhausted" };
1831
+ setMeta(`veto_calls:${ctx.sessionId}`, String(calls + 1));
1832
+ const { baseUrl, model } = vetoBaseUrls(cfg, env);
1833
+ const key = env.KIMI_GUARD_VETO_API_KEY.trim();
1834
+ try {
1835
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
1836
+ method: "POST",
1837
+ headers: {
1838
+ "Content-Type": "application/json",
1839
+ Authorization: `Bearer ${key}`
1840
+ },
1841
+ body: JSON.stringify({
1842
+ model,
1843
+ messages: [{ role: "user", content: buildVetoPrompt(ctx) }],
1844
+ max_tokens: 8,
1845
+ temperature: 0,
1846
+ stream: false
1847
+ }),
1848
+ signal: AbortSignal.timeout(cfg.timeoutMs)
1849
+ });
1850
+ if (!res.ok) return { vetoed: false, error: `http ${res.status}` };
1851
+ const data = await res.json();
1852
+ const raw = (data.choices?.[0]?.message?.content ?? "").trim();
1853
+ return { vetoed: /^VETO:\s*yes\b/i.test(raw), raw };
1854
+ } catch (err) {
1855
+ return { vetoed: false, error: err.message };
1856
+ }
1857
+ }
1858
+
1859
+ // src/status.ts
1860
+ function ok(msg) {
1861
+ console.log(`${pc.green("\u2713")} ${msg}`);
1862
+ }
1863
+ function warn(msg) {
1864
+ console.log(`${pc.yellow("!")} ${msg}`);
1865
+ }
1866
+ function fail(msg) {
1867
+ console.log(`${pc.red("\u2717")} ${msg}`);
1868
+ }
1869
+ var CALIBRATION_KEYS = {
1870
+ repeat: "repeat.maxRepeats",
1871
+ nearRepeat: "nearRepeat.blockAt",
1872
+ noGain: "noGain.blockAt",
1873
+ churn: "churn.blockAt",
1874
+ noProgress: "noProgress.blockAt",
1875
+ explore: "explore.blockAt",
1876
+ budget: "budget.reservePercent",
1877
+ cycle: "cycle.enabled = false",
1878
+ verify: "verify.blockOnNoEvidence = false (or enable verify.veto)",
1879
+ killSwitch: "policy.maxBlocksPerSession"
1880
+ };
1881
+ function calibrationHints(stats) {
1882
+ const hints = [];
1883
+ for (const s of stats) {
1884
+ if (s.n < 5) continue;
1885
+ const rate = s.fp / s.n;
1886
+ if (rate > 0.3) {
1887
+ const key = CALIBRATION_KEYS[s.kind] ?? `${s.kind} thresholds`;
1888
+ hints.push(`${s.kind}: ${s.fp}/${s.n} blocks marked false positive (${Math.round(rate * 100)}%) \u2014 consider raising ${key}`);
1889
+ }
1890
+ }
1891
+ return hints;
1892
+ }
1893
+ function buildGuardReport(cfg = loadConfig()) {
1894
+ const s = buildStatus();
1895
+ const detectors = blockKindStats().map((k) => ({
1896
+ kind: k.kind,
1897
+ blocks: k.n,
1898
+ falsePositives: k.fp,
1899
+ confirmed: k.tp,
1900
+ fpRate: k.n > 0 ? Math.round(k.fp / k.n * 100) / 100 : 0
1901
+ }));
1902
+ const sid = latestSessionId() ?? "unknown";
1903
+ const snap = budgetSnapshot(sid, cfg.budget);
1904
+ return {
1905
+ tool: "agent-guard",
1906
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1907
+ sessions: knownSessions(1e3).length,
1908
+ calls24h: s.calls24h,
1909
+ blocks24h: s.blocks24h.reduce((acc, b) => acc + b.n, 0),
1910
+ detectors,
1911
+ budget: {
1912
+ plan: cfg.budget.plan,
1913
+ precise: snap.precise,
1914
+ fiveHourPercent: snap.fiveHour.percent,
1915
+ weeklyPercent: snap.weekly.percent
1916
+ }
1917
+ };
1918
+ }
1919
+ function agentProcessRunning() {
1920
+ if (process.platform !== "darwin" && process.platform !== "linux") return false;
1921
+ try {
1922
+ const r = spawnSync("ps", ["-eo", "args"], { encoding: "utf8", timeout: 3e3, maxBuffer: 8 * 1024 * 1024 });
1923
+ if (r.status !== 0 || !r.stdout) return false;
1924
+ return r.stdout.split("\n").some((line) => {
1925
+ const l = line.trim();
1926
+ if (l.includes("agentguard") || l.includes("kimi-guard") || l.includes("kguard")) return false;
1927
+ return /(?:^|[/\s])(kimi|claude)(?:\s|$)/.test(l);
1928
+ });
1929
+ } catch {
1930
+ return false;
1931
+ }
1932
+ }
1933
+ function cmdStatus() {
1934
+ const cfg = loadConfig();
1935
+ const s = buildStatus();
1936
+ const dt = s.lastActivityTs ? new Date(s.lastActivityTs).toISOString() : "never";
1937
+ const lastHookTs = Number(getMeta("last_hook_ts") ?? "0");
1938
+ const lastHook = lastHookTs > 0 ? `${new Date(lastHookTs).toISOString()} (${getMeta("last_hook_event") ?? "?"})` : "never";
1939
+ const normalizeMisses = Number(getMeta("normalize_misses") ?? "0");
1940
+ console.log(`agent-guard status (state: ${stateDbPath()})`);
1941
+ console.log(` last activity: ${dt}`);
1942
+ console.log(` last hook activity: ${lastHook}`);
1943
+ if (normalizeMisses > 0) {
1944
+ console.log(` ${pc.yellow("!")} payload normalization misses: ${normalizeMisses} \u2014 possible upstream schema drift; run 'kguard probe on' and compare 'kguard doctor' field coverage`);
1945
+ }
1946
+ console.log(` tool calls (24h): ${s.calls24h}`);
1947
+ const parts24 = s.blocks24h.map((b) => `${b.kind}\xD7${b.n}`).join(", ");
1948
+ console.log(` interventions (24h): ${parts24 || "none"}`);
1949
+ const stats = blockKindStats();
1950
+ const withFeedback = stats.filter((k) => k.fp + k.tp > 0);
1951
+ if (withFeedback.length > 0) {
1952
+ console.log(" intervention quality (all time):");
1953
+ for (const k of stats) {
1954
+ const rate = k.n > 0 ? Math.round(k.fp / k.n * 100) : 0;
1955
+ console.log(` ${k.kind.padEnd(12)} blocks=${k.n} fp=${k.fp} (${rate}%) confirmed=${k.tp}`);
1956
+ }
1957
+ for (const h of calibrationHints(stats)) console.log(` ${pc.yellow("!")} calibration: ${h}`);
1958
+ }
1959
+ if (s.events24h.length > 0) {
1960
+ console.log(` agent events (24h): ${s.events24h.map((e) => `${e.kind}\xD7${e.n}`).join(", ")}`);
1961
+ }
1962
+ const sessions = knownSessions(3);
1963
+ if (sessions.length > 0) {
1964
+ console.log(" recent sessions:");
1965
+ for (const sess of sessions) {
1966
+ console.log(` ${sess.session_id.slice(0, 24)} calls=${sess.n} last=${new Date(sess.last_ts).toISOString()}`);
1967
+ }
1968
+ }
1969
+ if (s.topRepeated.length > 0) {
1970
+ console.log(" top repeated call signatures:");
1971
+ for (const r of s.topRepeated) console.log(` ${r.tool_name} [${r.args_hash}] \xD7${r.n}`);
1972
+ }
1973
+ console.log("");
1974
+ console.log(`budget (${cfg.budget.plan}):`);
1975
+ const sid = latestSessionId() ?? "unknown";
1976
+ console.log(
1977
+ formatSnapshot(budgetSnapshot(sid, cfg.budget)).split("\n").map((l) => " " + l).join("\n")
1978
+ );
1979
+ }
1980
+ function cmdDoctor() {
1981
+ let failures = 0;
1982
+ const cfg = loadConfig();
1983
+ const check = (good, okMsg, failMsg) => {
1984
+ if (good) ok(okMsg);
1985
+ else {
1986
+ fail(failMsg);
1987
+ failures++;
1988
+ }
1989
+ };
1990
+ check(
1991
+ Number(process.versions.node.split(".")[0]) >= 22,
1992
+ `node ${process.versions.node}`,
1993
+ `node ${process.versions.node} is too old (need >=22.13 for node:sqlite)`
1994
+ );
1995
+ try {
1996
+ fs8.mkdirSync(guardHome(), { recursive: true });
1997
+ fs8.accessSync(guardHome(), fs8.constants.W_OK);
1998
+ check(true, `state dir writable: ${guardHome()}`, "");
1999
+ openDb().prepare("SELECT 1").get();
2000
+ check(true, `state db opens (schema v2): ${stateDbPath()}`, "");
2001
+ } catch (err) {
2002
+ check(false, "", `state dir/db problem: ${err.message}`);
2003
+ }
2004
+ const kimi = detectKimiConfig();
2005
+ const hasClaude = claudeDetected();
2006
+ if (kimi.exists || !hasClaude) {
2007
+ check(
2008
+ kimi.exists,
2009
+ `kimi config found: ${kimi.path}`,
2010
+ `kimi config not found (looked at ${kimi.path}); is Kimi Code CLI installed?`
2011
+ );
2012
+ if (kimi.exists) {
2013
+ check(hooksInstalled(kimi.path), "[kimi] hooks managed block present in kimi config", "[kimi] hooks not installed \u2014 run: agentguard install");
2014
+ }
2015
+ } else {
2016
+ console.log(` ${pc.dim("-")} kimi code not detected (skipped)`);
2017
+ }
2018
+ if (hasClaude) {
2019
+ check(
2020
+ claudeHooksInstalled(),
2021
+ `[claude] hooks present in ${claudeSettingsPath()}`,
2022
+ `[claude] hooks not installed \u2014 run: agentguard install --harness claude`
2023
+ );
2024
+ } else {
2025
+ console.log(` ${pc.dim("-")} claude code not detected (skipped)`);
2026
+ }
2027
+ const kimiConfigText = fs8.existsSync(kimi.path) ? fs8.readFileSync(kimi.path, "utf8") : "";
2028
+ const hasSecurityLayer = /kimi-boost managed|destructive|secret-guard|branch-guard|block-dangerous/i.test(kimiConfigText);
2029
+ if (hasSecurityLayer) {
2030
+ ok("security-layer hooks detected (authorization axis covered)");
2031
+ } else {
2032
+ warn("no security-layer hooks detected \u2014 agent-guard covers runtime behavior only (authorization axis). Consider kimi-boost presets: npx kimi-boost install");
2033
+ }
2034
+ const which = spawnSync("agentguard", ["--version"], { encoding: "utf8" });
2035
+ check(
2036
+ which.status === 0 || Boolean(process.argv[1]?.includes("agentguard") || process.argv[1]?.includes("kimi-guard")),
2037
+ "agentguard resolves on PATH",
2038
+ "agentguard is not on PATH \u2014 hook commands will fail-open. Install globally: npm i -g @shidesheng0218/agentguard"
2039
+ );
2040
+ const probeFile = probeLogPath();
2041
+ if (fs8.existsSync(probeFile)) {
2042
+ const lines = fs8.readFileSync(probeFile, "utf8").trim().split("\n").filter(Boolean);
2043
+ ok(`probe log has ${lines.length} samples (${probeFile})`);
2044
+ const keys = /* @__PURE__ */ new Map();
2045
+ for (const line of lines.slice(-50)) {
2046
+ try {
2047
+ const p = JSON.parse(line);
2048
+ for (const k of Object.keys(p.payload ?? {})) keys.set(k, (keys.get(k) ?? 0) + 1);
2049
+ } catch {
2050
+ }
2051
+ }
2052
+ if (keys.size > 0) {
2053
+ console.log(` observed payload keys: ${[...keys.entries()].map(([k, n]) => `${k}(${n})`).join(", ")}`);
2054
+ }
2055
+ } else {
2056
+ warn("no probe samples yet \u2014 run 'kguard probe on', use Kimi Code a bit, then 'kguard doctor'");
2057
+ }
2058
+ if (fs8.existsSync(userConfigPath())) ok(`config present: ${userConfigPath()}`);
2059
+ else warn("no user config (defaults in effect) \u2014 run 'kguard config init' to create one");
2060
+ try {
2061
+ const normalizeMisses = Number(getMeta("normalize_misses") ?? "0");
2062
+ if (normalizeMisses > 0) {
2063
+ const lastMiss = getMeta("last_normalize_miss_ts");
2064
+ warn(
2065
+ `${normalizeMisses} hook payload(s) failed to normalize (last: ${lastMiss ? new Date(Number(lastMiss)).toISOString() : "unknown"}) \u2014 possible upstream schema drift; run 'kguard probe on', use the CLI, then 'kguard probe show'`
2066
+ );
2067
+ } else {
2068
+ ok("hook payload normalization: no misses recorded");
2069
+ }
2070
+ const lastHookTs = Number(getMeta("last_hook_ts") ?? "0");
2071
+ if (hooksInstalled(kimi.path)) {
2072
+ if (lastHookTs === 0) {
2073
+ warn("hooks installed but no hook activity recorded yet \u2014 the guard has never fired in this state db");
2074
+ } else if (Date.now() - lastHookTs > 24 * 36e5 && agentProcessRunning()) {
2075
+ warn(
2076
+ `hooks installed but no hook activity for over 24h while a kimi process is running \u2014 the guard may be silently inert (CLI update? reinstall hooks with 'kguard install')`
2077
+ );
2078
+ } else {
2079
+ ok(`hook activity last seen ${new Date(lastHookTs).toISOString()}`);
2080
+ }
2081
+ }
2082
+ } catch {
2083
+ }
2084
+ if (cfg.verify.veto.enabled) {
2085
+ if (vetoKeyConfigured()) ok(`verify veto: enabled, KIMI_GUARD_VETO_API_KEY present`);
2086
+ else warn("verify veto enabled but KIMI_GUARD_VETO_API_KEY is not set \u2014 the veto is inert at runtime (deterministic gate still works)");
2087
+ } else {
2088
+ ok("verify veto: off (pure deterministic gate)");
2089
+ }
2090
+ if (cfg.budget.precise) {
2091
+ if (preciseKeyConfigured()) ok("budget precise metering: enabled, KIMI_API_KEY present");
2092
+ else warn("budget precise metering enabled but KIMI_API_KEY is not set \u2014 falling back to event-based estimates");
2093
+ }
2094
+ if (failures === 0) console.log("\nAll checks passed.");
2095
+ else console.log(`
2096
+ ${failures} check(s) failed.`);
2097
+ return failures === 0 ? 0 : 1;
2098
+ }
2099
+
2100
+ // src/wire/supervisor.ts
2101
+ import fs9 from "fs";
2102
+ import path6 from "path";
2103
+
2104
+ // src/wire/client.ts
2105
+ import { spawn } from "child_process";
2106
+ import readline from "readline";
2107
+ import { randomUUID } from "crypto";
2108
+
2109
+ // src/wire/protocol.ts
2110
+ var WIRE_PROTOCOL_VERSION = "1.10";
2111
+
2112
+ // src/wire/client.ts
2113
+ var WireClient = class {
2114
+ proc = null;
2115
+ pending = /* @__PURE__ */ new Map();
2116
+ nextId = 0;
2117
+ closed = false;
2118
+ opts;
2119
+ constructor(opts) {
2120
+ this.opts = opts;
2121
+ }
2122
+ get running() {
2123
+ return this.proc !== null && this.proc.exitCode === null;
2124
+ }
2125
+ async start() {
2126
+ const command = this.opts.command ?? ["kimi", "--wire"];
2127
+ this.proc = spawn(command[0], command.slice(1), {
2128
+ cwd: this.opts.cwd,
2129
+ env: { ...process.env, ...this.opts.env },
2130
+ stdio: ["pipe", "pipe", "pipe"]
2131
+ });
2132
+ const rl = readline.createInterface({ input: this.proc.stdout });
2133
+ rl.on("line", (line) => this.handleLine(line));
2134
+ const stderr = readline.createInterface({ input: this.proc.stderr });
2135
+ stderr.on("line", (line) => this.opts.onStderr?.(line));
2136
+ const exited = new Promise((_, reject) => {
2137
+ this.proc.once("exit", (code) => {
2138
+ this.closed = true;
2139
+ for (const p of this.pending.values()) {
2140
+ clearTimeout(p.timer);
2141
+ p.reject(new Error(`wire process exited (code ${code})`));
2142
+ }
2143
+ this.pending.clear();
2144
+ reject(new Error(`wire process exited unexpectedly (code ${code})`));
2145
+ });
2146
+ });
2147
+ exited.catch(() => {
2148
+ });
2149
+ return await this.initialize();
2150
+ }
2151
+ handleLine(line) {
2152
+ const trimmed = line.trim();
2153
+ if (!trimmed) return;
2154
+ this.opts.onRawLine?.("in", trimmed);
2155
+ let msg;
2156
+ try {
2157
+ msg = JSON.parse(trimmed);
2158
+ } catch {
2159
+ return;
2160
+ }
2161
+ if ("id" in msg && msg.id !== void 0 && ("result" in msg || "error" in msg)) {
2162
+ const pending = this.pending.get(msg.id);
2163
+ if (!pending) return;
2164
+ this.pending.delete(msg.id);
2165
+ clearTimeout(pending.timer);
2166
+ const resp = msg;
2167
+ if (resp.error) pending.reject(new Error(`wire error ${resp.error.code}: ${resp.error.message}`));
2168
+ else pending.resolve(resp.result);
2169
+ return;
2170
+ }
2171
+ if ("method" in msg) {
2172
+ if (msg.method === "event") {
2173
+ const params = msg.params;
2174
+ if (params) this.opts.onEvent?.(params.type, params.payload ?? {});
2175
+ return;
2176
+ }
2177
+ if (msg.method === "request") {
2178
+ void this.handleServerRequest(msg);
2179
+ return;
2180
+ }
2181
+ }
2182
+ }
2183
+ async handleServerRequest(msg) {
2184
+ let result;
2185
+ try {
2186
+ result = await this.opts.onRequest?.(msg.params.type, msg.params.payload) ?? {};
2187
+ } catch (err) {
2188
+ result = { error: err.message };
2189
+ }
2190
+ this.write({ jsonrpc: "2.0", id: msg.id, result });
2191
+ }
2192
+ write(msg) {
2193
+ if (!this.proc?.stdin || this.closed) return;
2194
+ const line = JSON.stringify(msg);
2195
+ this.opts.onRawLine?.("out", line);
2196
+ this.proc.stdin.write(line + "\n");
2197
+ }
2198
+ id() {
2199
+ this.nextId++;
2200
+ return `kguard-${this.nextId}-${randomUUID().slice(0, 8)}`;
2201
+ }
2202
+ async request(method, params, timeoutMs = this.opts.requestTimeoutMs ?? 6e5) {
2203
+ const id = this.id();
2204
+ const promise = new Promise((resolve2, reject) => {
2205
+ const timer = setTimeout(() => {
2206
+ this.pending.delete(id);
2207
+ reject(new Error(`wire request timeout: ${method}`));
2208
+ }, timeoutMs);
2209
+ this.pending.set(id, { resolve: resolve2, reject, timer });
2210
+ });
2211
+ this.write({ jsonrpc: "2.0", method, id, params });
2212
+ return promise;
2213
+ }
2214
+ async initialize() {
2215
+ try {
2216
+ return await this.request(
2217
+ "initialize",
2218
+ {
2219
+ protocol_version: WIRE_PROTOCOL_VERSION,
2220
+ client: { name: "agent-guard", version },
2221
+ capabilities: { supports_question: false },
2222
+ hooks: this.opts.hooks ?? []
2223
+ },
2224
+ 3e4
2225
+ );
2226
+ } catch (err) {
2227
+ if (err.message.includes("-32601")) return null;
2228
+ throw err;
2229
+ }
2230
+ }
2231
+ prompt(userInput, timeoutMs) {
2232
+ return this.request("prompt", { user_input: userInput }, timeoutMs);
2233
+ }
2234
+ steer(userInput) {
2235
+ return this.request("steer", { user_input: userInput }, 3e4);
2236
+ }
2237
+ cancel() {
2238
+ return this.request("cancel", {}, 3e4);
2239
+ }
2240
+ stop() {
2241
+ this.closed = true;
2242
+ for (const p of this.pending.values()) {
2243
+ clearTimeout(p.timer);
2244
+ p.reject(new Error("client stopped"));
2245
+ }
2246
+ this.pending.clear();
2247
+ this.proc?.kill("SIGTERM");
2248
+ setTimeout(() => this.proc?.kill("SIGKILL"), 3e3).unref();
2249
+ }
2250
+ };
2251
+
2252
+ // src/wire/supervisor.ts
2253
+ var zeroUsage = () => ({ input_other: 0, output: 0, input_cache_read: 0, input_cache_creation: 0 });
2254
+ function addUsage(acc, u) {
2255
+ acc.input_other += u.input_other ?? 0;
2256
+ acc.output += u.output ?? 0;
2257
+ acc.input_cache_read += u.input_cache_read ?? 0;
2258
+ acc.input_cache_creation += u.input_cache_creation ?? 0;
2259
+ }
2260
+ async function runSupervised(opts) {
2261
+ const cfg = opts.config ?? loadConfig();
2262
+ const runId = `run-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${process.pid}`;
2263
+ const logDir = path6.join(guardHome(), "runs", runId);
2264
+ fs9.mkdirSync(logDir, { recursive: true });
2265
+ const logPath = path6.join(logDir, "wire.jsonl");
2266
+ const rawLog = (direction, line) => {
2267
+ try {
2268
+ fs9.appendFileSync(logPath, JSON.stringify({ dir: direction, ts: Date.now(), line }) + "\n");
2269
+ } catch {
2270
+ }
2271
+ };
2272
+ const startedAt = Date.now();
2273
+ const report = {
2274
+ runId,
2275
+ command: opts.command ?? ["kimi", "--wire"],
2276
+ startedAt: new Date(startedAt).toISOString(),
2277
+ finishedAt: "",
2278
+ durationMs: 0,
2279
+ turns: 0,
2280
+ steps: 0,
2281
+ toolCalls: 0,
2282
+ stepRetries: [],
2283
+ blocks: [],
2284
+ steers: [],
2285
+ approvals: { approved: 0, rejected: 0 },
2286
+ tokenUsage: zeroUsage(),
2287
+ finalStatus: "",
2288
+ endReason: "finished",
2289
+ resumes: 0,
2290
+ verifyRounds: 0,
2291
+ vetoes: 0,
2292
+ thinkingDominance: 0,
2293
+ reportPath: path6.join(logDir, "report.json"),
2294
+ logPath
2295
+ };
2296
+ let steersSent = 0;
2297
+ let cancelled = false;
2298
+ const pendingToolCalls = /* @__PURE__ */ new Map();
2299
+ const seenSubagents = /* @__PURE__ */ new Set();
2300
+ const hintedPatterns = /* @__PURE__ */ new Set();
2301
+ const anchoredSteps = /* @__PURE__ */ new Set();
2302
+ let killSwitchArmed = false;
2303
+ let timeoutTimer = null;
2304
+ let turnThinkChars = 0;
2305
+ let turnTextChars = 0;
2306
+ let turnText = "";
2307
+ const client = new WireClient({
2308
+ command: opts.command,
2309
+ cwd: opts.cwd,
2310
+ env: opts.env,
2311
+ onRawLine: rawLog,
2312
+ hooks: [{ id: "kguard", event: "PreToolUse", matcher: "", timeout: 10 }],
2313
+ onEvent: (type, payload) => {
2314
+ void handleEvent(type, payload);
2315
+ },
2316
+ onRequest: (type, payload) => handleRequest(type, payload)
2317
+ });
2318
+ function sessionId() {
2319
+ return runId;
2320
+ }
2321
+ async function steerOnce(kind, message) {
2322
+ if (!opts.steerOnWarn || steersSent >= opts.maxSteers) return;
2323
+ steersSent++;
2324
+ report.steers.push({ kind, message, ts: Date.now() });
2325
+ try {
2326
+ await client.steer(`[agent-guard] ${message}`);
2327
+ } catch {
2328
+ }
2329
+ }
2330
+ function anchorText() {
2331
+ const goal = opts.prompt.slice(0, cfg.anchor.maxChars);
2332
+ return `goal anchor: the user's task for this run, verbatim: "${goal}". Re-evaluate: does the current work still serve this goal? If you have drifted, get back on target; if the goal is already met, stop and summarize.`;
2333
+ }
2334
+ async function handleEvent(type, payload) {
2335
+ switch (type) {
2336
+ case "SubagentEvent": {
2337
+ handleSubagentEvent(payload);
2338
+ break;
2339
+ }
2340
+ case "StepBegin": {
2341
+ const p = payload;
2342
+ report.steps = p.n;
2343
+ recordEvent(sessionId(), "step", { n: p.n });
2344
+ if (cfg.anchor.enabled && opts.steerOnWarn && p.n > 1 && p.n % cfg.anchor.everyNPrompts === 0 && !anchoredSteps.has(p.n)) {
2345
+ anchoredSteps.add(p.n);
2346
+ await steerOnce("anchor", anchorText());
2347
+ }
2348
+ if (p.n > opts.maxSteps && !cancelled) {
2349
+ cancelled = true;
2350
+ report.endReason = "max_steps";
2351
+ void client.cancel().catch(() => {
2352
+ });
2353
+ }
2354
+ break;
2355
+ }
2356
+ case "CompactionEnd": {
2357
+ recordEvent(sessionId(), "compaction", { phase: "wire" });
2358
+ if (cfg.anchor.enabled && opts.steerOnWarn) {
2359
+ await steerOnce("anchor", `context was just compacted. ${anchorText()}`);
2360
+ }
2361
+ break;
2362
+ }
2363
+ case "StepRetry": {
2364
+ const p = payload;
2365
+ report.stepRetries.push({ n: p.n, error_type: p.error_type, status_code: p.status_code ?? null });
2366
+ recordEvent(sessionId(), "step_retry", { error_type: p.error_type, status_code: p.status_code ?? null });
2367
+ break;
2368
+ }
2369
+ case "ContentPart": {
2370
+ const part = payload;
2371
+ if (part.type === "think" && part.think) turnThinkChars += part.think.length;
2372
+ if (part.type === "text" && part.text) {
2373
+ turnTextChars += part.text.length;
2374
+ turnText = (turnText + part.text).slice(-16e3);
2375
+ }
2376
+ break;
2377
+ }
2378
+ case "TurnBegin": {
2379
+ turnThinkChars = 0;
2380
+ turnTextChars = 0;
2381
+ turnText = "";
2382
+ recordEvent(sessionId(), "turn", { wire: true });
2383
+ break;
2384
+ }
2385
+ case "TurnEnd": {
2386
+ if (cfg.thinking.enabled && turnThinkChars >= cfg.thinking.minThinkChars && turnTextChars / (turnThinkChars + turnTextChars) <= cfg.thinking.maxTextRatio) {
2387
+ report.thinkingDominance++;
2388
+ recordEvent(sessionId(), "thinking_dominance", { think_chars: turnThinkChars, text_chars: turnTextChars });
2389
+ }
2390
+ break;
2391
+ }
2392
+ case "StatusUpdate": {
2393
+ const p = payload;
2394
+ if (p.token_usage) addUsage(report.tokenUsage, p.token_usage);
2395
+ if (cfg.context.enabled && typeof p.context_usage === "number" && p.context_usage * 100 >= cfg.context.warnPercent && !hintedPatterns.has("context-fill")) {
2396
+ hintedPatterns.add("context-fill");
2397
+ await steerOnce(
2398
+ "context",
2399
+ `context window is ${Math.round(p.context_usage * 100)}% full. Wrap up the current unit of work, summarize what you have learned, and avoid starting large new explorations \u2014 compaction is imminent.`
2400
+ );
2401
+ }
2402
+ break;
2403
+ }
2404
+ case "ToolCall": {
2405
+ const p = payload;
2406
+ let args = {};
2407
+ try {
2408
+ args = p.function.arguments ? JSON.parse(p.function.arguments) : {};
2409
+ } catch {
2410
+ args = { _raw: p.function.arguments ?? "" };
2411
+ }
2412
+ pendingToolCalls.set(`main:${p.id}`, { name: p.function.name, args });
2413
+ break;
2414
+ }
2415
+ case "ToolResult": {
2416
+ const p = payload;
2417
+ const call = pendingToolCalls.get(`main:${p.tool_call_id}`);
2418
+ if (!call) break;
2419
+ pendingToolCalls.delete(`main:${p.tool_call_id}`);
2420
+ recordObservedCall(sessionId(), call, p.return_value);
2421
+ break;
2422
+ }
2423
+ default:
2424
+ break;
2425
+ }
2426
+ }
2427
+ function handleSubagentEvent(p) {
2428
+ const agentKey = p.agent_id ?? p.subagent_type ?? "unknown";
2429
+ if (!seenSubagents.has(agentKey)) {
2430
+ seenSubagents.add(agentKey);
2431
+ recordEvent(sessionId(), "subagent", { agent: agentKey });
2432
+ }
2433
+ const nested = p.event;
2434
+ if (!nested?.type) return;
2435
+ switch (nested.type) {
2436
+ case "ToolCall": {
2437
+ const tp = nested.payload;
2438
+ let args = {};
2439
+ try {
2440
+ args = tp.function.arguments ? JSON.parse(tp.function.arguments) : {};
2441
+ } catch {
2442
+ args = { _raw: tp.function.arguments ?? "" };
2443
+ }
2444
+ pendingToolCalls.set(`sub:${agentKey}:${tp.id}`, { name: tp.function.name, args });
2445
+ break;
2446
+ }
2447
+ case "ToolResult": {
2448
+ const tp = nested.payload;
2449
+ const call = pendingToolCalls.get(`sub:${agentKey}:${tp.tool_call_id}`);
2450
+ if (!call) break;
2451
+ pendingToolCalls.delete(`sub:${agentKey}:${tp.tool_call_id}`);
2452
+ recordObservedCall(`${sessionId()}|sub|${agentKey}`, call, tp.return_value);
2453
+ break;
2454
+ }
2455
+ case "StatusUpdate": {
2456
+ const sp = nested.payload;
2457
+ if (sp.token_usage) addUsage(report.tokenUsage, sp.token_usage);
2458
+ break;
2459
+ }
2460
+ case "StepBegin": {
2461
+ recordEvent(`${sessionId()}|sub|${agentKey}`, "step", { n: nested.payload?.n });
2462
+ break;
2463
+ }
2464
+ default:
2465
+ break;
2466
+ }
2467
+ }
2468
+ function recordObservedCall(sessionId2, call, returnValue) {
2469
+ report.toolCalls++;
2470
+ const output = typeof returnValue.output === "string" ? returnValue.output : JSON.stringify(returnValue.output);
2471
+ recordCall({
2472
+ sessionId: sessionId2,
2473
+ toolName: call.name,
2474
+ argsHash: fingerprint(call.name, call.args),
2475
+ argsJson: JSON.stringify(call.args).slice(0, 2048),
2476
+ outputHash: hashOutput(output),
2477
+ filePath: extractFile2(call.args),
2478
+ status: returnValue.is_error ? "failure" : "ok"
2479
+ });
2480
+ }
2481
+ async function handleRequest(type, payload) {
2482
+ switch (type) {
2483
+ case "HookRequest": {
2484
+ const p = payload;
2485
+ return await handleHookRequest(p);
2486
+ }
2487
+ case "ApprovalRequest": {
2488
+ const p = payload;
2489
+ if (opts.approval === "approve") {
2490
+ report.approvals.approved++;
2491
+ return { request_id: p.id, response: "approve" };
2492
+ }
2493
+ report.approvals.rejected++;
2494
+ return {
2495
+ request_id: p.id,
2496
+ response: "reject",
2497
+ feedback: "agent-guard run: interactive approvals are unavailable in supervised headless mode. Continue with non-interactive steps only, or summarize your findings."
2498
+ };
2499
+ }
2500
+ case "QuestionRequest": {
2501
+ const p = payload;
2502
+ return { request_id: p.id, answers: {} };
2503
+ }
2504
+ default:
2505
+ return {};
2506
+ }
2507
+ }
2508
+ async function handleHookRequest(p) {
2509
+ if (killSwitchArmed) {
2510
+ return {
2511
+ request_id: p.id,
2512
+ action: "block",
2513
+ reason: "[agent-guard] CIRCUIT BREAK: this session has hit the intervention limit. Stop making tool calls, summarize your findings, and end the turn."
2514
+ };
2515
+ }
2516
+ if (cancelled) {
2517
+ return { request_id: p.id, action: "allow", reason: "" };
2518
+ }
2519
+ const hookPayload = { ...p.input_data, session_id: sessionId() };
2520
+ const call = hookPayload;
2521
+ const toolName = typeof call.tool_name === "string" ? call.tool_name : p.target;
2522
+ const since = Date.now() - 30 * 6e4;
2523
+ const history = callsSince(sessionId(), since);
2524
+ const args = call.tool_input ?? {};
2525
+ const analysis = analyzeCall(history, { tool: toolName, argsHash: fingerprint(toolName, args), args }, cfg);
2526
+ if (cfg.budget.dispatchTools.includes(toolName)) {
2527
+ await refreshPreciseIfStale(cfg.budget, { ...process.env, ...opts.env });
2528
+ const budgetFinding = evaluateBudgetGate(sessionId(), cfg.budget, Date.now());
2529
+ if (budgetFinding) analysis.findings.unshift(budgetFinding);
2530
+ }
2531
+ const block = analysis.findings.find((f) => f.severity === "block");
2532
+ if (block) {
2533
+ recordBlock(sessionId(), toolName, block.kind);
2534
+ report.blocks.push({ tool: toolName, kind: block.kind, message: block.message, ts: Date.now() });
2535
+ const blocksInSession = countBlocks(sessionId(), Date.now() - cfg.policy.blockWindowMinutes * 6e4);
2536
+ if (cfg.policy.killSwitch && blocksInSession >= cfg.policy.maxBlocksPerSession) {
2537
+ killSwitchArmed = true;
2538
+ report.endReason = "kill-switch";
2539
+ cancelled = true;
2540
+ void client.cancel().catch(() => {
2541
+ });
2542
+ }
2543
+ return { request_id: p.id, action: "block", reason: `[agent-guard] Blocked (${block.kind}): ${block.message}` };
2544
+ }
2545
+ const warn2 = analysis.findings.find((f) => f.severity === "warn");
2546
+ if (warn2) {
2547
+ const key = `${warn2.kind}:${toolName}`;
2548
+ if (!hintedPatterns.has(key)) {
2549
+ hintedPatterns.add(key);
2550
+ await steerOnce(warn2.kind, warn2.message);
2551
+ }
2552
+ }
2553
+ return { request_id: p.id, action: "allow", reason: "" };
2554
+ }
2555
+ try {
2556
+ openDb();
2557
+ await client.start();
2558
+ recordEvent(sessionId(), "run_start", { prompt: opts.prompt.slice(0, 200) });
2559
+ timeoutTimer = setTimeout(() => {
2560
+ if (cancelled) return;
2561
+ cancelled = true;
2562
+ report.endReason = "timeout";
2563
+ void client.cancel().catch(() => {
2564
+ });
2565
+ }, opts.maxMinutes * 6e4);
2566
+ let attempt = 0;
2567
+ let currentPrompt = opts.prompt;
2568
+ while (true) {
2569
+ attempt++;
2570
+ report.turns++;
2571
+ const result = await client.prompt(currentPrompt, opts.maxMinutes * 6e4 + 6e4);
2572
+ report.finalStatus = result.status;
2573
+ if (result.status === "finished") {
2574
+ if (cfg.verify.enabled && report.verifyRounds < opts.maxVerifyRounds) {
2575
+ const claims = findClaims(turnText, cfg);
2576
+ if (claims.length > 0 && !hasEvidence(sessionId(), cfg)) {
2577
+ if (cfg.verify.veto.enabled) {
2578
+ const ctx = {
2579
+ ...collectVetoContext(sessionId(), cfg),
2580
+ claims,
2581
+ goal: opts.prompt
2582
+ };
2583
+ const env = { ...process.env, ...opts.env };
2584
+ const vote = await castVetoVote(ctx, cfg.verify.veto, env);
2585
+ if (vote.vetoed) {
2586
+ report.vetoes++;
2587
+ recordEvent(sessionId(), "veto", { claims: claims.length, raw: vote.raw });
2588
+ break;
2589
+ }
2590
+ if (vote.error) recordEvent(sessionId(), "veto_error", { error: vote.error });
2591
+ }
2592
+ report.verifyRounds++;
2593
+ report.endReason = "verify";
2594
+ recordEvent(sessionId(), "verify_gate", { claims: claims.length });
2595
+ captureCheckpoint(sessionId(), "verify-gate", Date.now(), cfg);
2596
+ currentPrompt = WIRE_VERIFY_CORRECTIVE;
2597
+ continue;
2598
+ }
2599
+ }
2600
+ if (report.endReason === "verify") report.endReason = "finished";
2601
+ break;
2602
+ }
2603
+ if (result.status === "max_steps_reached" && attempt <= opts.autoResume) {
2604
+ report.resumes++;
2605
+ const brief = captureCheckpoint(sessionId(), "auto-resume", Date.now(), cfg);
2606
+ const d6Note = report.thinkingDominance > 0 ? " Note: the previous turns were dominated by thinking with little action \u2014 act more, think less." : "";
2607
+ currentPrompt = (brief ? `You were stopped at the step limit. Observed state so far:
2608
+
2609
+ ${brief.brief}
2610
+
2611
+ ` : "") + "You reached the step limit. Continue from where you stopped \u2014 do not repeat work already done." + d6Note;
2612
+ continue;
2613
+ }
2614
+ if (result.status === "cancelled" && report.endReason === "finished") {
2615
+ report.endReason = killSwitchArmed ? "kill-switch" : "timeout";
2616
+ }
2617
+ break;
2618
+ }
2619
+ if (killSwitchArmed) {
2620
+ captureCheckpoint(sessionId(), "kill-switch", Date.now(), cfg);
2621
+ }
2622
+ } catch (err) {
2623
+ report.endReason = "error";
2624
+ const msg = err.message;
2625
+ report.finalStatus = /ENOENT|spawn/i.test(msg) ? `${msg} \u2014 is the agent CLI installed and on PATH? Or pass --exec <command...>` : msg;
2626
+ } finally {
2627
+ if (timeoutTimer) clearTimeout(timeoutTimer);
2628
+ client.stop();
2629
+ }
2630
+ report.durationMs = Date.now() - startedAt;
2631
+ report.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2632
+ recordEvent(sessionId(), "run_end", { endReason: report.endReason, blocks: report.blocks.length });
2633
+ try {
2634
+ fs9.writeFileSync(report.reportPath, JSON.stringify(report, null, 2), "utf8");
2635
+ } catch {
2636
+ }
2637
+ return report;
2638
+ }
2639
+ function extractFile2(args) {
2640
+ if (args === null || typeof args !== "object") return null;
2641
+ const obj = args;
2642
+ for (const k of ["file_path", "filePath", "path", "file", "filename"]) {
2643
+ const v = obj[k];
2644
+ if (typeof v === "string" && v) return v;
2645
+ }
2646
+ return null;
2647
+ }
2648
+ function formatReport(r) {
2649
+ const dur = Math.round(r.durationMs / 1e3);
2650
+ const lines = [
2651
+ `agent-guard run report`,
2652
+ ` run id: ${r.runId}`,
2653
+ ` command: ${r.command.join(" ")}`,
2654
+ ` duration: ${dur}s steps: ${r.steps} tool calls: ${r.toolCalls} turns: ${r.turns}${r.resumes > 0 ? ` (resumed \xD7${r.resumes})` : ""}`,
2655
+ ` end: ${r.endReason} (last prompt status: ${r.finalStatus})`,
2656
+ ` blocks: ${r.blocks.length === 0 ? "none" : r.blocks.map((b) => `${b.kind}(${b.tool})`).join(", ")}`,
2657
+ ` steers: ${r.steers.length === 0 ? "none" : String(r.steers.length)}`,
2658
+ r.verifyRounds > 0 ? ` verify: ${r.verifyRounds} corrective round(s) for unbacked completion claims` : "",
2659
+ r.vetoes > 0 ? ` veto: ${r.vetoes} false-positive veto vote(s) accepted the completion` : "",
2660
+ r.thinkingDominance > 0 ? ` thinking: ${r.thinkingDominance} thinking-dominated turn(s) flagged` : "",
2661
+ ` approvals: ${r.approvals.approved} approved, ${r.approvals.rejected} rejected`,
2662
+ ` tokens: in ${r.tokenUsage.input_other + r.tokenUsage.input_cache_read + r.tokenUsage.input_cache_creation} (cache read ${r.tokenUsage.input_cache_read}) / out ${r.tokenUsage.output}`,
2663
+ r.stepRetries.length > 0 ? ` retries: ${r.stepRetries.length} (last: ${r.stepRetries[r.stepRetries.length - 1]?.error_type})` : "",
2664
+ ` report: ${r.reportPath}`,
2665
+ ` wire log: ${r.logPath}`
2666
+ ];
2667
+ return lines.filter(Boolean).join("\n");
2668
+ }
2669
+
2670
+ // src/cli.ts
2671
+ var program = new Command();
2672
+ program.name("agentguard").description("Runtime behavior guard for coding agents (Kimi Code CLI & Claude Code): loop detection, quota gates, checkpoints").version(version, "-V, --version", "print version");
2673
+ program.command("install").description("install hook rules into detected agent CLIs (Kimi Code config.toml and/or Claude Code settings.json)").option("--compat", "legacy-safe mode: only the 3 universally supported hook events (for older kimi-cli versions)").option("--harness <name>", "kimi | claude | all (default: auto-detect installed harnesses, fallback kimi)").action((opts) => {
2674
+ const which = opts.harness ?? "auto";
2675
+ const doKimi = which === "kimi" || which === "all" || which === "auto";
2676
+ const doClaude = which === "claude" || which === "all" || which === "auto" && claudeDetected();
2677
+ if (!doKimi && !doClaude) {
2678
+ console.error(`unknown harness: ${which}`);
2679
+ process.exitCode = 1;
2680
+ return;
2681
+ }
2682
+ if (doKimi) {
2683
+ const r = installHooks("agentguard", Boolean(opts.compat));
2684
+ console.log(`\u2713 [kimi] config: ${r.configPath}${r.created ? " (created)" : ""}`);
2685
+ if (r.backupPath) console.log(`\u2713 [kimi] backup: ${r.backupPath}`);
2686
+ console.log(`\u2713 [kimi] managed hook block ${r.replaced ? "updated" : "added"}${opts.compat ? " (compat)" : ""}`);
2687
+ if (!opts.compat) {
2688
+ console.log(" note: if your CLI fails to load config after this (older kimi-cli), reinstall with: agentguard install --compat");
2689
+ }
2690
+ }
2691
+ if (doClaude) {
2692
+ const r = installClaudeHooks("agentguard", Boolean(opts.compat));
2693
+ console.log(`\u2713 [claude] config: ${r.configPath}${r.created ? " (created)" : ""}`);
2694
+ if (r.backupPath) console.log(`\u2713 [claude] backup: ${r.backupPath}`);
2695
+ console.log(`\u2713 [claude] hooks ${r.updated ? "installed" : "already up to date"}${opts.compat ? " (compat)" : ""}`);
2696
+ }
2697
+ console.log(" restart the agent CLI (or /reload) to take effect.");
2698
+ });
2699
+ program.command("uninstall").description("remove the managed hook entries from Kimi Code config.toml and Claude Code settings.json").option("--harness <name>", "kimi | claude | all (default: all)").action((opts) => {
2700
+ const which = opts.harness ?? "all";
2701
+ if (which === "kimi" || which === "all") {
2702
+ const r = uninstallHooks();
2703
+ console.log(r.removed ? `\u2713 [kimi] removed managed block from ${r.configPath}` : `[kimi] no managed block found in ${r.configPath}`);
2704
+ }
2705
+ if (which === "claude" || which === "all") {
2706
+ const r = uninstallClaudeHooks();
2707
+ console.log(r.removed ? `\u2713 [claude] removed hooks from ${r.configPath}` : `[claude] no managed hooks found in ${r.configPath}`);
2708
+ }
2709
+ });
2710
+ program.command("hook").argument("<event>", "hook event name, e.g. PreToolUse").description("hook entrypoint invoked by the agent CLI (reads JSON payload from stdin)").option("--harness <name>", "kimi | claude (default: kimi)", "kimi").action(async (event, opts) => {
2711
+ const harness = opts.harness === "claude" ? "claude" : "kimi";
2712
+ process.exitCode = await runHook(event, harness);
2713
+ });
2714
+ program.command("status").description("guard activity: calls, interventions, sessions, budget windows").action(() => cmdStatus());
2715
+ program.command("doctor").description("verify environment: node, state db, kimi config, PATH, probe samples").action(() => {
2716
+ process.exitCode = cmdDoctor();
2717
+ });
2718
+ program.command("budget").description("show the quota metering snapshot (windows, burn rate, projection)").option("-s, --session <id>", "session id (defaults to the most recent)").action(async (opts) => {
2719
+ const cfg = loadConfig();
2720
+ if (cfg.budget.precise) {
2721
+ const p = await refreshPreciseUsage(cfg.budget);
2722
+ if (!p) console.error("[agent-guard] precise metering unavailable (missing KIMI_API_KEY or API error) \u2014 showing event-based estimates");
2723
+ }
2724
+ const sid = opts.session ?? latestSessionId() ?? "unknown";
2725
+ console.log(formatSnapshot(budgetSnapshot(sid, cfg.budget)));
2726
+ const limits = resolveLimits(cfg.budget);
2727
+ if (limits.weekly === 0) {
2728
+ console.log(`
2729
+ available plans: ${Object.keys(PLANS).join(", ")} \u2014 or set weekly/fiveHour in config`);
2730
+ }
2731
+ });
2732
+ program.command("checkpoint").description("capture a research-state checkpoint for a session (also auto-captured on failures/interrupts)").option("-s, --session <id>", "session id (defaults to the most recent)").option("-r, --reason <text>", "why the checkpoint is being taken", "manual").action((opts) => {
2733
+ const sid = opts.session ?? latestSessionId();
2734
+ if (!sid) {
2735
+ console.log("no recorded sessions yet");
2736
+ return;
2737
+ }
2738
+ const cp = captureCheckpoint(sid, opts.reason);
2739
+ if (!cp) {
2740
+ console.log("nothing to checkpoint (no recent activity)");
2741
+ return;
2742
+ }
2743
+ console.log(`\u2713 checkpoint saved: ${cp.path}`);
2744
+ console.log(` resume later with: kguard resume`);
2745
+ });
2746
+ program.command("resume").description("print a paste-ready context block built from the latest checkpoint").option("-f, --file <path>", "use a specific checkpoint file (defaults to the latest)").action((opts) => {
2747
+ const file = opts.file ?? latestCheckpointFile();
2748
+ if (!file || !fs10.existsSync(file)) {
2749
+ console.log("no checkpoints found \u2014 run 'kguard checkpoint' first");
2750
+ return;
2751
+ }
2752
+ const content = fs10.readFileSync(file, "utf8");
2753
+ const reason = /- reason: (.*)/.exec(content)?.[1] ?? "interrupted";
2754
+ const idx = content.indexOf("## Observed activity");
2755
+ const brief = idx >= 0 ? content.slice(idx) : content;
2756
+ console.log(renderResumeBlock(brief, reason));
2757
+ });
2758
+ program.command("blocks").description("list recent guard blocks (with ids for feedback)").option("-n, --last <n>", "how many blocks to show", "20").action((opts) => {
2759
+ const rows = listBlocks(Number(opts.last));
2760
+ if (rows.length === 0) {
2761
+ console.log("no blocks recorded yet");
2762
+ return;
2763
+ }
2764
+ for (const r of rows) {
2765
+ const fb = r.feedback ? ` [${r.feedback === "fp" ? "FALSE POSITIVE" : "confirmed"}]` : "";
2766
+ console.log(`#${r.id} ${new Date(r.ts).toISOString()} ${r.kind} ${r.tool_name} session=${r.session_id.slice(0, 16)}${fb}`);
2767
+ }
2768
+ console.log(`
2769
+ mark a false positive: kguard feedback fp <id> (confirmed: kguard feedback tp <id>)`);
2770
+ });
2771
+ program.command("feedback").description("mark a block as a false positive (fp) or confirmed (tp) \u2014 feeds detector calibration").argument("<verdict>", "fp | tp").argument("<id>", "block id from 'kguard blocks' or the block message").action((verdict, id) => {
2772
+ if (verdict !== "fp" && verdict !== "tp") {
2773
+ console.error("verdict must be 'fp' (false positive) or 'tp' (confirmed)");
2774
+ process.exitCode = 1;
2775
+ return;
2776
+ }
2777
+ if (setBlockFeedback(Number(id), verdict)) {
2778
+ console.log(`\u2713 block #${id} marked ${verdict === "fp" ? "false positive" : "confirmed"}`);
2779
+ } else {
2780
+ console.error(`no block with id ${id} \u2014 see 'kguard blocks'`);
2781
+ process.exitCode = 1;
2782
+ }
2783
+ });
2784
+ program.command("report").description("anonymized aggregate of guard activity (no args/paths/commands \u2014 safe to share)").option("--json", "print JSON (default is a text summary)").action((opts) => {
2785
+ const report = buildGuardReport();
2786
+ if (opts.json) {
2787
+ console.log(JSON.stringify({ ...report, version }, null, 2));
2788
+ return;
2789
+ }
2790
+ console.log(`agent-guard report (${report.generatedAt})`);
2791
+ console.log(` sessions: ${report.sessions} calls 24h: ${report.calls24h} blocks 24h: ${report.blocks24h}`);
2792
+ if (report.detectors.length === 0) console.log(" detectors: no blocks recorded");
2793
+ for (const d of report.detectors) {
2794
+ console.log(` ${d.kind.padEnd(12)} blocks=${d.blocks} fp=${d.falsePositives} (${Math.round(d.fpRate * 100)}%) tp=${d.confirmed}`);
2795
+ }
2796
+ console.log(` budget: ${report.budget.plan}${report.budget.precise ? " (precise)" : ""} 5h=${report.budget.fiveHourPercent}% weekly=${report.budget.weeklyPercent}%`);
2797
+ console.log(" (aggregate only \u2014 no arguments, paths or commands are included)");
2798
+ });
2799
+ var probe = program.command("probe").description("capture raw hook payloads for schema discovery");
2800
+ probe.command("on").action(() => {
2801
+ setMeta("probe_enabled", "1");
2802
+ console.log(`\u2713 probe on \u2192 ${probeLogPath()}`);
2803
+ });
2804
+ probe.command("off").action(() => {
2805
+ setMeta("probe_enabled", "0");
2806
+ console.log("\u2713 probe off");
2807
+ });
2808
+ probe.command("show").option("-n, --last <n>", "how many samples to show", "10").action((opts) => {
2809
+ if (!fs10.existsSync(probeLogPath())) {
2810
+ console.log("no probe samples yet \u2014 run 'kguard probe on' first");
2811
+ return;
2812
+ }
2813
+ const lines = fs10.readFileSync(probeLogPath(), "utf8").trim().split("\n").filter(Boolean);
2814
+ for (const line of lines.slice(-Number(opts.last))) console.log(line);
2815
+ });
2816
+ var cfgCmd = program.command("config").description("manage the guard config.toml");
2817
+ cfgCmd.command("init").description("create the config file with documented defaults").action(() => {
2818
+ console.log(writeConfigTemplate() ? `\u2713 created ${userConfigPath()}` : `already exists: ${userConfigPath()}`);
2819
+ });
2820
+ cfgCmd.command("path").action(() => {
2821
+ console.log(userConfigPath());
2822
+ });
2823
+ cfgCmd.command("show").description("print the effective config (file values merged over defaults)").action(() => {
2824
+ console.log(JSON.stringify(loadConfig(), null, 2));
2825
+ });
2826
+ cfgCmd.command("get <key>").description("print one effective value, e.g. budget.plan or repeat.maxRepeats").action((key) => {
2827
+ const cfg = loadConfig();
2828
+ const value = key.split(".").reduce((acc, k) => acc && typeof acc === "object" ? acc[k] : void 0, cfg);
2829
+ console.log(value === void 0 ? `<unset: ${key}>` : typeof value === "object" ? JSON.stringify(value) : String(value));
2830
+ });
2831
+ program.command("run").description("supervised headless run: spawns the agent in Wire mode, enforces loop guards, meters tokens, auto-checkpoints").argument("[prompt...]", "task prompt (or use --prompt)").option("-p, --prompt <text>", "task prompt").option("-e, --exec <command...>", "agent command to supervise (default: kimi --wire)").option("--max-steps <n>", "hard step cap (per turn)", "200").option("--max-minutes <n>", "hard wall-clock cap for the whole run", "30").option("--auto-resume <n>", "re-prompt with checkpoint brief after max_steps/kill-switch", "0").option("--max-verify-rounds <n>", "corrective rounds when the final message makes unbacked completion claims", "2").option("--no-steer", "disable soft mid-turn corrections").option("--max-steers <n>", "cap on steer injections", "5").option("--yolo", "auto-approve every approval request").option("--json", "print machine-readable report JSON").action(async (promptParts, opts) => {
2832
+ const prompt = opts.prompt ?? promptParts.join(" ");
2833
+ if (!prompt.trim()) {
2834
+ console.error("error: a prompt is required (argument or --prompt)");
2835
+ process.exit(1);
2836
+ }
2837
+ const report = await runSupervised({
2838
+ prompt,
2839
+ command: opts.exec ?? ["kimi", "--wire"],
2840
+ maxSteps: Number(opts.maxSteps),
2841
+ maxMinutes: Number(opts.maxMinutes),
2842
+ steerOnWarn: opts.steer,
2843
+ maxSteers: Number(opts.maxSteers),
2844
+ autoResume: Number(opts.autoResume),
2845
+ maxVerifyRounds: Number(opts.maxVerifyRounds),
2846
+ approval: opts.yolo ? "approve" : "reject",
2847
+ json: Boolean(opts.json)
2848
+ });
2849
+ if (opts.json) console.log(JSON.stringify(report, null, 2));
2850
+ else console.log(formatReport(report));
2851
+ process.exitCode = report.endReason === "finished" ? 0 : 2;
2852
+ });
2853
+ program.parseAsync(process.argv).catch((err) => {
2854
+ process.stderr.write(`[agent-guard] ${err.message}
2855
+ `);
2856
+ process.exit(1);
2857
+ });