@praneeth_54/agentdoctor 0.3.0-beta → 1.0.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 (65) hide show
  1. package/CHANGELOG.md +76 -1
  2. package/README.md +60 -30
  3. package/dist/agents/inspect.js +58 -51
  4. package/dist/cli/commands/explain.js +1 -1
  5. package/dist/cli/commands/fix.js +22 -6
  6. package/dist/cli/commands/scan.d.ts +5 -0
  7. package/dist/cli/commands/scan.js +25 -6
  8. package/dist/cli/commands/verify.d.ts +6 -0
  9. package/dist/cli/commands/verify.js +25 -11
  10. package/dist/cli/program.js +48 -7
  11. package/dist/constants.d.ts +1 -1
  12. package/dist/constants.js +1 -1
  13. package/dist/core/fix/apply.d.ts +5 -1
  14. package/dist/core/fix/apply.js +34 -14
  15. package/dist/core/fix/plan.d.ts +1 -1
  16. package/dist/core/fix/plan.js +110 -3
  17. package/dist/core/fix/render.d.ts +2 -0
  18. package/dist/core/fix/render.js +57 -9
  19. package/dist/core/fix/run.js +5 -1
  20. package/dist/core/fix/types.d.ts +1 -1
  21. package/dist/core/fix/types.js +5 -1
  22. package/dist/core/fix/writers/claude-settings.d.ts +16 -0
  23. package/dist/core/fix/writers/claude-settings.js +100 -0
  24. package/dist/core/fix/writers/codex-config.d.ts +22 -0
  25. package/dist/core/fix/writers/codex-config.js +183 -0
  26. package/dist/core/fix/writers/cursorignore.js +2 -8
  27. package/dist/core/path-resolution/index.d.ts +5 -0
  28. package/dist/core/path-resolution/index.js +5 -0
  29. package/dist/core/path-resolution/prepare.d.ts +33 -0
  30. package/dist/core/path-resolution/prepare.js +98 -0
  31. package/dist/core/policy/evaluate.d.ts +44 -0
  32. package/dist/core/policy/evaluate.js +120 -0
  33. package/dist/core/rules/claude-deny.d.ts +8 -0
  34. package/dist/core/rules/claude-deny.js +39 -0
  35. package/dist/core/rules/codex-deny.d.ts +5 -0
  36. package/dist/core/rules/codex-deny.js +42 -0
  37. package/dist/core/rules/context/generated-directory.js +43 -9
  38. package/dist/core/rules/context/large-log-file.js +27 -10
  39. package/dist/core/rules/ignore.js +2 -5
  40. package/dist/core/rules/instructions/missing-path-reference.d.ts +5 -1
  41. package/dist/core/rules/instructions/missing-path-reference.js +24 -6
  42. package/dist/core/rules/path-kind.d.ts +15 -2
  43. package/dist/core/rules/path-kind.js +113 -10
  44. package/dist/core/rules/security/env-file-exposure.js +9 -24
  45. package/dist/core/rules/text-cache.js +46 -40
  46. package/dist/discovery/files.js +7 -2
  47. package/dist/discovery/log-like.d.ts +2 -0
  48. package/dist/discovery/log-like.js +7 -0
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.js +1 -0
  51. package/dist/reporters/github/annotations.d.ts +8 -0
  52. package/dist/reporters/github/annotations.js +42 -0
  53. package/dist/reporters/github/emit.d.ts +23 -0
  54. package/dist/reporters/github/emit.js +31 -0
  55. package/dist/reporters/github/summary.d.ts +20 -0
  56. package/dist/reporters/github/summary.js +111 -0
  57. package/dist/reporters/terminal/report.d.ts +5 -0
  58. package/dist/reporters/terminal/report.js +68 -5
  59. package/dist/reporters/verify/terminal.d.ts +4 -0
  60. package/dist/reporters/verify/terminal.js +26 -0
  61. package/dist/utils/fs.d.ts +5 -0
  62. package/dist/utils/fs.js +34 -0
  63. package/dist/utils/path.d.ts +5 -0
  64. package/dist/utils/path.js +16 -0
  65. package/package.json +11 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Notes
