@praneeth_54/agentdoctor 0.1.4-beta → 0.2.0-beta

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/CHANGELOG.md CHANGED
@@ -9,8 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Planned
11
11
 
12
- - Deterministic readiness scoring
13
12
  - Safe automatic fixes
13
+ - Terminal readiness line and GitHub Action score-gate inputs (deferred; see scoring.md v2+)
14
+
15
+ ## [0.2.0-beta] — 2026-08-02
16
+
17
+ Minor beta: deterministic readiness scoring and CLI `--min-score` enforcement.
18
+
19
+ ### Added
20
+
21
+ - Deterministic readiness scoring (v1): `scan()` populates `scoringAvailable: true` and
22
+ `scores` (`overall`, `categories`, `agents`) from post-dedupe findings
23
+ ([docs/scoring.md](docs/scoring.md))
24
+ - CLI `--min-score N` enforcement: exit code `1` when `scores.overall < N`
25
+ - `--ci` without `--min-score` remains report-only (exit `0` on successful scan)
26
+ - Scoring specification and compatibility / exit-code docs updated for shipped behavior
27
+
28
+ ### Compatibility
29
+
30
+ - No new JSON top-level fields (`scoringModel` / `scoreExplanation` deferred)
31
+ - Findings, rule IDs, and agent detection unchanged
32
+ - GitHub Action remains `--ci --json` report-only (no score-gate inputs)
33
+ - Default Action `version` input is `0.2.0-beta`
14
34
 
15
35
  ## [0.1.4-beta] — 2026-08-02
16
36
 
@@ -123,7 +143,8 @@ First public beta.
123
143
  - Not a complete secret scanner
124
144
  - Git “tracked secret” detection deferred
125
145
 
126
- [Unreleased]: https://github.com/pranee54/AgentDoctor/compare/v0.1.4-beta...HEAD
146
+ [Unreleased]: https://github.com/pranee54/AgentDoctor/compare/v0.2.0-beta...HEAD
147
+ [0.2.0-beta]: https://github.com/pranee54/AgentDoctor/releases/tag/v0.2.0-beta
127
148
  [0.1.4-beta]: https://github.com/pranee54/AgentDoctor/releases/tag/v0.1.4-beta
128
149
  [0.1.3-beta]: https://github.com/pranee54/AgentDoctor/releases/tag/v0.1.3-beta
129
150
  [0.1.2-beta]: https://github.com/pranee54/AgentDoctor/releases/tag/v0.1.2-beta
package/README.md CHANGED
@@ -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.1.x-beta`). Readiness scoring and automatic fixes are not available yet.
19
+ Public beta (`0.1.x-beta`). Deterministic readiness scores ship in JSON; automatic fixes are not available yet.
20
20
 
21
21
  ---
22
22
 
@@ -24,12 +24,12 @@ Public beta (`0.1.x-beta`). Readiness scoring and automatic fixes are not availa
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.1.4-beta._
27
+ _Real scan of the included `insecure-agent-project` fixture using AgentDoctor v0.2.0-beta._
28
28
 
29
29
  ```text
30
30
  $ npx @praneeth_54/agentdoctor
31
31
 
32
- 🩺 AgentDoctor v0.1.4-beta
32
+ 🩺 AgentDoctor v0.2.0-beta
33
33
 
34
34
  Scanning repository...
35
35
 
@@ -155,7 +155,7 @@ npx @praneeth_54/agentdoctor
155
155
  Pin a beta version when you need a fixed install:
156
156
 
157
157
  ```bash
158
- npx @praneeth_54/agentdoctor@0.1.4-beta
158
+ npx @praneeth_54/agentdoctor@0.2.0-beta
159
159
  ```
160
160
 
161
161
  ### Global (optional)
@@ -259,7 +259,7 @@ steps:
259
259
 
260
260
  - name: Audit coding-agent configuration
261
261
  id: agentdoctor
