@chimix/agentscan 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sima Alexandru
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,475 @@
1
+ <h1 align="center">agentscan</h1>
2
+
3
+ <p align="center">
4
+ <em>Your agent config says the guard is on. The script is gone. Nothing told you.</em>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <img src="https://img.shields.io/badge/checks-23-111111?style=flat-square" alt="23 checks">
9
+ <img src="https://img.shields.io/badge/tests-183%20passing-111111?style=flat-square" alt="183 tests">
10
+ <img src="https://img.shields.io/badge/network-none-111111?style=flat-square" alt="No network">
11
+ <img src="https://img.shields.io/badge/writes-none-111111?style=flat-square" alt="Writes nothing">
12
+ <img src="https://img.shields.io/badge/runtime-bun-111111?style=flat-square" alt="Bun">
13
+ <img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT license">
14
+ </p>
15
+
16
+ <p align="center">
17
+ <strong>23 checks &middot; 3.4k lines &middot; 0 network calls &middot; every check sourced to a published spec line</strong><br>
18
+ <sub>Alpha. An earlier build reported 37 findings across 17 real projects of which <strong>25 were false</strong> — two checks had been written from what real projects looked like instead of from the spec. Both were deleted, and every check that survived is recorded in <a href="docs/spec/">docs/spec/</a> with the URL it came from and the date it was read. That story is the reason this tool exists in its current shape.</sub>
19
+ </p>
20
+
21
+ ---
22
+
23
+ Linters read the code your agent writes. This reads **the agent itself** — skills, `skills-lock.json`, hooks, MCP servers, agent definitions, policy files.
24
+
25
+ ## The failure it exists for
26
+
27
+ You registered a `PreToolUse` hook to stop destructive shell commands:
28
+
29
+ ```jsonc
30
+ // .claude/settings.json
31
+ { "hooks": { "PreToolUse": [{ "hooks": [
32
+ { "type": "command", "command": ".claude/hooks/guard-destructive-bash.js" }
33
+ ] }] } }
34
+ ```
35
+
36
+ Someone deleted the script six weeks ago.
37
+
38
+ ```console
39
+ $ ls .claude/hooks/guard-destructive-bash.js
40
+ ls: cannot access '.claude/hooks/guard-destructive-bash.js': No such file or directory
41
+ ```
42
+
43
+ Your agent starts normally. No warning, no error, no log line. The guard you
44
+ think you have has silently not existed for six weeks.
45
+
46
+ ```console
47
+ $ agentscan check
48
+
49
+ WARN hook:PreToolUse:.claude/hooks/guard-destructive-bash.js
50
+ rule:hook.missing-script
51
+ PreToolUse hook points at a script that does not exist
52
+ evidence: hook PreToolUse @ …/.claude/settings.json
53
+
54
+ Summary: 6 warn · 4 info hidden (--verbose) · score 40/100
55
+ ```
56
+
57
+ That is the whole category: **a config file asserting something that is not true
58
+ of the filesystem**, where nothing else in your stack will ever tell you.
59
+
60
+ ## How it works
61
+
62
+ No AI, no network, no heuristics. Read the config, read the disk, compare:
63
+
64
+ ```
65
+ 1. Discover .claude/ .agents/ .mcp.json AGENTS.md skills-lock.json
66
+ 2. Extract immutable facts — never re-read during checking
67
+ 3. Check 23 checks, each against one published spec line
68
+ 4. Report text · --json · --output prompt (handoff for a fixing agent)
69
+ ```
70
+
71
+ Same tree in, same findings out, every time. It never writes to the tree it
72
+ scans and never opens a socket.
73
+
74
+ ## What it will not do
75
+
76
+ It will not compare a skill's frontmatter `name` to its directory, and it will
77
+ not validate model ids or MCP tool names. All three need a hardcoded list of
78
+ valid values, with everything absent from the list reported as broken. That is
79
+ exactly how the 25 false findings happened: a hook-event list with 9 names when
80
+ the spec has 31, calling working hooks dead at severity `error`.
81
+
82
+ A check here has to point at a spec line. If it cannot, it does not ship.
83
+
84
+ ## Try it in 30 seconds
85
+
86
+ `agentscan` is **not on npm yet**, so `bunx agentscan` will 404. Clone and run it
87
+ against any project — it reads that project, writes nothing, and never leaves
88
+ your machine:
89
+
90
+ ```bash
91
+ git clone --depth=1 https://github.com/SimaAlexandru99/agentscan
92
+ cd agentscan && bun install
93
+
94
+ bun run src/cli.ts check ~/path/to/your-project
95
+ ```
96
+
97
+ Point it at a project that actually has agent config — a `.claude/` directory,
98
+ an `.mcp.json`, an `AGENTS.md`. On a project with none of those it will
99
+ correctly find nothing and say so.
100
+
101
+ ```bash
102
+ # what it could possibly report, before you run it
103
+ bun run src/cli.ts rules
104
+
105
+ # the interesting bits it hides by default
106
+ bun run src/cli.ts check ~/your-project --verbose
107
+
108
+ # why one specific finding fired
109
+ bun run src/cli.ts explain hook.missing-script:hook:PreToolUse:./x.js ~/your-project
110
+ ```
111
+
112
+ Requires **Bun** 1.1+. There is no Node build: the CLI uses `import.meta.dir`
113
+ and `Bun.which` directly. `check` never opens a socket and never writes to the
114
+ tree it scans.
115
+
116
+ ### In CI
117
+
118
+ The Action runs from its own checkout, so it works before the npm release:
119
+
120
+ ```yaml
121
+ - uses: SimaAlexandru99/agentscan@v0
122
+ with:
123
+ fail-on: error
124
+ ```
125
+
126
+ ### After the npm release
127
+
128
+ ```bash
129
+ bun add -d agentscan
130
+ bunx agentscan check
131
+ ```
132
+
133
+ ## Usage
134
+
135
+ ```bash
136
+ bunx agentscan check # text report (cwd)
137
+ bunx agentscan check ./my-app # explicit root
138
+ bunx agentscan check --json # machine-readable
139
+ bunx agentscan check --quiet # summary only
140
+ bunx agentscan check --verbose # include KEEP and info-severity findings
141
+ bunx agentscan check --fail-on warning
142
+ bunx agentscan check --global # also scan global skill dirs
143
+
144
+ bunx agentscan explain <findingId> # detail one finding
145
+ bunx agentscan rules # every check + rule id that can fire
146
+ bunx agentscan init # write .agentscanrc.json
147
+ ```
148
+
149
+ Flags for `check`:
150
+
151
+ | Flag | Meaning |
152
+ |------|---------|
153
+ | `--json` | JSON report (alias for `--output json`) |
154
+ | `--output <format>` | `human` (default) · `json` · `prompt` |
155
+ | `--copy` | Also copy the report to the system clipboard |
156
+ | `--quiet` | Summary line only |
157
+ | `--verbose` | Show KEEP + info-severity findings |
158
+ | `--fail-on <level>` | `never` (default) · `warning` · `error` |
159
+ | `--fail-under <0-100>` | Fail when the score drops below this floor |
160
+ | `--global` | Also scan `~/.claude/skills` and `~/.codex/skills` (see below) |
161
+ | `--config <path>` | Config file path |
162
+
163
+ **v1 does not write the tree** — no `apply`, no skill delete/install. Findings may *suggest* shell commands; you run them yourself.
164
+
165
+ ## Sample output
166
+
167
+ ```text
168
+ agentscan v0.1.0 — touchagency
169
+
170
+ Stack: 46 deps · 54 skills · 1 mcp · 2 agents · packageManager=bun
171
+
172
+ WARN hook:PreToolUse:.claude/hooks/guard-destructive-bash.js
173
+ rule:hook.missing-script
174
+ PreToolUse hook points at a script that does not exist: .claude/hooks/guard-destructive-bash.js
175
+ evidence: hook PreToolUse @ …/.claude/settings.json · script .claude/hooks/guard-destructive-bash.js
176
+
177
+ WARN hook:PreToolUse:.claude/hooks/protect-artists-json.js
178
+ rule:hook.missing-script
179
+ PreToolUse hook points at a script that does not exist: .claude/hooks/protect-artists-json.js
180
+ evidence: hook PreToolUse @ …/.claude/settings.json · script .claude/hooks/protect-artists-json.js
181
+
182
+ Summary: 6 warn · 4 info hidden (--verbose) · score 40/100
183
+ ```
184
+
185
+ The `Stack:` line is orientation only — which project, how big. The summary lists
186
+ only actions that actually occurred; a clean project prints `Summary: no findings`.
187
+
188
+ JSON shape (abridged):
189
+
190
+ ```json
191
+ {
192
+ "version": "0.1.0",
193
+ "root": "/path/to/project",
194
+ "factsSummary": {
195
+ "packageManager": "bun",
196
+ "depCount": 1,
197
+ "skillCount": 1,
198
+ "globalSkillCount": 0
199
+ },
200
+ "findings": [
201
+ {
202
+ "id": "hook.missing-script:hook:PreToolUse:.claude/hooks/protect-env.js",
203
+ "ruleId": "hook.missing-script",
204
+ "action": "warn",
205
+ "severity": "error",
206
+ "subject": "hook:PreToolUse:.claude/hooks/protect-env.js",
207
+ "message": "PreToolUse hook points at a script that does not exist: .claude/hooks/protect-env.js",
208
+ "reason": "The hook is registered but its script is missing, so it never runs.",
209
+ "evidence": [{ "kind": "script", "value": ".claude/hooks/protect-env.js" }],
210
+ "suggest": "Restore the script or remove the hook"
211
+ }
212
+ ]
213
+ }
214
+ ```
215
+
216
+ Same tree → same sorted findings, with stable unique `id`s.
217
+
218
+ ### `--global`
219
+
220
+ Adds `~/.claude/skills` and `~/.codex/skills` to the structural checks: a
221
+ `SKILL.md` that is malformed is malformed wherever it lives, and those findings
222
+ carry a `source: global` evidence entry so you can tell them apart.
223
+
224
+ Lockfile checks stay project-scoped — a project lockfile cannot pin a skill that
225
+ lives in your home directory, so reporting one as "not in the lockfile" could
226
+ only ever be wrong.
227
+
228
+ `includeGlobal: true` in `.agentscanrc.json` does the same thing without the
229
+ flag.
230
+
231
+ ## Score
232
+
233
+ ```
234
+ score = max(0, 100 - 10 × errors - 3 × warnings)
235
+ ```
236
+
237
+ That is the whole formula. Count two lines of the report and you can recompute
238
+ it. The comparable tools both score 0-100 and neither states its formula in
239
+ prose: shadscan's category weights are readable in its MIT source but not
240
+ documented, and react-doctor computes the score by POSTing your diagnostics to
241
+ `react.doctor/api/score`, so it cannot be recomputed offline at all. A number
242
+ whose derivation cannot be inspected is the false precision a score is supposed
243
+ to replace.
244
+
245
+ **Deduction, not coverage.** The obvious model — checks passed over checks run —
246
+ was measured against 17 real projects and rejected. Every one scored between
247
+ 97.7% and 100%, so it discriminated nothing, and it *inverted* severity: the
248
+ denominator grows with the size of your config while the defects do not, so a
249
+ project with 85 skills and one warning outscored a project with 10 skills and
250
+ the same one warning. Deduction has no denominator. Measured on the same 17
251
+ projects it spans 40 to 100, and the 85-skill project scores 97 while a
252
+ 54-skill one with six broken hooks scores 40.
253
+
254
+ **Info findings cost nothing.** They are budgets and hygiene notes, and they do
255
+ grow with project size — charging for them would reintroduce the inversion.
256
+
257
+ The score is a summary, not a verdict. `--fail-on error` is still the sharper
258
+ CI gate: it says which *kind* of problem fails the build, where `--fail-under`
259
+ only says how many points of unspecified trouble is too much.
260
+
261
+ ## Exit codes
262
+
263
+ | Code | When |
264
+ |------|------|
265
+ | `0` | OK, or findings below `--fail-on` threshold (default `never` always `0` for findings) |
266
+ | `1` | Findings at/above `--fail-on`, or score below `--fail-under` |
267
+ | `2` | Usage / config / load error |
268
+
269
+ ## CI
270
+
271
+ ```yaml
272
+ - uses: SimaAlexandru99/agentscan@v0
273
+ with:
274
+ fail-on: error # never | warning | error
275
+ output: human # human | json | prompt
276
+ ```
277
+
278
+ The action installs Bun, runs the scan, and exposes the report as the `report`
279
+ output so a later step can post it. Or run it directly:
280
+
281
+ ```yaml
282
+ - name: agentscan
283
+ run: bunx agentscan check --fail-on error
284
+ ```
285
+
286
+ Or with a local checkout of this repo:
287
+
288
+ ```bash
289
+ bun run src/cli.ts check --fail-on warning --json
290
+ ```
291
+
292
+ Default `failOn` is `never` so local runs stay non-blocking until you opt in.
293
+
294
+ ## Config
295
+
296
+ Optional `.agentscanrc.json` (create with `agentscan init`):
297
+
298
+ ```json
299
+ {
300
+ "skillPaths": [".agents/skills", ".claude/skills", "skills", ".cursor/skills"],
301
+ "mcpPaths": [".mcp.json", ".claude/mcp.json", "mcp.json"],
302
+ "policyFiles": ["AGENTS.md", "CLAUDE.md"],
303
+ "ignoreSkills": [],
304
+ "ignoreRules": [],
305
+ "ignoreFindings": [],
306
+ "failOn": "never",
307
+ "includeGlobal": false,
308
+ "requireLock": false,
309
+ "thresholds": {
310
+ "skillDescriptionBytes": 16000,
311
+ "skills": 30,
312
+ "mcp": 5,
313
+ "agentsMdLines": 150,
314
+ "claudeMdLines": 200,
315
+ "agents": 8
316
+ }
317
+ }
318
+ ```
319
+
320
+ Budget rules are **info** (hidden unless `--verbose`) so they do not flood default text or CI with `--fail-on warning`.
321
+
322
+ ### Handing findings to an agent
323
+
324
+ ```bash
325
+ agentscan check --output prompt
326
+ ```
327
+
328
+ A paste-ready markdown handoff: each item carries the message, why it matters,
329
+ the suggested fix, its evidence, and its finding id. Info-severity findings are
330
+ left out — budgets and hygiene notes are for a maintainer to weigh, not work
331
+ items for an executor.
332
+
333
+ ### Getting the report out of the terminal
334
+
335
+ Saving and piping are the shell's job, and it already does them:
336
+
337
+ ```bash
338
+ agentscan check --output prompt > handoff.md # save
339
+ agentscan check --output prompt | claude # send to another tool
340
+ agentscan check --json | jq '.findings[]' # filter
341
+ ```
342
+
343
+ The clipboard is the one thing no shell does portably, so that one is a flag:
344
+
345
+ ```bash
346
+ agentscan check --output prompt --copy
347
+ ```
348
+
349
+ `--copy` tries `wl-copy`, `xclip`, `xsel`, `pbcopy`, `clip.exe` in turn and uses
350
+ the first that succeeds — being installed is not the same as working, and on a
351
+ headless WSL shell the first two are present and both fail. The report still
352
+ goes to stdout; the "copied" note goes to stderr, so `--copy` composes with
353
+ redirection. A clipboard that cannot be reached prints why and does not change
354
+ the exit code.
355
+
356
+ This is the only place agentscan starts a subprocess, and it runs only under
357
+ `--copy`. It writes to the clipboard and nowhere else.
358
+
359
+ ### Suppressing one finding
360
+
361
+ `ignoreRules` disables a check project-wide, which is too blunt when a single
362
+ hook uses an exotic launcher. `ignoreFindings` takes exact finding ids — the
363
+ same strings `agentscan explain` accepts, so the value you already have from
364
+ reading the report is the value you paste:
365
+
366
+ ```json
367
+ { "ignoreFindings": ["hook.missing-script:hook:PreToolUse:./bin/wrapper"] }
368
+ ```
369
+
370
+ ## What it checks
371
+
372
+ Every check lives in `src/checks/` and runs on every `check`. `agentscan rules`
373
+ lists all of them with their ids; `agentscan explain <id>` details any finding.
374
+
375
+ Most validate one discovered item against its own file on disk. The five
376
+ `budget.*` / `policy.*` entries at the bottom judge aggregate size instead —
377
+ they are all **info**, they are sourced to `docs/spec/thresholds.md`, and they
378
+ are retuned through `thresholds` in `.agentscanrc.json` rather than switched
379
+ off.
380
+
381
+ | id | Severity | Catches |
382
+ |----|----------|---------|
383
+ | `config.unreadable` | error | A config file that is not valid JSON, so whatever it declares is silently not in effect |
384
+ | `hook.missing-script` | error | A registered hook whose script does not exist — it never runs |
385
+ | `hook.unknown-event` | error | A hook registered under an event name that is never dispatched |
386
+ | `agent.missing-frontmatter` | warning | An agent definition with no `---` block |
387
+ | `agent.missing-description` | info | Agent frontmatter has no `description`, so it will not be dispatched on its own |
388
+ | `mcp.no-launch` | error | An MCP server with neither `command` nor `url`; its tools are never available |
389
+ | `mcp.url-without-type` | error | A remote MCP server with a `url` but no `type` — read as stdio, fails, and is skipped |
390
+ | `mcp.hardcoded-secret` | error | A token-shaped literal in MCP config (the value is never echoed back) |
391
+ | `mcp.literal-env` | warning | Long literal `env` values that should be `${VAR}` |
392
+ | `skill.missing-skill-md` | warning | A directory under a skill path with no `SKILL.md` |
393
+ | `skill.missing-frontmatter` | warning | `SKILL.md` with no `---` block |
394
+ | `skill.broken-reference` | warning | The body points at a bundled file that does not exist |
395
+ | `skill.duplicate-description` | warning | Two or more skills carry an identical description, so routing between them is arbitrary |
396
+ | `skill.missing-description` | info | Frontmatter has no `description`, so Claude will not load the skill on its own |
397
+ | `skill.locked-not-installed` | warning | `skills-lock.json` pins a skill that is not on disk |
398
+ | `skill.not-in-lock` | info | A skill on disk that the lockfile does not track — local and unpinned |
399
+ | `skill.description-budget` | info | Skill names + descriptions exceed the startup character budget |
400
+ | `skill.no-lockfile` | info | Skills present with no lockfile at all (only with `requireLock`) |
401
+
402
+ Agent definitions are checked for structure only. An agent's frontmatter `name`
403
+ is a display name — 16 of 34 real files declare one that differs from the
404
+ filename, and nothing keys on the filename — so it is deliberately not compared,
405
+ and a test guards against re-adding that check. Model identifiers and tool names
406
+ are likewise not validated: both would need a hardcoded list, which is the shape
407
+ that already shipped a false error. See [plans/003](plans/003-validate-agent-definitions.md).
408
+
409
+ `skill.broken-reference` reads the body, not just the frontmatter. It looks for
410
+ paths under the conventional bundled directories (`scripts/`, `references/`,
411
+ `assets/`, `templates/`, `examples/`) and resolves each against the skill's own
412
+ directory first, then the repo root — both bases are needed, because of 1674
413
+ references measured across 17 projects, 1645 resolved skill-relative and 12 only
414
+ at the root. Fenced code blocks are stripped first: a path in an example is
415
+ illustration, not a pointer.
416
+
417
+ `hook.missing-script` is deliberately conservative. A command is only resolved
418
+ when it is a single invocation with a path-like argument; shell programs
419
+ (`a && b`, `$(...)`, pipes) are skipped, because a hook written as
420
+ `[ ! -f x ] || node x` already handles the missing file and flagging it would be
421
+ a false positive. `node -e "<code>"` is never treated as a path. Only
422
+ `$CLAUDE_PROJECT_DIR` is expanded — other variables are left alone rather than
423
+ guessed at.
424
+
425
+ Checks are written against the published specs, not against what happens to
426
+ appear in real projects. Two that were written the other way round shipped as
427
+ false positives — a nine-name hook-event list where the spec has 31, and a
428
+ `name` must equal the directory rule that the spec explicitly contradicts
429
+ (`name` is optional and defaults to the directory). Both are gone.
430
+
431
+ Every assumption a check encodes is recorded in **[docs/spec/](docs/spec/)**
432
+ with its source URL, the date it was read, and an honest confidence rating.
433
+ When adding a check, add its spec line there first.
434
+
435
+ ## Known limits
436
+
437
+ - **No dependency→skill knowledge.** Nothing here says "you have `next`, you
438
+ should add a `next-*` skill". That needed a hand-maintained registry, and the
439
+ version that existed fired on none of the 17 projects measured, so it was
440
+ removed. Skill provenance comes from `skills-lock.json` instead.
441
+ - **No rule produces `refresh` or `keep`.** Comparing a skill's content against
442
+ the `computedHash` in `skills-lock.json` is the obvious `refresh` source; the
443
+ hash algorithm used by the installing tool is not documented here, and it is
444
+ not reproducible from `SKILL.md` bytes alone, so it is not implemented.
445
+ - **Bun only.** `import.meta.dir` / `import.meta.main`; it will not run on Node.
446
+ - Policy files are read up to 100 KB, so `policyLines` undercounts past that.
447
+
448
+ ## Development
449
+
450
+ ```bash
451
+ bun install
452
+ bun run typecheck
453
+ bun test
454
+ bun run src/cli.ts check tests/fixtures/lock-drift
455
+ ```
456
+
457
+ | Script | Command |
458
+ |--------|---------|
459
+ | `agentscan` | `bun run src/cli.ts` |
460
+ | `typecheck` | `tsc --noEmit` |
461
+ | `test` | `bun test` |
462
+ | `check` | `bun run src/cli.ts check` |
463
+
464
+ ## Design docs
465
+
466
+ **This README is the source of truth for current behavior.** The documents under
467
+ `docs/superpowers/specs/` are the original design and are **superseded** —
468
+ several decisions changed while building: Bun-only runtime, the dep-to-skill map
469
+ and its "orphan" heuristic dropped in favour of `skills-lock.json`, budget
470
+ checks added, structural checks added, and the YAML rule engine deleted
471
+ (`plans/010`). Read them as history, not as behavior.
472
+
473
+ ## License
474
+
475
+ MIT
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@chimix/agentscan",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "bin": {
7
+ "agentscan": "./src/cli.ts"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "keywords": [
18
+ "agent-skills",
19
+ "claude-code",
20
+ "mcp",
21
+ "hooks",
22
+ "lint",
23
+ "config",
24
+ "cli",
25
+ "bun"
26
+ ],
27
+ "homepage": "https://github.com/SimaAlexandru99/agentscan#readme",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/SimaAlexandru99/agentscan.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/SimaAlexandru99/agentscan/issues"
34
+ },
35
+ "author": "Sima Alexandru <sima.alex33@gmail.com>",
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
+ "agentscan": "bun run src/cli.ts",
39
+ "typecheck": "tsc --noEmit",
40
+ "test": "bun test",
41
+ "check": "bun run src/cli.ts check",
42
+ "spec:check": "bun run scripts/spec-drift.ts"
43
+ },
44
+ "engines": {
45
+ "bun": ">=1.1.0"
46
+ },
47
+ "license": "MIT",
48
+ "dependencies": {
49
+ "yaml": "^2.7.0",
50
+ "zod": "^3.24.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/bun": "latest",
54
+ "typescript": "^5.8.0"
55
+ }
56
+ }
package/src/analyze.ts ADDED
@@ -0,0 +1,82 @@
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
+ }