11
+
12
+ - Owner release sequence for `1.0.0`: `npm publish` → `git tag v1.0.0` → GitHub Release → then bump Action `version` default `0.3.0-beta` → `1.0.0` (only after npm serves `1.0.0`).
13
+ - Project Brain V1 remains a parallel laboratory under `src/core/understanding/brain` and is excluded from the published npm pack. Docs: [docs/project-brain.md](docs/project-brain.md).
14
+
15
+ ## [1.0.0] — 2026-08-12
16
+
17
+ First production release: Scan → Fix → Verify → CI contract frozen for v1.
18
+
19
+ ### Added
20
+
21
+ - Claude Code safe-context Fix writer: `agentdoctor fix` appends allowlisted
22
+ `permissions.deny` Read rules to `.claude/settings.json` for
23
+ `context/generated-directory` and `context/large-log-file` when Claude Code is
24
+ configured (alongside existing Cursor `.cursorignore` fixes).
25
+ - Codex safe-context Fix writer: `agentdoctor fix` merges allowlisted filesystem
26
+ `deny` keys into `.codex/config.toml` permission profiles for the same safe
27
+ context findings when Codex is detected. Skips when `sandbox_mode` is set or
28
+ `default_permissions` selects a built-in `:…` profile.
29
+ - GitHub Action / CLI CI policy enforcement: `minimum-score` / `--min-score`,
30
+ `fail-on-severity` / `--fail-on-severity`, `fail-on-rule` / `--fail-on-rule`,
31
+ `fail-on-new` / `--fail-on-new`, `verify-baseline`, `json-output`, `summary` /
32
+ `--summary`, and `annotations` / `--annotations`. Action `version: workspace`
33
+ runs the checked-out `dist/cli` for local CI.
34
+ - Guided Next steps on failed `scan` / `fix` / `verify` terminal output, and on
35
+ GitHub Step Summary when a policy gate fails — shortest path back to green
36
+ (reproduce → fix or explain → verify).
37
+ - Windows CI quality job (Node 20) and Windows-safe Fix/Action overwrite writes.
38
+ - Action smoke coverage for `fail-on-rule`, `verify-baseline`, and baseline
39
+ symlink escape rejection.
40
+
41
+ ### Fixed
42
+
43
+ - `instructions/missing-path-reference` also resolves non-`./` paths relative to the
44
+ instruction file directory (monorepo package docs), while root-level instruction
45
+ files still require repository-root paths. Corpus-100: 121 → 88 findings for this
46
+ rule (−33); other rules unchanged.
47
+ - `context/generated-directory` and `context/large-log-file` honor Claude Code Read
48
+ deny exclusions when computing `affectedAgents`, so Fix → Verify clears Claude
49
+ context findings after a deny rule is applied.
50
+ - The same rules honor Codex filesystem deny keys in `.codex/config.toml` when
51
+ computing `affectedAgents`.
52
+ - Action writes the JSON report even when a policy gate fails (exit `1`), so
53
+ artifacts remain available for triage.
54
+ - `--min-score` / Action `minimum-score` fail when no supported agents are
55
+ configured (`agentSecurityAnalysis: limited`) instead of passing on a vacuous 100.
56
+ - Terminal readiness prints `n/a` when analysis is limited (no agents).
57
+ - Agentless first scan no longer shows a green “No agent-configuration findings”
58
+ success line; it tells the user to add Cursor / Claude Code / Codex config and
59
+ re-run.
60
+ - Invalid `--min-score` values exit `2` (usage) instead of `3` (internal).
61
+ - Codex Fix refuses invalid `.codex/config.toml` during planning (same as Claude
62
+ invalid JSON) instead of silently skipping Codex and writing Cursor-only fixes.
63
+ - Codex Fix refuses unrecognizable / invalid `.codex/config.toml` content instead of
64
+ appending permission profiles into garbage TOML.
65
+ - `agentdoctor fix` exits `2` when confirmation is cancelled (non-TTY without `--yes`)
66
+ or when Fix refuses invalid settings / cannot write due to permissions.
67
+ - `scan --ci` now fails (exit `1`) when any **critical** finding exists. Omit `--ci` for
68
+ report-only scans. The GitHub Action stays report-only unless policy inputs are set
69
+ (it no longer passes a bare `--ci`).
70
+ - Discovery keeps oversized log/dump-like paths as size metadata so
71
+ `context/large-log-file` flags files above the content-read limit (previously silent
72
+ false negatives for the largest logs).
73
+ - Action `verify-baseline` re-checks workspace containment after `realpath` so a
74
+ workspace-relative symlink cannot escape to an outside file.
75
+ - Fix writers and Action report overwrite use Windows-safe replace (rename cannot
76
+ overwrite an existing destination on Windows).
77
+
78
+ ### Compatibility
79
+
80
+ - CLI + JSON + rule ID contracts frozen for v1 (see [docs/compatibility.md](docs/compatibility.md))
81
+ - Action `version` input default remains `0.3.0-beta` until `1.0.0` is published to npm;
82
+ pin `1.0.0` or use `version: workspace` after the release is cut
83
+
10
84
  ## [0.3.0-beta] — 2026-08-07
11
85
 
12
86
  Minor beta: completes the Scan → Fix → Verify CLI loop and corrects release-facing honesty.
@@ -171,7 +245,8 @@ First public beta.
171
245
  - Not a complete secret scanner
