@chimix/agentscan 0.1.0 → 0.2.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 +27 -17
- package/dist/cli.js +13128 -0
- package/package.json +7 -5
- package/src/analyze.ts +0 -82
- package/src/checks/budgets.ts +0 -189
- package/src/checks/index.ts +0 -738
- package/src/checks/make.ts +0 -37
- package/src/cli.ts +0 -230
- package/src/commands/check.ts +0 -64
- package/src/commands/explain.ts +0 -64
- package/src/commands/init.ts +0 -42
- package/src/commands/rules.ts +0 -36
- package/src/config/load.ts +0 -26
- package/src/config/schema.ts +0 -63
- package/src/discover/index.ts +0 -888
- package/src/facts/extract.ts +0 -273
- package/src/facts/semver.ts +0 -41
- package/src/facts/types.ts +0 -113
- package/src/report/clipboard.ts +0 -90
- package/src/report/exit-code.ts +0 -36
- package/src/report/json.ts +0 -36
- package/src/report/prompt.ts +0 -102
- package/src/report/safe.ts +0 -0
- package/src/report/score.ts +0 -47
- package/src/report/sort.ts +0 -26
- package/src/report/text.ts +0 -142
- package/src/version.ts +0 -1
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chimix/agentscan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Find broken agent configuration in a project — hooks whose scripts are gone, MCP servers that cannot start, skills that disagree with their lockfile",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"agentscan": "./
|
|
7
|
+
"agentscan": "./dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"publishConfig": {
|
|
10
10
|
"access": "public"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"
|
|
13
|
+
"dist",
|
|
14
14
|
"README.md",
|
|
15
15
|
"LICENSE"
|
|
16
16
|
],
|
|
@@ -34,14 +34,16 @@
|
|
|
34
34
|
},
|
|
35
35
|
"author": "Sima Alexandru <sima.alex33@gmail.com>",
|
|
36
36
|
"scripts": {
|
|
37
|
-
"preinstall": "node -e \"try{require('child_process').execSync('bun --version',{stdio:'ignore'})}catch{console.error('\\n agentscan runs on Bun. Install it: https://bun.sh\\n (the CLI will not start without it)\\n')}\"",
|
|
38
37
|
"agentscan": "bun run src/cli.ts",
|
|
39
38
|
"typecheck": "tsc --noEmit",
|
|
40
39
|
"test": "bun test",
|
|
41
40
|
"check": "bun run src/cli.ts check",
|
|
42
|
-
"spec:check": "bun run scripts/spec-drift.ts"
|
|
41
|
+
"spec:check": "bun run scripts/spec-drift.ts",
|
|
42
|
+
"build": "bun build src/cli.ts --target=node --outfile dist/cli.js --banner '#!/usr/bin/env node'",
|
|
43
|
+
"prepublishOnly": "bun run build"
|
|
43
44
|
},
|
|
44
45
|
"engines": {
|
|
46
|
+
"node": ">=20.11.0",
|
|
45
47
|
"bun": ">=1.1.0"
|
|
46
48
|
},
|
|
47
49
|
"license": "MIT",
|
package/src/analyze.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { runChecks } from "./checks/index";
|
|
3
|
-
import { loadConfig } from "./config/load";
|
|
4
|
-
import type { AgentscanConfig } from "./config/schema";
|
|
5
|
-
import { resolveRoot } from "./discover/index";
|
|
6
|
-
import { extractFacts } from "./facts/extract";
|
|
7
|
-
import type { Facts, Finding } from "./facts/types";
|
|
8
|
-
import { sortFindings } from "./report/sort";
|
|
9
|
-
|
|
10
|
-
export type AnalyzeOptions = {
|
|
11
|
-
dir?: string;
|
|
12
|
-
global?: boolean;
|
|
13
|
-
configPath?: string;
|
|
14
|
-
failOn?: AgentscanConfig["failOn"];
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export type Analysis = {
|
|
18
|
-
root: string;
|
|
19
|
-
/** Set only when the requested directory was not itself the project root. */
|
|
20
|
-
resolvedFrom?: string;
|
|
21
|
-
config: AgentscanConfig;
|
|
22
|
-
facts: Facts;
|
|
23
|
-
findings: Finding[];
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* The one definition of "what is wrong with this project".
|
|
28
|
-
*
|
|
29
|
-
* Every command that reports on a project goes through here — `explain`
|
|
30
|
-
* silently missed every structural finding while it built its own pipeline.
|
|
31
|
-
*/
|
|
32
|
-
export function analyze(options: AnalyzeOptions = {}): Analysis {
|
|
33
|
-
const requested = resolve(options.dir ?? process.cwd());
|
|
34
|
-
const root = resolveRoot(requested);
|
|
35
|
-
const loaded = loadConfig(root, options.configPath);
|
|
36
|
-
|
|
37
|
-
const config: AgentscanConfig = {
|
|
38
|
-
...loaded,
|
|
39
|
-
...(options.failOn !== undefined ? { failOn: options.failOn } : {}),
|
|
40
|
-
...(options.global !== undefined ? { includeGlobal: options.global } : {}),
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const includeGlobal = options.global ?? config.includeGlobal;
|
|
44
|
-
const facts = extractFacts(root, config, { includeGlobal });
|
|
45
|
-
|
|
46
|
-
const ignoredSkills = new Set(config.ignoreSkills);
|
|
47
|
-
const ignoredRules = new Set(config.ignoreRules);
|
|
48
|
-
|
|
49
|
-
const structural = runChecks(facts, {
|
|
50
|
-
requireLock: config.requireLock,
|
|
51
|
-
skillDescriptionBytes: config.thresholds.skillDescriptionBytes,
|
|
52
|
-
budgets: {
|
|
53
|
-
agentsMdLines: config.thresholds.agentsMdLines,
|
|
54
|
-
claudeMdLines: config.thresholds.claudeMdLines,
|
|
55
|
-
agents: config.thresholds.agents,
|
|
56
|
-
mcp: config.thresholds.mcp,
|
|
57
|
-
},
|
|
58
|
-
}).filter(
|
|
59
|
-
(f) =>
|
|
60
|
-
!ignoredRules.has(f.ruleId) &&
|
|
61
|
-
!(
|
|
62
|
-
f.subject.startsWith("skill:") &&
|
|
63
|
-
ignoredSkills.has(f.subject.slice("skill:".length))
|
|
64
|
-
),
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// Suppression by exact finding id. Without it a single false positive costs
|
|
68
|
-
// an entire check via ignoreRules — and under the README's CI recipe that is
|
|
69
|
-
// the only way to get a build green again.
|
|
70
|
-
const ignoredFindings = new Set(config.ignoreFindings);
|
|
71
|
-
const findings = sortFindings(
|
|
72
|
-
structural.filter((f) => !ignoredFindings.has(f.id)),
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
root,
|
|
77
|
-
...(root === requested ? {} : { resolvedFrom: requested }),
|
|
78
|
-
config,
|
|
79
|
-
facts,
|
|
80
|
-
findings,
|
|
81
|
-
};
|
|
82
|
-
}
|
package/src/checks/budgets.ts
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
|
-
import type { Facts, Finding } from "../facts/types";
|
|
3
|
-
import { make } from "./make";
|
|
4
|
-
|
|
5
|
-
type PolicyFile = Facts["policyFiles"][number];
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Budget checks: the config is well-formed, but there is too much of it.
|
|
9
|
-
*
|
|
10
|
-
* These used to be five YAML files evaluated by a 581-line rule engine. Across
|
|
11
|
-
* 21 real projects that engine produced 9 findings from 3 of its 5 rules, while
|
|
12
|
-
* every actionable finding came from the structural checks next door. The
|
|
13
|
-
* engine's one remaining job was letting users write their own rules — a
|
|
14
|
-
* feature nobody had used, on a tool with no users, whose concrete case
|
|
15
|
-
* (retuning a budget) `thresholds` in `.agentscanrc.json` already covered
|
|
16
|
-
* without any YAML. It was deleted; see plans/010.
|
|
17
|
-
*
|
|
18
|
-
* Every finding these emit is byte-identical to what the rules produced,
|
|
19
|
-
* including the `rule:` subjects, which is why the ids look the way they do.
|
|
20
|
-
*
|
|
21
|
-
* All of them are **info**. They are judgement calls about size, sourced to
|
|
22
|
-
* docs/spec/thresholds.md, not defects — a project can sit over any of these
|
|
23
|
-
* budgets deliberately. Nothing here should ever fail a build.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
type BudgetOptions = {
|
|
27
|
-
agentsMdLines: number;
|
|
28
|
-
claudeMdLines: number;
|
|
29
|
-
agents: number;
|
|
30
|
-
mcp: number;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Lines the way `wc -l` counts them: a trailing newline terminates the last
|
|
35
|
-
* line, it does not start another one.
|
|
36
|
-
*/
|
|
37
|
-
function lineCount(text: string): number {
|
|
38
|
-
return text.length === 0 ? 0 : text.replace(/\r?\n$/, "").split(/\r?\n/).length;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** First policy file with this basename that is over `limit`, if any. */
|
|
42
|
-
function overBudget(
|
|
43
|
-
facts: Facts,
|
|
44
|
-
file: string,
|
|
45
|
-
limit: number,
|
|
46
|
-
): { policy: PolicyFile; lines: number } | null {
|
|
47
|
-
for (const policy of facts.policyFiles) {
|
|
48
|
-
if (basename(policy.path) !== file) {
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
const lines = lineCount(policy.text);
|
|
52
|
-
if (lines > limit) {
|
|
53
|
-
return { policy, lines };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function policyLengthFinding(args: {
|
|
60
|
-
ruleId: string;
|
|
61
|
-
file: string;
|
|
62
|
-
limit: number;
|
|
63
|
-
facts: Facts;
|
|
64
|
-
reason: string;
|
|
65
|
-
suggest: string;
|
|
66
|
-
}): Finding[] {
|
|
67
|
-
const hit = overBudget(args.facts, args.file, args.limit);
|
|
68
|
-
if (hit === null) {
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
return [
|
|
72
|
-
make(args.ruleId, `rule:${args.ruleId}`, {
|
|
73
|
-
action: "warn",
|
|
74
|
-
severity: "info",
|
|
75
|
-
message: `${args.file} is long (${hit.lines} lines > ${args.limit})`,
|
|
76
|
-
reason: args.reason,
|
|
77
|
-
evidence: [
|
|
78
|
-
{ kind: "policy", value: hit.policy.path },
|
|
79
|
-
{ kind: "count", value: `lines=${hit.lines}` },
|
|
80
|
-
{ kind: "threshold", value: String(args.limit) },
|
|
81
|
-
],
|
|
82
|
-
suggest: args.suggest,
|
|
83
|
-
}),
|
|
84
|
-
];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function countFinding(args: {
|
|
88
|
-
ruleId: string;
|
|
89
|
-
count: number;
|
|
90
|
-
limit: number;
|
|
91
|
-
message: string;
|
|
92
|
-
countLabel: string;
|
|
93
|
-
reason: string;
|
|
94
|
-
suggest: string;
|
|
95
|
-
}): Finding[] {
|
|
96
|
-
if (args.count <= args.limit) {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
return [
|
|
100
|
-
make(args.ruleId, `rule:${args.ruleId}`, {
|
|
101
|
-
action: "warn",
|
|
102
|
-
severity: "info",
|
|
103
|
-
message: args.message,
|
|
104
|
-
reason: args.reason,
|
|
105
|
-
evidence: [
|
|
106
|
-
{ kind: "count", value: `${args.countLabel}=${args.count}` },
|
|
107
|
-
{ kind: "threshold", value: String(args.limit) },
|
|
108
|
-
],
|
|
109
|
-
suggest: args.suggest,
|
|
110
|
-
}),
|
|
111
|
-
];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* The package manager a policy file tells an agent to use, versus the one the
|
|
116
|
-
* project actually declares. Narrow on purpose: it fires only on the exact
|
|
117
|
-
* string `npm install` in a project whose `packageManager` is bun, because a
|
|
118
|
-
* looser match would flag every doc that mentions npm in passing.
|
|
119
|
-
*/
|
|
120
|
-
function packageManagerDrift(facts: Facts): Finding[] {
|
|
121
|
-
if (facts.packageManager !== "bun") {
|
|
122
|
-
return [];
|
|
123
|
-
}
|
|
124
|
-
const hits = facts.policyFiles.filter((p) => p.text.includes("npm install"));
|
|
125
|
-
if (hits.length === 0) {
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
|
-
return [
|
|
129
|
-
make("policy.package-manager-drift", "policy:package-manager", {
|
|
130
|
-
action: "drift",
|
|
131
|
-
severity: "info",
|
|
132
|
-
message: "Policy mentions npm install but packageManager is bun",
|
|
133
|
-
reason: "Docs drift vs packageManager field / lockfile convention",
|
|
134
|
-
evidence: [
|
|
135
|
-
{ kind: "packageManager", value: facts.packageManager },
|
|
136
|
-
...hits.map((p) => ({ kind: "policy" as const, value: p.path })),
|
|
137
|
-
],
|
|
138
|
-
suggest: "Prefer bun install in AGENTS.md / CLAUDE.md",
|
|
139
|
-
}),
|
|
140
|
-
];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function runBudgets(facts: Facts, options: BudgetOptions): Finding[] {
|
|
144
|
-
return [
|
|
145
|
-
...policyLengthFinding({
|
|
146
|
-
ruleId: "budget.agents-md",
|
|
147
|
-
file: "AGENTS.md",
|
|
148
|
-
limit: options.agentsMdLines,
|
|
149
|
-
facts,
|
|
150
|
-
suggest:
|
|
151
|
-
"Delete rather than reorganise — keep only what an agent cannot infer from the code. Or set thresholds.agentsMdLines in .agentscanrc.json",
|
|
152
|
-
reason:
|
|
153
|
-
"Measured across 2,500+ repositories: past ~150 lines additional content delivers diminishing returns and raises inference cost 20-23% without improving agent performance. Unnecessary requirements actively hurt, by broadening exploration. A lean file is 40-60 lines of commands and boundaries.",
|
|
154
|
-
}),
|
|
155
|
-
...policyLengthFinding({
|
|
156
|
-
ruleId: "budget.claude-md",
|
|
157
|
-
file: "CLAUDE.md",
|
|
158
|
-
limit: options.claudeMdLines,
|
|
159
|
-
facts,
|
|
160
|
-
suggest:
|
|
161
|
-
"Keep only what is true every session; move the rest into a skill, which loads on demand. Or set thresholds.claudeMdLines in .agentscanrc.json",
|
|
162
|
-
reason:
|
|
163
|
-
"Frontier models reliably follow roughly 150-200 instructions, and Claude Code's own system prompt already spends about 50 of them. Past that the file is skim-read rather than obeyed. Imports do not help: imported files load at launch too, so splitting organises the text without reducing what competes for attention.",
|
|
164
|
-
}),
|
|
165
|
-
...countFinding({
|
|
166
|
-
ruleId: "budget.agents",
|
|
167
|
-
count: facts.agents.length,
|
|
168
|
-
limit: options.agents,
|
|
169
|
-
countLabel: "agents",
|
|
170
|
-
message: `Agent roster large (${facts.agents.length} > ${options.agents} definitions)`,
|
|
171
|
-
reason:
|
|
172
|
-
"Guidance converges on three or four specialised agents being the productive ceiling — past that, output drops rather than rises. That is about agents you run, not files on disk, so this counts definitions as a proxy: each one's description is loaded so the main session can choose between them, and a long roster dilutes that choice the same way a long skill list does. See docs/spec/thresholds.md.",
|
|
173
|
-
suggest:
|
|
174
|
-
"Archive definitions you do not dispatch, or set thresholds.agents in .agentscanrc.json",
|
|
175
|
-
}),
|
|
176
|
-
...countFinding({
|
|
177
|
-
ruleId: "budget.mcp",
|
|
178
|
-
count: facts.mcp.length,
|
|
179
|
-
limit: options.mcp,
|
|
180
|
-
countLabel: "mcp",
|
|
181
|
-
message: `MCP surface above sweet spot (${facts.mcp.length} > ${options.mcp})`,
|
|
182
|
-
reason:
|
|
183
|
-
"What actually degrades an agent is the number of tool definitions in context, not the number of servers: benchmarks show large models near-perfect at ~20 tools and failing outright past ~100, and a typical server carries tens of tools. Server count is the only proxy available without connecting to them, so treat this as a hint to go count the tools, not as a measurement.",
|
|
184
|
-
suggest:
|
|
185
|
-
"Count the tools your servers actually expose; disable unused servers in mcp.json or set thresholds.mcp in .agentscanrc.json",
|
|
186
|
-
}),
|
|
187
|
-
...packageManagerDrift(facts),
|
|
188
|
-
];
|
|
189
|
-
}
|