@hiro-c/agent-gate 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,6 +76,7 @@ Full options: see [docs/config.md](docs/config.md) (TODO) or `AgentGatePluginCon
76
76
  | `agent-gate install` / `uninstall` | Register or remove the Claude Code hook |
77
77
  | `agent-gate lint [--ai]` | Audit instruction files for ambiguity, emptiness, missing rules. `--ai` adds AI-driven contradiction / ambiguity / missing-imperative checks |
78
78
  | `agent-gate stats` | Summarize the decision log (after `AGENT_GATE_LOG=1`) |
79
+ | `agent-gate suggest` | Surface rule candidates from repeated AI blocks and stale built-in rules |
79
80
  | `agent-gate daemon` | Long-lived server on a Unix socket (opt-in speedup, set `AGENT_GATE_DAEMON=1`) |
80
81
 
81
82
  ## Environment
@@ -1 +1 @@
1
- {"version":3,"file":"agent-gate.d.ts","sourceRoot":"","sources":["../../src/cli/agent-gate.ts"],"names":[],"mappings":";AAIA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAA;AAYtE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AA4C7C,wBAAsB,GAAG,CACvB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,CAAC,gBAAgB,CAAC,CAE3B;AAuHD,UAAU,UAAU;IAClB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,OAAO,CAAA;IACpB,EAAE,EAAE,OAAO,CAAA;CACZ;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAqCpD"}
1
+ {"version":3,"file":"agent-gate.d.ts","sourceRoot":"","sources":["../../src/cli/agent-gate.ts"],"names":[],"mappings":";AAIA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAA;AAYtE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AA+C7C,wBAAsB,GAAG,CACvB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,CAAC,gBAAgB,CAAC,CAE3B;AAuHD,UAAU,UAAU;IAClB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,OAAO,CAAA;IACpB,EAAE,EAAE,OAAO,CAAA;CACZ;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAqCpD"}
@@ -8,7 +8,9 @@ const processHookData_1 = require("../hooks/processHookData");
8
8
  const installer_1 = require("./installer");
9
9
  const adapters_1 = require("../adapters");
10
10
  const stats_1 = require("../observability/stats");
11
+ const suggest_1 = require("../observability/suggest");
11
12
  const decisionLogger_1 = require("../observability/decisionLogger");
13
+ const defaultRules_1 = require("../deterministic/defaultRules");
12
14
  const collectRuleSources_1 = require("../collector/collectRuleSources");
13
15
  const lintRuleSources_1 = require("../doctor/lintRuleSources");
14
16
  const lintRuleSourcesWithAi_1 = require("../doctor/lintRuleSourcesWithAi");
@@ -28,6 +30,7 @@ Usage:
28
30
  agent-gate install Register the hook in ~/.claude/settings.json
29
31
  agent-gate uninstall Remove the hook from ~/.claude/settings.json
30
32
  agent-gate stats Summarize decisions from the log file
33
+ agent-gate suggest Surface rule candidates and stale rules from the decision log
31
34
  agent-gate lint [--ai] Audit CLAUDE.md / AGENTS.md / etc. for AI-friendliness
32
35
  (--ai adds AI-driven contradiction / ambiguity / missing-imperative checks)
33
36
  agent-gate daemon Start the long-lived daemon (Unix socket)
@@ -212,6 +215,9 @@ function main() {
212
215
  case 'stats':
213
216
  runStats();
214
217
  return;
218
+ case 'suggest':
219
+ runSuggest();
220
+ return;
215
221
  case 'lint':
216
222
  void runLint(parsedArgs.ai);
217
223
  return;
@@ -228,6 +234,17 @@ function runStats() {
228
234
  const stats = (0, stats_1.readStats)((0, decisionLogger_1.defaultLogPath)());
229
235
  console.log((0, stats_1.formatStats)(stats));
230
236
  }
237
+ function runSuggest() {
238
+ const windowDays = parseInt(process.env.AGENT_GATE_SUGGEST_WINDOW_DAYS ?? '7', 10);
239
+ const minPatternCount = parseInt(process.env.AGENT_GATE_SUGGEST_MIN_COUNT ?? '3', 10);
240
+ const knownRuleIds = defaultRules_1.defaultDeterministicRules.map((r) => r.id);
241
+ const suggestions = (0, suggest_1.suggestRules)((0, decisionLogger_1.defaultLogPath)(), {
242
+ windowDays: Number.isNaN(windowDays) ? 7 : windowDays,
243
+ minPatternCount: Number.isNaN(minPatternCount) ? 3 : minPatternCount,
244
+ knownRuleIds,
245
+ });
246
+ console.log((0, suggest_1.formatSuggestions)(suggestions));
247
+ }
231
248
  async function runLint(useAi) {
232
249
  const cwd = process.cwd();
233
250
  const sources = (0, collectRuleSources_1.collectRuleSources)(cwd);
package/dist/index.d.ts CHANGED
@@ -34,6 +34,8 @@ export { sendToDaemon } from './daemon/client';
34
34
  export type { SendToDaemonOptions } from './daemon/client';
35
35
  export { defaultSocketPath as defaultDaemonSocketPath, } from './daemon/protocol';
36
36
  export type { DaemonRequest, DaemonResponse, DaemonErrorResponse, } from './daemon/protocol';
37
+ export { suggestRules, formatSuggestions } from './observability/suggest';
38
+ export type { Suggestion, SuggestionKind, SuggestOptions, } from './observability/suggest';
37
39
  export { EventBus } from './observability/eventBus';
38
40
  export { JsonlFileSink } from './observability/sinks/JsonlFileSink';
39
41
  export type { PipelineEvent, RuleFiredEvent, AiRequestedEvent, AiCompletedEvent, VerdictDecidedEvent, PipelineErrorEvent, Sink, } from './observability/sinks/Sink';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.processHookData = exports.validator = exports.collectRuleSources = exports.JsonlFileSink = exports.EventBus = exports.defaultDaemonSocketPath = exports.sendToDaemon = exports.DaemonServer = exports.DecisionCache = exports.formatFindings = exports.lintRuleSourcesWithAi = exports.lintRuleSources = exports.AgentSdkClient = exports.CompositeModelClient = exports.cursorAdapter = exports.claudeCodeAdapter = exports.buildDefaultDeterministicRules = exports.defaultDeterministicRules = exports.forbidFilePathPattern = exports.forbidContentPattern = exports.forbidCommandPattern = exports.HookDataSchema = exports.loadPluginConfig = exports.loadAgentGateConfig = exports.defineConfig = exports.Config = void 0;
3
+ exports.processHookData = exports.validator = exports.collectRuleSources = exports.JsonlFileSink = exports.EventBus = exports.formatSuggestions = exports.suggestRules = exports.defaultDaemonSocketPath = exports.sendToDaemon = exports.DaemonServer = exports.DecisionCache = exports.formatFindings = exports.lintRuleSourcesWithAi = exports.lintRuleSources = exports.AgentSdkClient = exports.CompositeModelClient = exports.cursorAdapter = exports.claudeCodeAdapter = exports.buildDefaultDeterministicRules = exports.defaultDeterministicRules = exports.forbidFilePathPattern = exports.forbidContentPattern = exports.forbidCommandPattern = exports.HookDataSchema = exports.loadPluginConfig = exports.loadAgentGateConfig = exports.defineConfig = exports.Config = void 0;
4
4
  // Config
5
5
  var Config_1 = require("./config/Config");
6
6
  Object.defineProperty(exports, "Config", { enumerable: true, get: function () { return Config_1.Config; } });
@@ -48,6 +48,9 @@ Object.defineProperty(exports, "sendToDaemon", { enumerable: true, get: function
48
48
  var protocol_1 = require("./daemon/protocol");
49
49
  Object.defineProperty(exports, "defaultDaemonSocketPath", { enumerable: true, get: function () { return protocol_1.defaultSocketPath; } });
50
50
  // Observability
51
+ var suggest_1 = require("./observability/suggest");
52
+ Object.defineProperty(exports, "suggestRules", { enumerable: true, get: function () { return suggest_1.suggestRules; } });
53
+ Object.defineProperty(exports, "formatSuggestions", { enumerable: true, get: function () { return suggest_1.formatSuggestions; } });
51
54
  var eventBus_1 = require("./observability/eventBus");
52
55
  Object.defineProperty(exports, "EventBus", { enumerable: true, get: function () { return eventBus_1.EventBus; } });
53
56
  var JsonlFileSink_1 = require("./observability/sinks/JsonlFileSink");
@@ -0,0 +1,20 @@
1
+ export type SuggestionKind = 'add-rule' | 'disable-rule' | 'maintain';
2
+ export interface Suggestion {
3
+ kind: SuggestionKind;
4
+ toolName?: string;
5
+ reasonExcerpt?: string;
6
+ ruleId?: string;
7
+ count: number;
8
+ message: string;
9
+ }
10
+ export interface SuggestOptions {
11
+ /** Only consider log entries within this many days. */
12
+ windowDays: number;
13
+ /** Minimum repetitions before suggesting an add-rule. Default 3. */
14
+ minPatternCount?: number;
15
+ /** Known deterministic rule ids to evaluate for stale-rule suggestions. */
16
+ knownRuleIds?: string[];
17
+ }
18
+ export declare function suggestRules(logPath: string, options: SuggestOptions): Suggestion[];
19
+ export declare function formatSuggestions(suggestions: Suggestion[]): string;
20
+ //# sourceMappingURL=suggest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suggest.d.ts","sourceRoot":"","sources":["../../src/observability/suggest.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,cAAc,GAAG,UAAU,CAAA;AAErE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACxB;AAyCD,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,cAAc,GACtB,UAAU,EAAE,CA2Dd;AAED,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,MAAM,CA0BnE"}
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.suggestRules = suggestRules;
4
+ exports.formatSuggestions = formatSuggestions;
5
+ const fs_1 = require("fs");
6
+ function readEntries(logPath) {
7
+ if (!(0, fs_1.existsSync)(logPath))
8
+ return [];
9
+ const content = (0, fs_1.readFileSync)(logPath, 'utf-8');
10
+ const out = [];
11
+ for (const line of content.split('\n')) {
12
+ const t = line.trim();
13
+ if (!t)
14
+ continue;
15
+ try {
16
+ out.push(JSON.parse(t));
17
+ }
18
+ catch {
19
+ // skip malformed
20
+ }
21
+ }
22
+ return out;
23
+ }
24
+ function withinWindow(timestamp, cutoff) {
25
+ const t = Date.parse(timestamp);
26
+ if (Number.isNaN(t))
27
+ return false;
28
+ return t >= cutoff;
29
+ }
30
+ function reasonKey(reason) {
31
+ // Group by the first ~80 chars of the reason; longer divergence
32
+ // probably reflects per-call detail, not a distinct pattern.
33
+ return reason.trim().slice(0, 80);
34
+ }
35
+ function trimExcerpt(reason) {
36
+ const trimmed = reason.trim();
37
+ return trimmed.length > 120 ? trimmed.slice(0, 117) + '...' : trimmed;
38
+ }
39
+ function suggestRules(logPath, options) {
40
+ const entries = readEntries(logPath);
41
+ if (entries.length === 0)
42
+ return [];
43
+ const cutoff = Date.now() - options.windowDays * 86400_000;
44
+ const recent = entries.filter((e) => typeof e.timestamp === 'string' && withinWindow(e.timestamp, cutoff));
45
+ const minCount = options.minPatternCount ?? 3;
46
+ const suggestions = [];
47
+ // add-rule: AI blocks that repeat
48
+ const buckets = new Map();
49
+ for (const e of recent) {
50
+ if (e.decision !== 'block')
51
+ continue;
52
+ if (e.source !== 'ai')
53
+ continue;
54
+ const key = `${e.toolName}::${reasonKey(e.reason)}`;
55
+ const existing = buckets.get(key);
56
+ if (existing) {
57
+ existing.count++;
58
+ }
59
+ else {
60
+ buckets.set(key, {
61
+ toolName: e.toolName,
62
+ reasonExcerpt: trimExcerpt(e.reason),
63
+ count: 1,
64
+ });
65
+ }
66
+ }
67
+ for (const b of buckets.values()) {
68
+ if (b.count < minCount)
69
+ continue;
70
+ suggestions.push({
71
+ kind: 'add-rule',
72
+ toolName: b.toolName,
73
+ reasonExcerpt: b.reasonExcerpt,
74
+ count: b.count,
75
+ message: `The AI has blocked ${b.count} ${b.toolName} calls with this reason. Consider promoting it to a deterministic rule to skip the AI roundtrip.`,
76
+ });
77
+ }
78
+ // disable-rule: known rules that did not fire in the window
79
+ if (options.knownRuleIds && options.knownRuleIds.length > 0) {
80
+ const firedRecent = new Set();
81
+ for (const e of recent) {
82
+ if (e.source === 'deterministic' && e.ruleId) {
83
+ firedRecent.add(e.ruleId);
84
+ }
85
+ }
86
+ for (const rid of options.knownRuleIds) {
87
+ if (firedRecent.has(rid))
88
+ continue;
89
+ suggestions.push({
90
+ kind: 'disable-rule',
91
+ ruleId: rid,
92
+ count: 0,
93
+ message: `Rule "${rid}" has not fired in the last ${options.windowDays} days. If your project never triggers it, you can disable it in .agent-gate.config.{ts,json} to reduce noise.`,
94
+ });
95
+ }
96
+ }
97
+ return suggestions;
98
+ }
99
+ function formatSuggestions(suggestions) {
100
+ if (suggestions.length === 0) {
101
+ return 'No suggestions. Nothing to do.';
102
+ }
103
+ const lines = [];
104
+ let i = 1;
105
+ for (const s of suggestions) {
106
+ if (s.kind === 'add-rule') {
107
+ lines.push(`${i}. [add-rule] ${s.toolName ?? '?'} blocked ${s.count} times`);
108
+ if (s.reasonExcerpt) {
109
+ lines.push(` reason: ${s.reasonExcerpt}`);
110
+ }
111
+ lines.push(` ${s.message}`);
112
+ }
113
+ else if (s.kind === 'disable-rule') {
114
+ lines.push(`${i}. [disable-rule] ${s.ruleId ?? '?'} (0 fires)`);
115
+ lines.push(` ${s.message}`);
116
+ }
117
+ else {
118
+ lines.push(`${i}. [${s.kind}] ${s.message}`);
119
+ }
120
+ lines.push('');
121
+ i++;
122
+ }
123
+ lines.push(`${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}.`);
124
+ return lines.join('\n');
125
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hiro-c/agent-gate",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Runtime rule enforcer for AI coding agents. Reads CLAUDE.md / AGENTS.md / .cursorrules and enforces them via Claude Code and Cursor hooks, with a deterministic safety baseline.",
5
5
  "author": "Hiro-Chiba",
6
6
  "license": "MIT",