262
- uses: pranee54/AgentDoctor@v0.1.4-beta
262
+ uses: pranee54/AgentDoctor@v0.2.0-beta
263
263
  with:
264
264
  path: .
265
265
  output-file: agentdoctor-report.json
@@ -271,7 +271,7 @@ steps:
271
271
  path: ${{ steps.agentdoctor.outputs.report-path }}
272
272
  ```
273
273
 
274
- The action installs the published `@praneeth_54/agentdoctor@0.1.4-beta` package, runs it with
274
+ The action installs the published `@praneeth_54/agentdoctor@0.2.0-beta` package, runs it with
275
275
  `--ci --json`, and writes the report inside the checked-out workspace. It sets up Node.js 20
276
276
  for the CLI. The optional `version` input accepts an exact npm version or the `latest` / `beta`
277
277
  dist-tag.
@@ -281,13 +281,28 @@ dist-tag.
281
281
  Use JSON directly in other CI systems:
282
282
 
283
283
  ```bash
284
+ # Report-only (exit 0 even when findings exist; scores still in JSON)
284
285
  npx @praneeth_54/agentdoctor --ci --json
286
+
287
+ # Fail CI when overall readiness is below 70
288
+ npx @praneeth_54/agentdoctor --ci --json --min-score 70
285
289
  ```
286
290
 
287
- `--ci` runs non-interactively. Until readiness scoring ships, successful scans exit `0` even when findings exist, and `--min-score` is accepted but ignored.
291
+ `--ci` runs non-interactively and does **not** apply an implicit score threshold.
292
+ Use `--min-score N` (with or without `--ci`) to fail with exit code `1` when
293
+ `scores.overall < N`.
288
294
 
289
295
  Exit codes: [docs/exit-codes.md](docs/exit-codes.md). Compatibility promises: [docs/compatibility.md](docs/compatibility.md).
290
296
 
297
+ ### Readiness scoring
298
+
299
+ Scans populate `scoringAvailable: true` and a deterministic `scores` object
300
+ (overall, categories, agents). The terminal report does not render a readiness line yet;
301
+ scores are available in JSON (`--json`).
302
+
303
+ `--min-score N` is enforced by the CLI. Details (weights, security caps, threshold rules,
304
+ and deferred v2 items): [docs/scoring.md](docs/scoring.md).
305
+
291
306
  ---
292
307
 
293
308
  ## Beta limitations
@@ -296,21 +311,21 @@ Honest limits of the current public beta:
296
311
 
297
312
  | Limitation | Status |
298
313
  | ----------------------------------- | --------------------------------------------------------------- |
299
- | Readiness scoring | Not available (`scores` is `null`, `scoringAvailable: false`) |
300
- | `--min-score` | Accepted, ignored until scoring ships |
314
+ | Terminal readiness line | Scores ship in JSON only; terminal does not print N/100 yet |
315
+ | GitHub Action score gates | Action remains `--ci --json` report-only (no `min-score` input) |
301
316
  | Automatic fixes (`agentdoctor fix`) | Stub only — does not modify files |
302
317
  | Secret-content scanning | Filename / config heuristics only |
303
318
  | Detection style | Intentionally conservative; false security findings are avoided |
304
319
  | Agent coverage | Cursor, Claude Code, Codex project configs |
305
320
 
306
- See [CHANGELOG.md](CHANGELOG.md) and [docs/release-notes-v0.1.4-beta.md](docs/release-notes-v0.1.4-beta.md).
321
+ See [CHANGELOG.md](CHANGELOG.md) and [docs/release-notes-v0.2.0-beta.md](docs/release-notes-v0.2.0-beta.md).
307
322
 
308
323
  ---
309
324
 
310
325
  ## Architecture
311
326
 
312
327
  ```text