172
246
  - Git “tracked secret” detection deferred
173
247
 
174
- [Unreleased]: https://github.com/pranee54/AgentDoctor/compare/v0.3.0-beta...HEAD
248
+ [Unreleased]: https://github.com/pranee54/AgentDoctor/compare/v1.0.0...HEAD
249
+ [1.0.0]: https://github.com/pranee54/AgentDoctor/releases/tag/v1.0.0
175
250
  [0.3.0-beta]: https://github.com/pranee54/AgentDoctor/releases/tag/v0.3.0-beta
176
251
  [0.2.0-beta]: https://github.com/pranee54/AgentDoctor/releases/tag/v0.2.0-beta
177
252
  [0.1.4-beta]: https://github.com/pranee54/AgentDoctor/releases/tag/v0.1.4-beta
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AgentDoctor
2
2
 
3
- [![npm](https://img.shields.io/npm/v/@praneeth_54/agentdoctor)](https://www.npmjs.com/package/@praneeth_54/agentdoctor)
3
+ [![npm](https://img.shields.io/npm/v/@praneeth_54/agentdoctor?label=npm)](https://www.npmjs.com/package/@praneeth_54/agentdoctor)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@praneeth_54/agentdoctor)](https://www.npmjs.com/package/@praneeth_54/agentdoctor)
5
5
  [![CI](https://img.shields.io/github/actions/workflow/status/pranee54/AgentDoctor/ci.yml?branch=main&label=CI)](https://github.com/pranee54/AgentDoctor/actions)
6
6
  [![Node](https://img.shields.io/node/v/@praneeth_54/agentdoctor)](https://nodejs.org)
@@ -16,7 +16,7 @@ AgentDoctor is a local CLI that inspects project-level AI coding agent setup —
16
16
  npx @praneeth_54/agentdoctor
17
17
  ```
18
18
 
19
- Public beta (`0.3.0-beta`). Scan → Fix → Verify. Deterministic scores in the terminal and JSON.
19
+ Public release (`1.0.0`). Scan → Fix → Verify → CI. Deterministic scores in the terminal and JSON.
20
20
 
21
21
  ---
22
22
 
@@ -24,12 +24,12 @@ Public beta (`0.3.0-beta`). Scan → Fix → Verify. Deterministic scores in the
24
24
 
25
25
  ![AgentDoctor scanning a repository and reporting coding-agent security findings](docs/images/cli-scan.png)
26
26
 
27
- _Real scan of the included `insecure-agent-project` fixture using AgentDoctor v0.3.0-beta._
27
+ _Real scan of the included `insecure-agent-project` fixture using AgentDoctor v1.0.0._
28
28
 
29
29
  ```text
30
30
  $ npx @praneeth_54/agentdoctor
31
31
 
32
- 🩺 AgentDoctor v0.3.0-beta
32
+ 🩺 AgentDoctor v1.0.0
33
33
 
34
34
  Scanning repository...
35
35
 
@@ -100,7 +100,7 @@ Manually reviewing all of that across Cursor, Claude Code, and Codex is slow and
100
100
  | ESLint | Source code |
101
101
  | **AgentDoctor** | **AI coding agent environments** |
102
102
 
103
- AgentDoctor analyzes configuration. It does not run agents or call an LLM. `agentdoctor fix` may append safe Cursor ignore patterns; it does not rewrite security settings or credentials.
103
+ AgentDoctor analyzes configuration. It does not run agents or call an LLM. `agentdoctor fix` may append safe context exclusions (Cursor `.cursorignore`, Claude Code Read deny rules, and Codex filesystem deny keys); it does not rewrite secrets, credentials, or security modes such as `bypassPermissions`.
104
104
 
105
105
  ---
106
106
 
@@ -142,7 +142,7 @@ You get deterministic findings with:
142
142
  - conservative recommendations
143
143
  - readiness score (`scores.overall` in JSON; overall line in the terminal)
144
144
 
145
- Safe Cursor context exclusions can be applied with `agentdoctor fix`. Security and review findings stay manual — Fix explains why and does not invent unsafe edits.
145
+ Safe context exclusions (Cursor / Claude Code / Codex) can be applied with `agentdoctor fix`. Security and review findings stay manual — Fix explains why and does not invent unsafe edits.
146
146
 
147
147
  ---
148
148
 
@@ -154,10 +154,10 @@ Safe Cursor context exclusions can be applied with `agentdoctor fix`. Security a
154
154
  npx @praneeth_54/agentdoctor
155
155
  ```
156
156
 
157
- Pin a beta version when you need a fixed install:
157
+ Pin a version when you need a fixed install:
158
158
 
159
159
  ```bash
160
- npx @praneeth_54/agentdoctor@0.3.0-beta
160
+ npx @praneeth_54/agentdoctor@1.0.0
161
161
  ```
162
162
 
163
163
  ### Global (optional)
@@ -173,7 +173,7 @@ agentdoctor
173
173
  # 1. Scan (save a baseline for Verify)
174
174
  npx @praneeth_54/agentdoctor scan . --json > agentdoctor-report.json
175
175
 
176
- # 2. Fix safe Cursor context exclusions (preview first with --dry-run)
176
+ # 2. Fix safe context exclusions (preview first with --dry-run)
177
177
  npx @praneeth_54/agentdoctor fix . --dry-run
178
178
  npx @praneeth_54/agentdoctor fix . -y
179
179
 
@@ -181,7 +181,11 @@ npx @praneeth_54/agentdoctor fix . -y
181
181
  npx @praneeth_54/agentdoctor verify . --baseline agentdoctor-report.json
182
182
  ```
183
183
 
184
- `fix` currently writes `.cursorignore` patterns for safe context findings (for example unignored `build/` or large logs). Review/manual security findings are listed as skipped — address those yourself, then re-run `verify`.
184
+ `fix` writes safe context exclusions for Cursor (`.cursorignore`), Claude Code
185
+ (`permissions.deny` Read rules in `.claude/settings.json`), and Codex (filesystem `deny`
186
+ keys under a permissions profile in `.codex/config.toml`) for findings such as unignored
187
+ `build/` or large logs. Review/manual security findings are listed as skipped — address those
188
+ yourself, then re-run `verify`.
185
189
 
186
190
  ### Common commands
187
191
 
@@ -279,10 +283,14 @@ steps:
279
283
 
280
284
  - name: Audit coding-agent configuration
281
285
  id: agentdoctor
282
- uses: pranee54/AgentDoctor@v0.3.0-beta
286
+ uses: pranee54/AgentDoctor@v1.0.0
283
287
  with:
284
288
  path: .
289
+ version: "1.0.0"
285
290
  output-file: agentdoctor-report.json
291
+ minimum-score: "70"
292
+ fail-on-severity: critical
293
+ summary: "true"
286
294
 
287
295
  - name: Upload AgentDoctor report
288
296
  uses: actions/upload-artifact@v4
@@ -291,10 +299,13 @@ steps:
291
299
  path: ${{ steps.agentdoctor.outputs.report-path }}
292
300
  ```
293
301
 
294
- The action installs the published `@praneeth_54/agentdoctor@0.3.0-beta` package, runs it with
295
- `--ci --json`, and writes the report inside the checked-out workspace. It sets up Node.js 20
296
- for the CLI. The optional `version` input accepts an exact npm version or the `latest` / `beta`
297
- dist-tag.
302
+ Policy inputs: `minimum-score`, `fail-on-severity`, `fail-on-rule`, `fail-on-new`,
303
+ `verify-baseline`, `summary`, `annotations`. The Action stays report-only until you set a
304
+ policy input. Explicitly set `version: "1.0.0"` after npm publish (the Action default remains
305
+ `0.3.0-beta` until that post-publish bump). For local CI against this repo, use
306
+ `version: workspace` after `npm run build`. The action installs `@praneeth_54/agentdoctor`,
307
+ runs scan (or `verify` when `verify-baseline` is set) with `--json`, and writes the report
308
+ inside the workspace.
298
309
 
299
310
  ### CLI
300
311
 
@@ -302,15 +313,21 @@ Use JSON directly in other CI systems:
302
313
 
303
314
  ```bash
304
315
  # Report-only (exit 0 even when findings exist; scores still in JSON)
316
+ npx @praneeth_54/agentdoctor --json
317
+
318
+ # Fail when any critical finding exists
305
319
  npx @praneeth_54/agentdoctor --ci --json
306
320
 
307
- # Fail CI when overall readiness is below 70
321
+ # Fail when overall readiness is below 70 (with --ci also fails on criticals)
308
322
  npx @praneeth_54/agentdoctor --ci --json --min-score 70
323
+
324
+ # Fail on warning-or-higher (overrides the default critical gate from --ci)
325
+ npx @praneeth_54/agentdoctor --ci --json --fail-on-severity warning
309
326
  ```
310
327
 
311
- `--ci` runs non-interactively and does **not** apply an implicit score threshold.
312
- Use `--min-score N` (with or without `--ci`) to fail with exit code `1` when
313
- `scores.overall < N`.
328
+ `--ci` fails when any **critical** finding exists. Override the severity floor with
329
+ `--fail-on-severity`, and use `--min-score` / `--fail-on-rule` for additional gates.
330
+ Omit `--ci` for report-only JSON (exit `0` even when findings exist).
314
331
 
315
332
  Exit codes: [docs/exit-codes.md](docs/exit-codes.md). Compatibility promises: [docs/compatibility.md](docs/compatibility.md).
316
333
 
@@ -325,19 +342,18 @@ and deferred v2 items): [docs/scoring.md](docs/scoring.md).
325
342
 
326
343
  ---
327
344
 
328
- ## Beta limitations
345
+ ## Known limitations
329
346
 
330
- Honest limits of the current public beta:
347
+ Honest limits of v1:
331
348
 
332
- | Limitation | Status |
333
- | --------------------------- | ------------------------------------------------------------------- |
334
- | Automatic fixes | Safe Cursor `.cursorignore` context exclusions only |
335
- | Claude Code / Codex writers | Not implemented — Fix skips with an explicit reason |
336
- | Security findings | Review/manual — Fix does not rewrite secrets or permission settings |
337
- | GitHub Action score gates | Action remains `--ci --json` report-only (no `min-score` input) |
338
- | Secret-content scanning | Filename / config heuristics only |
339
- | Detection style | Intentionally conservative; false security findings are avoided |
340
- | Agent coverage | Cursor, Claude Code, Codex project configs |
349
+ | Limitation | Status |
350
+ | --------------------------- | ------------------------------------------------------------------------------- |
351
+ | Automatic fixes | Safe Cursor / Claude Code / Codex context exclusions only |
352
+ | Security findings | Review/manual — Fix does not rewrite secrets or security modes |
353
+ | Secret-content scanning | Filename / config heuristics only |
354
+ | Detection style | Intentionally conservative; false security findings are avoided |
355
+ | Agent coverage | Cursor, Claude Code, Codex project configs |
356
+ | Missing-path residual noise | Broad path-lattice expansion deferred; instruction-directory resolution shipped |
341
357
 
342
358
  See [CHANGELOG.md](CHANGELOG.md) and [docs/compatibility.md](docs/compatibility.md).
343
359
 
@@ -388,6 +404,20 @@ node dist/cli/index.js ./fixtures/clean-configured-project
388
404
 
389
405
  ---
390
406
 
407
+ ## Project Brain (separate laboratory capability)
408
+
409
+ AgentDoctor V1 ships the **safety** product: Scan → Fix → Verify → Policy → CI.
410
+
411
+ A parallel **Project Brain** engineering layer lives under `src/core/understanding/` (durable local claims, evidence, snapshots, query/trace/delta). It is:
412
+
413
+ - **not** part of the published npm package (`tsconfig.build` excludes it)
414
+ - **not** wired into the public CLI
415
+ - documented in [docs/project-brain.md](docs/project-brain.md)
416
+
417
+ See [PROJECT_AUDIT.txt](PROJECT_AUDIT.txt) and [RELEASE_CHECKLIST.txt](RELEASE_CHECKLIST.txt).
418
+
419
+ ---
420
+
391
421
  ## Next steps
392
422
 
393
423
  - **Try it:** `npx @praneeth_54/agentdoctor`
@@ -23,11 +23,29 @@ export async function inspectRepoFile(root, relativePath, maxFileSizeBytes = DEF
23
23
  };
24
24
  }
25
25
  try {
26
- const lstat = await fs.lstat(absolutePath);
27
- const isSymlink = lstat.isSymbolicLink();
28
- if (isSymlink) {
29
- const real = await fs.realpath(absolutePath);
30
- if (!isPathInsideRoot(root, real)) {
26
+ // Open first, then inspect via the same handle (avoids TOCTOU with prior lstat/stat).
27
+ const handle = await fs.open(absolutePath, "r");
28
+ try {
29
+ const lstat = await fs.lstat(absolutePath);
30
+ const isSymlink = lstat.isSymbolicLink();
31
+ if (isSymlink) {
32
+ const real = await fs.realpath(absolutePath);
33
+ if (!isPathInsideRoot(root, real)) {
34
+ return {
35
+ relativePath: normalizedRelative,
36
+ absolutePath,
37
+ exists: true,
38
+ readable: false,
39
+ sizeBytes: 0,
40
+ empty: true,
41
+ isSymlink: true,
42
+ text: null,
43
+ error: "Symlink target is outside repository root",
44
+ };
45
+ }
46
+ }
47
+ const stat = await handle.stat();
48
+ if (!stat.isFile()) {
31
49
  return {
32
50
  relativePath: normalizedRelative,
33
51
  absolutePath,
@@ -35,64 +53,53 @@ export async function inspectRepoFile(root, relativePath, maxFileSizeBytes = DEF
35
53
  readable: false,
36
54
  sizeBytes: 0,
37
55
  empty: true,
38
- isSymlink: true,
56
+ isSymlink,
39
57
  text: null,
40
- error: "Symlink target is outside repository root",
58
+ error: "Not a regular file",
41
59
  };
42
60
  }
43
- }
44
- const stat = await fs.stat(absolutePath);
45
- if (!stat.isFile()) {
46
- return {
47
- relativePath: normalizedRelative,
48
- absolutePath,
49
- exists: true,
50
- readable: false,
51
- sizeBytes: 0,
52
- empty: true,
53
- isSymlink,
54
- text: null,
55
- error: "Not a regular file",
56
- };
57
- }
58
- const sizeBytes = Number(stat.size);
59
- if (sizeBytes === 0) {
61
+ const sizeBytes = Number(stat.size);
62
+ if (sizeBytes === 0) {
63
+ return {
64
+ relativePath: normalizedRelative,
65
+ absolutePath,
66
+ exists: true,
67
+ readable: true,
68
+ sizeBytes: 0,
69
+ empty: true,
70
+ isSymlink,
71
+ text: "",
72
+ };
73
+ }
74
+ if (sizeBytes > maxFileSizeBytes) {
75
+ return {
76
+ relativePath: normalizedRelative,
77
+ absolutePath,
78
+ exists: true,
79
+ readable: false,
80
+ sizeBytes,
81
+ empty: false,
82
+ isSymlink,
83
+ text: null,
84
+ error: "File exceeds max size limit",
85
+ };
86
+ }
87
+ const text = await handle.readFile("utf8");
88
+ const trimmedEmpty = text.trim().length === 0;
60
89
  return {
61
90
  relativePath: normalizedRelative,
62
91
  absolutePath,
63
92
  exists: true,
64
93
  readable: true,
65
- sizeBytes: 0,
66
- empty: true,
67
- isSymlink,
68
- text: "",
69
- };
70
- }
71
- if (sizeBytes > maxFileSizeBytes) {
72
- return {
73
- relativePath: normalizedRelative,
74
- absolutePath,
75
- exists: true,
76
- readable: false,
77
94
  sizeBytes,
78
- empty: false,
95
+ empty: trimmedEmpty,
79
96
  isSymlink,
80
- text: null,
81
- error: "File exceeds max size limit",
97
+ text,
82
98
  };
83
99
  }
84
- const text = await fs.readFile(absolutePath, "utf8");
85
- const trimmedEmpty = text.trim().length === 0;
86
- return {
87
- relativePath: normalizedRelative,
88
- absolutePath,
89
- exists: true,
90
- readable: true,
91
- sizeBytes,
92
- empty: trimmedEmpty,
93
- isSymlink,
94
- text,
95
- };
100
+ finally {
101
+ await handle.close();
102
+ }
96
103
  }
97
104
  catch (error) {
98
105
  const message = error instanceof Error ? error.message : "Unable to read file";
@@ -39,7 +39,7 @@ export async function runExplainCommand(ruleId) {
39
39
  lines.push("");
40
40
  lines.push(colors.bold("Can AgentDoctor safely fix it?"));
41
41
  lines.push(rule.fixability === "safe"
42
- ? " Yes for Cursor context exclusions (`agentdoctor fix`). Other agents may still need a manual step."
42
+ ? " Yes for Cursor (`.cursorignore`), Claude Code (Read deny in `.claude/settings.json`), and Codex (filesystem deny in `.codex/config.toml`)."
43
43
  : rule.fixability === "review"
44
44
  ? " No — requires human review. Fix reports why and leaves the file unchanged."
45
45
  : rule.fixability === "manual"
@@ -1,7 +1,7 @@
1
1
  import { EXIT_CODES } from "../../types/index.js";
2
2
  import { isDirectory } from "../../utils/fs.js";
3
3
  import { resolveRepoRoot } from "../../utils/path.js";
4
- import { applyFixPlan, readCursorignore } from "../../core/fix/apply.js";
4
+ import { applyFixPlan, readClaudeSettings, readCodexConfig, readCursorignore, } from "../../core/fix/apply.js";
5
5
  import { buildFixPlan } from "../../core/fix/plan.js";
6
6
  import { renderFixPlanTerminal } from "../../core/fix/render.js";
7
7
  import { runFix } from "../../core/fix/run.js";
@@ -23,10 +23,14 @@ export async function runFixCommand(options) {
23
23
  const result = await scan({ cwd: target });
24
24
  const plan = await buildFixPlan(result);
25
25
  const cursorContent = await readCursorignore(plan.root);
26
+ const claudeSettingsContent = await readClaudeSettings(plan.root);
27
+ const codexConfigContent = await readCodexConfig(plan.root);
26
28
  const applyResult = await applyFixPlan(plan, { dryRun: true });
27
29
  process.stdout.write(renderFixPlanTerminal(plan, {
28
30
  dryRun: true,
29
31
  cursorContent,
32
+ claudeSettingsContent,
33
+ codexConfigContent,
30
34
  applyResult,
31
35
  }));
32
36
  return EXIT_CODES.SUCCESS;
@@ -38,27 +42,39 @@ export async function runFixCommand(options) {
38
42
  });
39
43
  if (cancelled) {
40
44
  process.stdout.write("\n Cancelled. No files were modified.\n\n");
41
- return EXIT_CODES.SUCCESS;
45
+ return EXIT_CODES.USAGE_ERROR;
42
46
  }
43
47
  const cursorContentAfter = await readCursorignore(plan.root);
48
+ const claudeSettingsAfter = await readClaudeSettings(plan.root);
49
+ const codexConfigAfter = await readCodexConfig(plan.root);
44
50
  process.stdout.write(renderFixPlanTerminal(plan, {
45
51
  dryRun: false,
46
52
  cursorContent: cursorContentAfter,
53
+ claudeSettingsContent: claudeSettingsAfter,
54
+ codexConfigContent: codexConfigAfter,
47
55
  applyResult,
48
56
  }));
49
- // After apply, show a short re-scan hint / delta for Cursor-fixable rules
50
57
  if (applyResult.writtenFiles.length > 0) {
51
58
  const after = await scan({ cwd: target });
52
59
  const remainingSafeContext = after.findings.filter((f) => f.fixability === "safe" &&
53
- (f.ruleId === "context/generated-directory" || f.ruleId === "context/large-log-file") &&
54
- f.affectedAgents.includes("cursor"));
55
- process.stdout.write(colors.dim(` Re-scan: ${remainingSafeContext.length} Cursor-related safe context finding(s) remain.\n\n`));
60
+ (f.ruleId === "context/generated-directory" || f.ruleId === "context/large-log-file"));
61
+ process.stdout.write(colors.dim(` Re-scan: ${remainingSafeContext.length} safe context finding(s) remain.\n\n`));
56
62
  }
57
63
  return EXIT_CODES.SUCCESS;
58
64
  }
59
65
  catch (error) {
60
66
  const message = error instanceof Error ? error.message : String(error);
61
67
  console.error(`Error: ${message}`);
68
+ if (isFixConfigOrPermissionError(message)) {
69
+ return EXIT_CODES.USAGE_ERROR;
70
+ }
62
71
  return EXIT_CODES.INTERNAL_ERROR;
63
72
  }
64
73
  }
74
+ function isFixConfigOrPermissionError(message) {
75
+ return (message.includes("refusing") ||
76
+ message.includes("not valid JSON") ||
77
+ message.includes("EACCES") ||
78
+ message.includes("EPERM") ||
79
+ message.includes("permission denied"));
80
+ }
@@ -1,3 +1,4 @@
1
+ import { type PolicyOptions } from "../../core/policy/evaluate.js";
1
2
  import { type ExitCode } from "../../types/index.js";
2
3
  export interface ScanCommandOptions {
3
4
  targetPath?: string;
@@ -5,6 +6,10 @@ export interface ScanCommandOptions {
5
6
  ci?: boolean;
6
7
  verbose?: boolean;
7
8
  minScore?: number;
9
+ failOnSeverity?: PolicyOptions["failOnSeverity"];
10
+ failOnRules?: string[];
11
+ summary?: boolean;
12
+ annotations?: boolean;
8
13
  }
9
14
  export declare function runScanCommand(options: ScanCommandOptions): Promise<ExitCode>;
10
15
  export declare function resolveTargetArgument(pathArg: string | undefined, cwd?: string): string;
@@ -1,10 +1,12 @@
1
1
  import path from "node:path";
2
+ import { evaluateScanPolicy } from "../../core/policy/evaluate.js";
2
3
  import { scan } from "../../core/scanner/scan.js";
4
+ import { emitGithubReports } from "../../reporters/github/emit.js";
3
5
  import { renderJsonReport } from "../../reporters/json/report.js";
4
6
  import { renderTerminalReport } from "../../reporters/terminal/report.js";
5
7
  import { EXIT_CODES } from "../../types/index.js";
6
- import { resolveRepoRoot } from "../../utils/path.js";
7
8
  import { isDirectory } from "../../utils/fs.js";
9
+ import { resolveRepoRoot } from "../../utils/path.js";
8
10
  export async function runScanCommand(options) {
9
11
  const target = resolveRepoRoot(options.targetPath ?? process.cwd());
10
12
  if (!(await isDirectory(target))) {
@@ -24,13 +26,30 @@ export async function runScanCommand(options) {
24
26
  verbose: options.verbose === true,
25
27
  }));
26
28
  }
27
- if (options.minScore !== undefined && result.scores !== null) {
28
- if (result.scores.overall < options.minScore) {
29
- if (!options.json) {
30
- console.error(`\nCI check failed: overall score ${result.scores.overall} is below --min-score ${options.minScore}`);
29
+ const failOnSeverity = options.failOnSeverity ?? (options.ci === true ? "critical" : undefined);
30
+ const policy = {
31
+ ...(options.minScore !== undefined ? { minimumScore: options.minScore } : {}),
32
+ ...(failOnSeverity !== undefined ? { failOnSeverity } : {}),
33
+ ...(options.failOnRules && options.failOnRules.length > 0
34
+ ? { failOnRules: options.failOnRules }
35
+ : {}),
36
+ };
37
+ const violations = evaluateScanPolicy(result, policy);
38
+ await emitGithubReports({
39
+ mode: "scan",
40
+ findings: result.findings,
41
+ overallScore: result.scores?.overall ?? null,
42
+ violations,
43
+ summary: options.summary === true,
44
+ annotations: options.annotations === true,
45
+ });
46
+ if (violations.length > 0) {
47
+ if (!options.json) {
48
+ for (const violation of violations) {
49
+ console.error(`\n${violation.message}`);
31
50
  }
32
- return EXIT_CODES.ISSUES_OR_THRESHOLD;
33
51
  }
52
+ return EXIT_CODES.ISSUES_OR_THRESHOLD;
34
53
  }
35
54
  return EXIT_CODES.SUCCESS;
36
55
  }
@@ -1,3 +1,4 @@
1
+ import { type PolicyOptions } from "../../core/policy/evaluate.js";
1
2
  import { type ExitCode } from "../../types/index.js";
2
3
  export interface VerifyCommandOptions {
3
4
  targetPath?: string;
@@ -6,6 +7,11 @@ export interface VerifyCommandOptions {
6
7
  ci?: boolean;
7
8
  verbose?: boolean;
8
9
  minScore?: number;
10
+ failOnSeverity?: PolicyOptions["failOnSeverity"];
11
+ failOnRules?: string[];
12
+ failOnNew?: boolean;
13
+ summary?: boolean;
14
+ annotations?: boolean;
9
15
  }
10
16
  /**
11
17
  * Verify: re-scan after Fix and compare against a prior scan JSON baseline.
@@ -1,4 +1,6 @@
1
+ import { evaluateVerifyPolicy } from "../../core/policy/evaluate.js";
1
2
  import { verify } from "../../core/verify/verify.js";
3
+ import { emitGithubReports } from "../../reporters/github/emit.js";
2
4
  import { renderVerifyJsonReport } from "../../reporters/verify/json.js";
3
5
  import { renderVerifyTerminalReport } from "../../reporters/verify/terminal.js";
4
6
  import { EXIT_CODES } from "../../types/index.js";
@@ -25,18 +27,30 @@ export async function runVerifyCommand(options) {
25
27
  else {
26
28
  process.stdout.write(renderVerifyTerminalReport(result, options.verbose === true));
27
29
  }
28
- if (options.minScore !== undefined && result.scores !== null) {
29
- if (result.scores.overall < options.minScore) {
30
- if (!options.json) {
31
- console.error(`\nCI check failed: overall score ${result.scores.overall} is below --min-score ${options.minScore}`);
32
- }
33
- return EXIT_CODES.ISSUES_OR_THRESHOLD;
34
- }
35
- }
36
- // In CI, newly introduced findings are regressions after Fix.
37
- if (options.ci === true && result.summary.new > 0) {
30
+ const failOnNew = options.failOnNew === true || options.ci === true;
31
+ const policy = {
32
+ ...(options.minScore !== undefined ? { minimumScore: options.minScore } : {}),
33
+ ...(options.failOnSeverity !== undefined ? { failOnSeverity: options.failOnSeverity } : {}),
34
+ ...(options.failOnRules && options.failOnRules.length > 0
35
+ ? { failOnRules: options.failOnRules }
36
+ : {}),
37
+ ...(failOnNew ? { failOnNew: true } : {}),
38
+ };
39
+ const violations = evaluateVerifyPolicy(result, policy);
40
+ await emitGithubReports({
41
+ mode: "verify",
42
+ findings: result.after.findings,
43
+ overallScore: result.scores?.overall ?? null,
44
+ violations,
45
+ verifySummary: result.summary,
46
+ summary: options.summary === true,
47
+ annotations: options.annotations === true,
48
+ });
49
+ if (violations.length > 0) {
38
50
  if (!options.json) {
39
- console.error(`\nCI check failed: verify found ${result.summary.new} new finding(s) not present in the baseline`);
51
+ for (const violation of violations) {
52
+ console.error(`\n${violation.message}`);
53
+ }
40
54
  }
41
55
  return EXIT_CODES.ISSUES_OR_THRESHOLD;
42
56
  }