@hiro-c/agent-gate 1.4.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 +2 -1
- package/dist/cli/agent-gate.d.ts +1 -0
- package/dist/cli/agent-gate.d.ts.map +1 -1
- package/dist/cli/agent-gate.js +41 -5
- package/dist/doctor/findings.d.ts +1 -1
- package/dist/doctor/findings.d.ts.map +1 -1
- package/dist/doctor/lintRuleSourcesWithAi.d.ts +5 -0
- package/dist/doctor/lintRuleSourcesWithAi.d.ts.map +1 -0
- package/dist/doctor/lintRuleSourcesWithAi.js +111 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/observability/suggest.d.ts +20 -0
- package/dist/observability/suggest.d.ts.map +1 -0
- package/dist/observability/suggest.js +125 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,8 +74,9 @@ Full options: see [docs/config.md](docs/config.md) (TODO) or `AgentGatePluginCon
|
|
|
74
74
|
|---|---|
|
|
75
75
|
| `agent-gate` | Run as hook (reads stdin, used internally) |
|
|
76
76
|
| `agent-gate install` / `uninstall` | Register or remove the Claude Code hook |
|
|
77
|
-
| `agent-gate lint` | Audit instruction files for ambiguity, emptiness, missing rules |
|
|
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
|
package/dist/cli/agent-gate.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/cli/agent-gate.js
CHANGED
|
@@ -8,10 +8,16 @@ 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");
|
|
16
|
+
const lintRuleSourcesWithAi_1 = require("../doctor/lintRuleSourcesWithAi");
|
|
14
17
|
const formatFindings_1 = require("../doctor/formatFindings");
|
|
18
|
+
const Config_1 = require("../config/Config");
|
|
19
|
+
const AnthropicApi_1 = require("../validation/models/AnthropicApi");
|
|
20
|
+
const ClaudeCli_1 = require("../validation/models/ClaudeCli");
|
|
15
21
|
const server_1 = require("../daemon/server");
|
|
16
22
|
const client_1 = require("../daemon/client");
|
|
17
23
|
const protocol_1 = require("../daemon/protocol");
|
|
@@ -24,7 +30,9 @@ Usage:
|
|
|
24
30
|
agent-gate install Register the hook in ~/.claude/settings.json
|
|
25
31
|
agent-gate uninstall Remove the hook from ~/.claude/settings.json
|
|
26
32
|
agent-gate stats Summarize decisions from the log file
|
|
27
|
-
agent-gate
|
|
33
|
+
agent-gate suggest Surface rule candidates and stale rules from the decision log
|
|
34
|
+
agent-gate lint [--ai] Audit CLAUDE.md / AGENTS.md / etc. for AI-friendliness
|
|
35
|
+
(--ai adds AI-driven contradiction / ambiguity / missing-imperative checks)
|
|
28
36
|
agent-gate daemon Start the long-lived daemon (Unix socket)
|
|
29
37
|
agent-gate --help Show this help
|
|
30
38
|
agent-gate --version Show version
|
|
@@ -146,6 +154,7 @@ function parseArgs(args) {
|
|
|
146
154
|
let agentId = adapters_1.DEFAULT_ADAPTER_ID;
|
|
147
155
|
let showHelp = false;
|
|
148
156
|
let showVersion = false;
|
|
157
|
+
let ai = false;
|
|
149
158
|
const positional = [];
|
|
150
159
|
for (let i = 0; i < args.length; i++) {
|
|
151
160
|
const a = args[i];
|
|
@@ -161,6 +170,10 @@ function parseArgs(args) {
|
|
|
161
170
|
agentId = a.slice('--agent='.length);
|
|
162
171
|
continue;
|
|
163
172
|
}
|
|
173
|
+
if (a === '--ai') {
|
|
174
|
+
ai = true;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
164
177
|
if (a === '--help' || a === '-h' || a === 'help') {
|
|
165
178
|
showHelp = true;
|
|
166
179
|
continue;
|
|
@@ -171,7 +184,7 @@ function parseArgs(args) {
|
|
|
171
184
|
}
|
|
172
185
|
positional.push(a);
|
|
173
186
|
}
|
|
174
|
-
return { positional, agentId, showHelp, showVersion };
|
|
187
|
+
return { positional, agentId, showHelp, showVersion, ai };
|
|
175
188
|
}
|
|
176
189
|
function main() {
|
|
177
190
|
const parsedArgs = parseArgs(process.argv.slice(2));
|
|
@@ -202,8 +215,11 @@ function main() {
|
|
|
202
215
|
case 'stats':
|
|
203
216
|
runStats();
|
|
204
217
|
return;
|
|
218
|
+
case 'suggest':
|
|
219
|
+
runSuggest();
|
|
220
|
+
return;
|
|
205
221
|
case 'lint':
|
|
206
|
-
runLint();
|
|
222
|
+
void runLint(parsedArgs.ai);
|
|
207
223
|
return;
|
|
208
224
|
case 'daemon':
|
|
209
225
|
void runDaemon();
|
|
@@ -218,13 +234,33 @@ function runStats() {
|
|
|
218
234
|
const stats = (0, stats_1.readStats)((0, decisionLogger_1.defaultLogPath)());
|
|
219
235
|
console.log((0, stats_1.formatStats)(stats));
|
|
220
236
|
}
|
|
221
|
-
function
|
|
222
|
-
const
|
|
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
|
+
}
|
|
248
|
+
async function runLint(useAi) {
|
|
249
|
+
const cwd = process.cwd();
|
|
250
|
+
const sources = (0, collectRuleSources_1.collectRuleSources)(cwd);
|
|
223
251
|
if (sources.length === 0) {
|
|
224
252
|
console.log('No instruction files found (looked for CLAUDE.md, AGENTS.md, .cursorrules, .cursor/rules/*.mdc, .clinerules/*.md, .windsurf/rules/*.md, .github/copilot-instructions.md, CONVENTIONS.md).');
|
|
225
253
|
return;
|
|
226
254
|
}
|
|
227
255
|
const findings = (0, lintRuleSources_1.lintRuleSources)(sources);
|
|
256
|
+
if (useAi) {
|
|
257
|
+
const config = new Config_1.Config();
|
|
258
|
+
const client = config.useApi
|
|
259
|
+
? new AnthropicApi_1.AnthropicApi(config)
|
|
260
|
+
: new ClaudeCli_1.ClaudeCli(config, cwd);
|
|
261
|
+
const aiFindings = await (0, lintRuleSourcesWithAi_1.lintRuleSourcesWithAi)(sources, client);
|
|
262
|
+
findings.push(...aiFindings);
|
|
263
|
+
}
|
|
228
264
|
console.log((0, formatFindings_1.formatFindings)(findings));
|
|
229
265
|
const hasError = findings.some((f) => f.severity === 'error');
|
|
230
266
|
if (hasError) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RuleSourceKind } from '../contracts/types/RuleSource';
|
|
2
2
|
export type Severity = 'info' | 'warning' | 'error';
|
|
3
|
-
export type FindingCode = 'empty-file' | 'ambiguous-modifier' | 'no-concrete-rules';
|
|
3
|
+
export type FindingCode = 'empty-file' | 'ambiguous-modifier' | 'no-concrete-rules' | 'ambiguity' | 'contradiction' | 'missing-imperative';
|
|
4
4
|
export interface Finding {
|
|
5
5
|
ruleSourcePath: string;
|
|
6
6
|
ruleSourceKind: RuleSourceKind;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findings.d.ts","sourceRoot":"","sources":["../../src/doctor/findings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAE9D,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAA;AAEnD,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,oBAAoB,GACpB,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"findings.d.ts","sourceRoot":"","sources":["../../src/doctor/findings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAE9D,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAA;AAEnD,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,oBAAoB,GACpB,mBAAmB,GACnB,WAAW,GACX,eAAe,GACf,oBAAoB,CAAA;AAExB,MAAM,WAAW,OAAO;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,cAAc,CAAA;IAC9B,QAAQ,EAAE,QAAQ,CAAA;IAClB,IAAI,EAAE,WAAW,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { RuleSource } from '../contracts/types/RuleSource';
|
|
2
|
+
import { IModelClient } from '../contracts/types/ModelClient';
|
|
3
|
+
import { Finding } from './findings';
|
|
4
|
+
export declare function lintRuleSourcesWithAi(sources: RuleSource[], client: IModelClient): Promise<Finding[]>;
|
|
5
|
+
//# sourceMappingURL=lintRuleSourcesWithAi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lintRuleSourcesWithAi.d.ts","sourceRoot":"","sources":["../../src/doctor/lintRuleSourcesWithAi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAkB,MAAM,+BAA+B,CAAA;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAC7D,OAAO,EAAE,OAAO,EAAyB,MAAM,YAAY,CAAA;AA8E3D,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,UAAU,EAAE,EACrB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,OAAO,EAAE,CAAC,CA4CpB"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.lintRuleSourcesWithAi = lintRuleSourcesWithAi;
|
|
4
|
+
const PROMPT_HEADER = `You are auditing AI agent instruction files for issues that
|
|
5
|
+
make rules hard for an AI to enforce reliably. Look at the sources below
|
|
6
|
+
and report any of the following:
|
|
7
|
+
|
|
8
|
+
- "contradiction": two or more rules across one or more files that
|
|
9
|
+
conflict. Cite both/all sides in the excerpt.
|
|
10
|
+
- "ambiguity": a rule that uses vague language (e.g. "where possible",
|
|
11
|
+
"as needed", "appropriately") with no concrete threshold or condition.
|
|
12
|
+
- "missing-imperative": a rule expressed as a wish or description rather
|
|
13
|
+
than an imperative the AI can act on.
|
|
14
|
+
|
|
15
|
+
Respond as a JSON array of objects with this shape:
|
|
16
|
+
[
|
|
17
|
+
{
|
|
18
|
+
"code": "contradiction" | "ambiguity" | "missing-imperative",
|
|
19
|
+
"ruleSourcePath": "<one of the paths shown>",
|
|
20
|
+
"line": <1-indexed line number in that file, or null>,
|
|
21
|
+
"message": "<concise explanation>",
|
|
22
|
+
"excerpt": "<short literal text from the file>"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
If there are no issues, respond with []. Output ONLY the JSON array. No
|
|
27
|
+
markdown, no prose, no commentary.`;
|
|
28
|
+
const ALLOWED_CODES = [
|
|
29
|
+
'ambiguity',
|
|
30
|
+
'contradiction',
|
|
31
|
+
'missing-imperative',
|
|
32
|
+
];
|
|
33
|
+
const CODE_SEVERITY = {
|
|
34
|
+
'empty-file': 'warning',
|
|
35
|
+
'ambiguous-modifier': 'info',
|
|
36
|
+
'no-concrete-rules': 'warning',
|
|
37
|
+
ambiguity: 'info',
|
|
38
|
+
contradiction: 'warning',
|
|
39
|
+
'missing-imperative': 'info',
|
|
40
|
+
};
|
|
41
|
+
function buildPrompt(sources) {
|
|
42
|
+
const blocks = sources.map((s) => `--- path: ${s.path} (kind: ${s.kind}) ---\n${s.content}`);
|
|
43
|
+
return `${PROMPT_HEADER}\n\n${blocks.join('\n\n')}`;
|
|
44
|
+
}
|
|
45
|
+
function extractJsonArray(raw) {
|
|
46
|
+
const trimmed = raw.trim();
|
|
47
|
+
if (trimmed.startsWith('['))
|
|
48
|
+
return trimmed;
|
|
49
|
+
// Code fence variants
|
|
50
|
+
const fenced = trimmed.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
|
51
|
+
if (fenced)
|
|
52
|
+
return fenced[1].trim();
|
|
53
|
+
// Plain extraction: first [ to last ]
|
|
54
|
+
const start = trimmed.indexOf('[');
|
|
55
|
+
const end = trimmed.lastIndexOf(']');
|
|
56
|
+
if (start !== -1 && end > start)
|
|
57
|
+
return trimmed.slice(start, end + 1);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function pathToKind(path, sources) {
|
|
61
|
+
const match = sources.find((s) => s.path === path);
|
|
62
|
+
return match ? match.kind : null;
|
|
63
|
+
}
|
|
64
|
+
async function lintRuleSourcesWithAi(sources, client) {
|
|
65
|
+
if (sources.length === 0)
|
|
66
|
+
return [];
|
|
67
|
+
let response;
|
|
68
|
+
try {
|
|
69
|
+
response = await client.ask(buildPrompt(sources));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const jsonStr = extractJsonArray(response);
|
|
75
|
+
if (!jsonStr)
|
|
76
|
+
return [];
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(jsonStr);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(parsed))
|
|
85
|
+
return [];
|
|
86
|
+
const findings = [];
|
|
87
|
+
for (const raw of parsed) {
|
|
88
|
+
if (!raw || typeof raw !== 'object')
|
|
89
|
+
continue;
|
|
90
|
+
const code = raw.code;
|
|
91
|
+
if (!code || !ALLOWED_CODES.includes(code))
|
|
92
|
+
continue;
|
|
93
|
+
if (typeof raw.ruleSourcePath !== 'string')
|
|
94
|
+
continue;
|
|
95
|
+
const kind = pathToKind(raw.ruleSourcePath, sources);
|
|
96
|
+
if (!kind)
|
|
97
|
+
continue;
|
|
98
|
+
findings.push({
|
|
99
|
+
ruleSourcePath: raw.ruleSourcePath,
|
|
100
|
+
ruleSourceKind: kind,
|
|
101
|
+
severity: CODE_SEVERITY[code],
|
|
102
|
+
code,
|
|
103
|
+
message: typeof raw.message === 'string' ? raw.message : '',
|
|
104
|
+
line: typeof raw.line === 'number' && Number.isFinite(raw.line)
|
|
105
|
+
? raw.line
|
|
106
|
+
: undefined,
|
|
107
|
+
excerpt: typeof raw.excerpt === 'string' ? raw.excerpt : undefined,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return findings;
|
|
111
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type { CompositeModelClientOptions } from './validation/models/CompositeM
|
|
|
23
23
|
export { AgentSdkClient } from './validation/models/AgentSdkClient';
|
|
24
24
|
export type { AgentSdkClientOptions, AgentSdkQueryFn, } from './validation/models/AgentSdkClient';
|
|
25
25
|
export { lintRuleSources } from './doctor/lintRuleSources';
|
|
26
|
+
export { lintRuleSourcesWithAi } from './doctor/lintRuleSourcesWithAi';
|
|
26
27
|
export { formatFindings } from './doctor/formatFindings';
|
|
27
28
|
export type { Finding, FindingCode, Severity, } from './doctor/findings';
|
|
28
29
|
export { DecisionCache } from './cache/DecisionCache';
|
|
@@ -33,6 +34,8 @@ export { sendToDaemon } from './daemon/client';
|
|
|
33
34
|
export type { SendToDaemonOptions } from './daemon/client';
|
|
34
35
|
export { defaultSocketPath as defaultDaemonSocketPath, } from './daemon/protocol';
|
|
35
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';
|
|
36
39
|
export { EventBus } from './observability/eventBus';
|
|
37
40
|
export { JsonlFileSink } from './observability/sinks/JsonlFileSink';
|
|
38
41
|
export type { PipelineEvent, RuleFiredEvent, AiRequestedEvent, AiCompletedEvent, VerdictDecidedEvent, PipelineErrorEvent, Sink, } from './observability/sinks/Sink';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACxC,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpD,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAClE,YAAY,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAG9D,YAAY,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAA;AAC1E,YAAY,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAC1D,YAAY,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AACjE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAC9E,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAClE,YAAY,EACV,cAAc,EACd,YAAY,GACb,MAAM,kCAAkC,CAAA;AAGzC,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAA;AAGnE,YAAY,EACV,iBAAiB,EACjB,WAAW,GACZ,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACL,yBAAyB,EACzB,8BAA8B,GAC/B,MAAM,8BAA8B,CAAA;AAGrC,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACzD,YAAY,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0CAA0C,CAAA;AAC/E,YAAY,EAAE,2BAA2B,EAAE,MAAM,0CAA0C,CAAA;AAC3F,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAA;AACnE,YAAY,EACV,qBAAqB,EACrB,eAAe,GAChB,MAAM,oCAAoC,CAAA;AAG3C,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AACxD,YAAY,EACV,OAAO,EACP,WAAW,EACX,QAAQ,GACT,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,YAAY,EACV,QAAQ,EACR,oBAAoB,GACrB,MAAM,uBAAuB,CAAA;AAG9B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EACL,iBAAiB,IAAI,uBAAuB,GAC7C,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,aAAa,EACb,cAAc,EACd,mBAAmB,GACpB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAA;AACnE,YAAY,EACV,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,IAAI,GACL,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACxC,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpD,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAClE,YAAY,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAG9D,YAAY,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAA;AAC1E,YAAY,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAC1D,YAAY,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AACjE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAC9E,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAClE,YAAY,EACV,cAAc,EACd,YAAY,GACb,MAAM,kCAAkC,CAAA;AAGzC,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAA;AAGnE,YAAY,EACV,iBAAiB,EACjB,WAAW,GACZ,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACL,yBAAyB,EACzB,8BAA8B,GAC/B,MAAM,8BAA8B,CAAA;AAGrC,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACzD,YAAY,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0CAA0C,CAAA;AAC/E,YAAY,EAAE,2BAA2B,EAAE,MAAM,0CAA0C,CAAA;AAC3F,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAA;AACnE,YAAY,EACV,qBAAqB,EACrB,eAAe,GAChB,MAAM,oCAAoC,CAAA;AAG3C,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAA;AACtE,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AACxD,YAAY,EACV,OAAO,EACP,WAAW,EACX,QAAQ,GACT,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,YAAY,EACV,QAAQ,EACR,oBAAoB,GACrB,MAAM,uBAAuB,CAAA;AAG9B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EACL,iBAAiB,IAAI,uBAAuB,GAC7C,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,aAAa,EACb,cAAc,EACd,mBAAmB,GACpB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AACzE,YAAY,EACV,UAAU,EACV,cAAc,EACd,cAAc,GACf,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAA;AACnE,YAAY,EACV,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,IAAI,GACL,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA"}
|
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.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; } });
|
|
@@ -33,6 +33,8 @@ Object.defineProperty(exports, "AgentSdkClient", { enumerable: true, get: functi
|
|
|
33
33
|
// Doctor (CLAUDE.md linter)
|
|
34
34
|
var lintRuleSources_1 = require("./doctor/lintRuleSources");
|
|
35
35
|
Object.defineProperty(exports, "lintRuleSources", { enumerable: true, get: function () { return lintRuleSources_1.lintRuleSources; } });
|
|
36
|
+
var lintRuleSourcesWithAi_1 = require("./doctor/lintRuleSourcesWithAi");
|
|
37
|
+
Object.defineProperty(exports, "lintRuleSourcesWithAi", { enumerable: true, get: function () { return lintRuleSourcesWithAi_1.lintRuleSourcesWithAi; } });
|
|
36
38
|
var formatFindings_1 = require("./doctor/formatFindings");
|
|
37
39
|
Object.defineProperty(exports, "formatFindings", { enumerable: true, get: function () { return formatFindings_1.formatFindings; } });
|
|
38
40
|
// Cache
|
|
@@ -46,6 +48,9 @@ Object.defineProperty(exports, "sendToDaemon", { enumerable: true, get: function
|
|
|
46
48
|
var protocol_1 = require("./daemon/protocol");
|
|
47
49
|
Object.defineProperty(exports, "defaultDaemonSocketPath", { enumerable: true, get: function () { return protocol_1.defaultSocketPath; } });
|
|
48
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; } });
|
|
49
54
|
var eventBus_1 = require("./observability/eventBus");
|
|
50
55
|
Object.defineProperty(exports, "EventBus", { enumerable: true, get: function () { return eventBus_1.EventBus; } });
|
|
51
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.
|
|
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",
|