313
- Discovery → Project detect → Agent adapters → Rule engine → Findings → Terminal / JSON
328
+ Discovery → Project detect → Agent adapters → Rule engine → Findings → Scores → Terminal / JSON
314
329
  ```
315
330
 
316
331
  Details: [docs/architecture.md](docs/architecture.md)
@@ -319,17 +334,18 @@ Details: [docs/architecture.md](docs/architecture.md)
319
334
 
320
335
  ## Documentation
321
336
 
322
- | Doc | Contents |
323
- | ------------------------------------------------------------------ | ------------------------------ |
324
- | [docs/README.md](docs/README.md) | Documentation index |
325
- | [docs/architecture.md](docs/architecture.md) | Scan pipeline |
326
- | [docs/rules.md](docs/rules.md) | Stable rule IDs |
327
- | [docs/exit-codes.md](docs/exit-codes.md) | Process exit codes |
328
- | [docs/compatibility.md](docs/compatibility.md) | Beta compatibility promises |
329
- | [docs/development.md](docs/development.md) | Local development |
330
- | [docs/github-launch-checklist.md](docs/github-launch-checklist.md) | GitHub About / topics / launch |
331
- | [ROADMAP.md](ROADMAP.md) | Near- and medium-term plans |
332
- | [CHANGELOG.md](CHANGELOG.md) | Release history |
337
+ | Doc | Contents |
338
+ | ------------------------------------------------------------------ | ------------------------------- |
339
+ | [docs/README.md](docs/README.md) | Documentation index |
340
+ | [docs/architecture.md](docs/architecture.md) | Scan pipeline |
341
+ | [docs/rules.md](docs/rules.md) | Stable rule IDs |
342
+ | [docs/exit-codes.md](docs/exit-codes.md) | Process exit codes |
343
+ | [docs/scoring.md](docs/scoring.md) | Readiness scoring specification |
344
+ | [docs/compatibility.md](docs/compatibility.md) | Beta compatibility promises |
345
+ | [docs/development.md](docs/development.md) | Local development |
346
+ | [docs/github-launch-checklist.md](docs/github-launch-checklist.md) | GitHub About / topics / launch |
347
+ | [ROADMAP.md](ROADMAP.md) | Near- and medium-term plans |
348
+ | [CHANGELOG.md](CHANGELOG.md) | Release history |
333
349
 
334
350
  ---
335
351
 
@@ -15,7 +15,7 @@ export async function runDoctorCommand() {
15
15
  lines.push(` ${symbolOk()} Core scan API available`);
16
16
  lines.push("");
17
17
  lines.push(colors.dim("Environment looks ready."));
18
- lines.push(colors.dim("Readiness scoring and automatic fixes are not available yet."));
18
+ lines.push(colors.dim("Automatic fixes are not available yet."));
19
19
  lines.push("");
20
20
  process.stdout.write(lines.join("\n"));
21
21
  return EXIT_CODES.SUCCESS;
@@ -24,20 +24,12 @@ export async function runScanCommand(options) {
24
24
  verbose: options.verbose === true,
25
25
  }));
26
26
  }
27
- if (options.ci || options.minScore !== undefined) {
28
- if (!result.scoringAvailable || result.scores === null) {
27
+ if (options.minScore !== undefined && result.scores !== null) {
28
+ if (result.scores.overall < options.minScore) {
29
29
  if (!options.json) {
30
- console.error("Note: readiness scoring is not available in this release; --min-score was ignored.");
31
- }
32
- }
33
- else {
34
- const threshold = options.minScore ?? 0;
35
- if (result.scores.overall < threshold) {
36
- if (!options.json) {
37
- console.error(`\nCI check failed: overall score ${result.scores.overall} is below --min-score ${threshold}`);
38
- }
39
- return EXIT_CODES.ISSUES_OR_THRESHOLD;
30
+ console.error(`\nCI check failed: overall score ${result.scores.overall} is below --min-score ${options.minScore}`);
40
31
  }
32
+ return EXIT_CODES.ISSUES_OR_THRESHOLD;
41
33
  }
42
34
  }
43
35
  return EXIT_CODES.SUCCESS;
@@ -29,9 +29,9 @@ export function createProgram() {
29
29
  .version(PACKAGE_VERSION, "-V, --version", "Print AgentDoctor version")
30
30
  .argument("[path]", "Repository path to scan (default: current directory)")
31
31
  .option("--json", "Emit machine-readable JSON (no decorative output)", false)
32
- .option("--ci", "CI mode (non-interactive; --min-score is ignored until scoring ships)", false)
32
+ .option("--ci", "CI mode (non-interactive; report-only unless --min-score is set)", false)
33
33
  .option("--verbose", "Show timing and extra diagnostics", false)
34
- .option("--min-score <number>", "Fail when overall score is below this (no-op until scoring ships)", parseMinScore)
34
+ .option("--min-score <number>", "Exit 1 when overall readiness score is below this (0-100)", parseMinScore)
35
35
  .action(async (pathArg, options) => {
36
36
  const minScore = readMinScore(options);
37
37
  const code = await runScanCommand({
@@ -48,9 +48,9 @@ export function createProgram() {
48
48
  .description("Scan a repository for AI coding agent configuration issues (default command)")
49
49
  .argument("[path]", "Repository path to scan")
50
50
  .option("--json", "Emit machine-readable JSON", false)
51
- .option("--ci", "CI mode (--min-score ignored until scoring ships)", false)
51
+ .option("--ci", "CI mode (non-interactive; report-only unless --min-score is set)", false)
52
52
  .option("--verbose", "Show timing and extra diagnostics", false)
53
- .option("--min-score <number>", "Fail when overall score is below this (no-op until scoring ships)", parseMinScore)
53
+ .option("--min-score <number>", "Exit 1 when overall readiness score is below this (0-100)", parseMinScore)
54
54
  .action(async (pathArg, options) => {
55
55
  const minScore = readMinScore(options);
56
56
  const code = await runScanCommand({
@@ -1,4 +1,4 @@
1
- export declare const PACKAGE_VERSION = "0.1.4-beta";
1
+ export declare const PACKAGE_VERSION = "0.2.0-beta";
2
2
  export declare const DEFAULT_MAX_FILE_SIZE_BYTES: number;
3
3
  /** Directories skipped during normal discovery (unless a rule needs them later). */
4
4
  export declare const DEFAULT_IGNORE_DIRECTORIES: Set<string>;
package/dist/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- export const PACKAGE_VERSION = "0.1.4-beta";
1
+ export const PACKAGE_VERSION = "0.2.0-beta";
2
2
  export const DEFAULT_MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024; // 2 MiB
3
3
  /** Directories skipped during normal discovery (unless a rule needs them later). */
4
4
  export const DEFAULT_IGNORE_DIRECTORIES = new Set([
@@ -1,7 +1,6 @@
1
1
  import type { ScanOptions, ScanResult } from "../../types/index.js";
2
2
  /**
3
3
  * Public scan entry point.
4
- * Pipeline: discovery → project detection → agent adapters → rule engine → findings.
5
- * Readiness scoring is reserved for a later release (`scores` remains null for now).
4
+ * Pipeline: discovery → project detection → agent adapters → rule engine → findings → scores.
6
5
  */
7
6
  export declare function scan(options?: ScanOptions): Promise<ScanResult>;
@@ -4,10 +4,10 @@ import { detectProject } from "../../detectors/project.js";
4
4
  import { sanitizeTerminalText } from "../../security/redaction.js";
5
5
  import { buildRuleContext } from "../rules/build-context.js";
6
6
  import { runRules } from "../rules/run-rules.js";
7
+ import { computeReadinessScores } from "../scoring/compute-scores.js";
7
8
  /**
8
9
  * Public scan entry point.
9
- * Pipeline: discovery → project detection → agent adapters → rule engine → findings.
10
- * Readiness scoring is reserved for a later release (`scores` remains null for now).
10
+ * Pipeline: discovery → project detection → agent adapters → rule engine → findings → scores.
11
11
  */
12
12
  export async function scan(options = {}) {
13
13
  const totalStarted = performance.now();
@@ -51,21 +51,24 @@ export async function scan(options = {}) {
51
51
  if (agentSecurityAnalysis === "limited") {
52
52
  warnings.push("No supported coding-agent configuration detected; agent-specific security exposure checks are limited.");
53
53
  }
54
+ const scoringStarted = performance.now();
55
+ const scores = computeReadinessScores(findings);
56
+ const scoringMs = Math.round(performance.now() - scoringStarted);
54
57
  return {
55
58
  version: PACKAGE_VERSION,
56
59
  repository,
57
60
  agents,
58
61
  findings,
59
62
  summary,
60
- scores: null,
61
- scoringAvailable: false,
63
+ scores,
64
+ scoringAvailable: true,
62
65
  agentSecurityAnalysis,
63
66
  timing: {
64
67
  discoveryMs: discovery.elapsedMs,
65
68
  detectionMs,
66
69
  agentsMs,
67
70
  rulesMs,
68
- scoringMs: 0,
71
+ scoringMs,
69
72
  totalMs: Math.round(performance.now() - totalStarted),
70
73
  },
71
74
  diagnostics: {
@@ -0,0 +1,6 @@
1
+ import type { Finding, Scores } from "../../types/index.js";
2
+ /**
3
+ * v1 readiness scores from post-dedupe findings.
4
+ * Spec: docs/scoring.md
5
+ */
6
+ export declare function computeReadinessScores(findings: readonly Finding[]): Scores;
@@ -0,0 +1,90 @@
1
+ const SEVERITY_BASE = {
2
+ critical: 35,
3
+ warning: 10,
4
+ info: 2,
5
+ };
6
+ const SEVERITY_SORT_RANK = {
7
+ critical: 0,
8
+ warning: 1,
9
+ info: 2,
10
+ };
11
+ /**
12
+ * v1 readiness scores from post-dedupe findings.
13
+ * Spec: docs/scoring.md
14
+ */
15
+ export function computeReadinessScores(findings) {
16
+ const overallRaw = 100 - sumDeductions(findings);
17
+ const overall = clampScore(applySecurityCaps(overallRaw, findings));
18
+ return {
19
+ overall,
20
+ categories: {
21
+ security: scoreSubset(findings.filter((f) => f.category === "security")),
22
+ context: scoreSubset(findings.filter((f) => f.category === "context")),
23
+ instructions: scoreSubset(findings.filter((f) => f.category === "instructions")),
24
+ mcp: scoreSubset(findings.filter((f) => f.category === "mcp")),
25
+ compatibility: scoreSubset(findings.filter((f) => f.category === "compatibility")),
26
+ performance: scoreSubset(findings.filter((f) => f.category === "performance")),
27
+ },
28
+ agents: {
29
+ cursor: scoreSubset(findings.filter((f) => f.affectedAgents.includes("cursor"))),
30
+ "claude-code": scoreSubset(findings.filter((f) => f.affectedAgents.includes("claude-code"))),
31
+ codex: scoreSubset(findings.filter((f) => f.affectedAgents.includes("codex"))),
32
+ },
33
+ };
34
+ }
35
+ function scoreSubset(findings) {
36
+ return clampScore(100 - sumDeductions(findings));
37
+ }
38
+ function sumDeductions(findings) {
39
+ const sorted = sortFindings(findings);
40
+ const severityIndex = {
41
+ critical: 0,
42
+ warning: 0,
43
+ info: 0,
44
+ };
45
+ let total = 0;
46
+ for (const finding of sorted) {
47
+ const occurrence = severityIndex[finding.severity];
48
+ severityIndex[finding.severity] = occurrence + 1;
49
+ total += SEVERITY_BASE[finding.severity] * diminishingMultiplier(occurrence);
50
+ }
51
+ return total;
52
+ }
53
+ function diminishingMultiplier(zeroBasedIndex) {
54
+ if (zeroBasedIndex === 0)
55
+ return 1;
56
+ if (zeroBasedIndex === 1)
57
+ return 0.7;
58
+ if (zeroBasedIndex === 2)
59
+ return 0.5;
60
+ return 0.35;
61
+ }
62
+ function sortFindings(findings) {
63
+ return [...findings].sort((a, b) => {
64
+ const sev = SEVERITY_SORT_RANK[a.severity] - SEVERITY_SORT_RANK[b.severity];
65
+ if (sev !== 0)
66
+ return sev;
67
+ const rule = a.ruleId.localeCompare(b.ruleId);
68
+ if (rule !== 0)
69
+ return rule;
70
+ const pathA = a.evidence?.path ?? "";
71
+ const pathB = b.evidence?.path ?? "";
72
+ const pathCmp = pathA.localeCompare(pathB);
73
+ if (pathCmp !== 0)
74
+ return pathCmp;
75
+ return a.id.localeCompare(b.id);
76
+ });
77
+ }
78
+ function applySecurityCaps(overall, findings) {
79
+ const securityCriticals = findings.filter((f) => f.category === "security" && f.severity === "critical").length;
80
+ if (securityCriticals >= 2) {
81
+ return Math.min(overall, 49);
82
+ }
83
+ if (securityCriticals >= 1) {
84
+ return Math.min(overall, 69);
85
+ }
86
+ return overall;
87
+ }
88
+ function clampScore(value) {
89
+ return Math.max(0, Math.min(100, Math.round(value)));
90
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Scores } from "../../types/index.js";
2
2
  /**
3
- * Internal scoring stub retained for upcoming readiness scores.
4
- * Production scans currently return `scores: null`.
5
- * A clean undetected-agent repo stays below 100 by design.
3
+ * Historical filesScanned-based stub. Kept for unit tests that document the
4
+ * pre-v1 placeholder behavior. Production scans use computeReadinessScores
5
+ * (docs/scoring.md) — do not call this from scan().
6
6
  */
7
7
  export declare function computePlaceholderScores(filesScanned: number): Scores;
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Internal scoring stub retained for upcoming readiness scores.
3
- * Production scans currently return `scores: null`.
4
- * A clean undetected-agent repo stays below 100 by design.
2
+ * Historical filesScanned-based stub. Kept for unit tests that document the
3
+ * pre-v1 placeholder behavior. Production scans use computeReadinessScores
4
+ * (docs/scoring.md) — do not call this from scan().
5
5
  */
6
6
  export function computePlaceholderScores(filesScanned) {
7
7
  const base = 72;
@@ -106,11 +106,11 @@ export interface ScanResult {
106
106
  findings: Finding[];
107
107
  summary: FindingsSummary;
108
108
  /**
109
- * Readiness scores when scoring is available.
110
- * Null while scoring is unavailable — do not treat as readiness.
109
+ * Readiness scores (v1). Always populated when `scoringAvailable` is true.
110
+ * Remains typed as nullable for older consumers / transitional tooling.
111
111
  */
112
112
  scores: Scores | null;
113
- /** False until the readiness scoring model ships. */
113
+ /** True when the readiness scoring model is active and `scores` is populated. */
114
114
  scoringAvailable: boolean;
115
115
  /**
116
116
  * `limited` when no supported coding agent is detected/configured.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praneeth_54/agentdoctor",
3
- "version": "0.1.4-beta",
3
+ "version": "0.2.0-beta",
4
4
  "description": "Audit AI coding agent configuration in a repository — local, deterministic, no API key.",
5
5
  "type": "module",
6
6
  "bin": {