@praneeth_54/agentdoctor 1.1.0 → 2.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 (170) hide show
  1. package/CHANGELOG.md +119 -5
  2. package/README.md +165 -332
  3. package/dist/agents/aider/adapter.d.ts +1 -0
  4. package/dist/agents/aider/adapter.js +1 -0
  5. package/dist/agents/aider/detector.d.ts +14 -0
  6. package/dist/agents/aider/detector.js +120 -0
  7. package/dist/agents/copilot/adapter.d.ts +1 -0
  8. package/dist/agents/copilot/adapter.js +1 -0
  9. package/dist/agents/copilot/detector.d.ts +13 -0
  10. package/dist/agents/copilot/detector.js +113 -0
  11. package/dist/agents/gemini/adapter.d.ts +1 -0
  12. package/dist/agents/gemini/adapter.js +1 -0
  13. package/dist/agents/gemini/detector.d.ts +13 -0
  14. package/dist/agents/gemini/detector.js +151 -0
  15. package/dist/agents/registry.js +13 -1
  16. package/dist/agents/types.d.ts +1 -1
  17. package/dist/agents/windsurf/adapter.d.ts +1 -0
  18. package/dist/agents/windsurf/adapter.js +1 -0
  19. package/dist/agents/windsurf/detector.d.ts +14 -0
  20. package/dist/agents/windsurf/detector.js +119 -0
  21. package/dist/architecture/c4.d.ts +22 -0
  22. package/dist/architecture/c4.js +62 -0
  23. package/dist/cli/commands/brain.d.ts +12 -0
  24. package/dist/cli/commands/brain.js +162 -0
  25. package/dist/cli/commands/complete.d.ts +23 -0
  26. package/dist/cli/commands/complete.js +232 -0
  27. package/dist/cli/commands/explain.js +1 -1
  28. package/dist/cli/commands/fix.js +22 -2
  29. package/dist/cli/commands/mcp.d.ts +8 -0
  30. package/dist/cli/commands/mcp.js +14 -0
  31. package/dist/cli/commands/platform.d.ts +15 -0
  32. package/dist/cli/commands/platform.js +191 -0
  33. package/dist/cli/commands/v2.d.ts +55 -0
  34. package/dist/cli/commands/v2.js +278 -0
  35. package/dist/cli/program.js +809 -2
  36. package/dist/constants.d.ts +3 -2
  37. package/dist/constants.js +5 -1
  38. package/dist/contracts/adapters.d.ts +7 -0
  39. package/dist/contracts/adapters.js +60 -0
  40. package/dist/contracts/index.d.ts +162 -0
  41. package/dist/contracts/index.js +18 -0
  42. package/dist/core/baseline/store.d.ts +48 -0
  43. package/dist/core/baseline/store.js +163 -0
  44. package/dist/core/brain-cli/service.d.ts +26 -0
  45. package/dist/core/brain-cli/service.js +175 -0
  46. package/dist/core/brain-product/init.d.ts +45 -0
  47. package/dist/core/brain-product/init.js +232 -0
  48. package/dist/core/changes/analyze.d.ts +41 -0
  49. package/dist/core/changes/analyze.js +223 -0
  50. package/dist/core/context-health/analyze.d.ts +15 -0
  51. package/dist/core/context-health/analyze.js +234 -0
  52. package/dist/core/fix/apply.d.ts +21 -4
  53. package/dist/core/fix/apply.js +118 -18
  54. package/dist/core/fix/backup.d.ts +30 -0
  55. package/dist/core/fix/backup.js +193 -0
  56. package/dist/core/fix/plan.d.ts +6 -3
  57. package/dist/core/fix/plan.js +113 -9
  58. package/dist/core/fix/render.d.ts +2 -0
  59. package/dist/core/fix/render.js +36 -21
  60. package/dist/core/fix/run.js +5 -1
  61. package/dist/core/fix/safe-target.d.ts +14 -0
  62. package/dist/core/fix/safe-target.js +81 -0
  63. package/dist/core/fix/types.d.ts +12 -1
  64. package/dist/core/fix/types.js +2 -0
  65. package/dist/core/fix/writers/claude-settings.js +3 -1
  66. package/dist/core/fix/writers/codex-config.d.ts +3 -0
  67. package/dist/core/fix/writers/codex-config.js +24 -3
  68. package/dist/core/fix/writers/cursorignore.js +3 -1
  69. package/dist/core/fix/writers/simple-ignore.d.ts +16 -0
  70. package/dist/core/fix/writers/simple-ignore.js +68 -0
  71. package/dist/core/monorepo/detect.d.ts +13 -0
  72. package/dist/core/monorepo/detect.js +121 -0
  73. package/dist/core/rules/build-context.js +22 -3
  74. package/dist/core/rules/context/generated-directory.js +11 -1
  75. package/dist/core/rules/context/large-instruction-file.js +6 -0
  76. package/dist/core/rules/context/large-log-file.js +16 -1
  77. package/dist/core/rules/ignore.d.ts +11 -1
  78. package/dist/core/rules/ignore.js +22 -1
  79. package/dist/core/rules/instructions/empty-instructions.js +7 -0
  80. package/dist/core/rules/security/env-file-exposure.js +23 -2
  81. package/dist/core/rules/security/private-key-file.js +1 -1
  82. package/dist/core/scanner/scan.js +1 -1
  83. package/dist/core/schemas/validate.d.ts +6 -0
  84. package/dist/core/schemas/validate.js +62 -0
  85. package/dist/core/scoring/compute-scores.d.ts +1 -1
  86. package/dist/core/scoring/compute-scores.js +5 -1
  87. package/dist/core/scoring/placeholder.d.ts +1 -1
  88. package/dist/core/scoring/placeholder.js +5 -1
  89. package/dist/core/secrets/scan.d.ts +23 -0
  90. package/dist/core/secrets/scan.js +172 -0
  91. package/dist/dashboard/server.d.ts +24 -0
  92. package/dist/dashboard/server.js +386 -0
  93. package/dist/enforcement/runner.d.ts +23 -0
  94. package/dist/enforcement/runner.js +56 -0
  95. package/dist/index.d.ts +13 -0
  96. package/dist/index.js +12 -0
  97. package/dist/integrations/github/pr-analyze.d.ts +28 -0
  98. package/dist/integrations/github/pr-analyze.js +98 -0
  99. package/dist/integrations/local-ai/provider.d.ts +45 -0
  100. package/dist/integrations/local-ai/provider.js +104 -0
  101. package/dist/intelligence/git/analyze.d.ts +34 -0
  102. package/dist/intelligence/git/analyze.js +136 -0
  103. package/dist/intelligence/graph/build.d.ts +13 -0
  104. package/dist/intelligence/graph/build.js +215 -0
  105. package/dist/knowledge/store.d.ts +25 -0
  106. package/dist/knowledge/store.js +91 -0
  107. package/dist/mcp/agentdoctor/server.d.ts +16 -0
  108. package/dist/mcp/agentdoctor/server.js +67 -0
  109. package/dist/mcp/brain/session.js +7 -7
  110. package/dist/mcp/intelligence/handlers.d.ts +11 -0
  111. package/dist/mcp/intelligence/handlers.js +175 -0
  112. package/dist/mcp/intelligence/path-safety.d.ts +11 -0
  113. package/dist/mcp/intelligence/path-safety.js +82 -0
  114. package/dist/mcp/intelligence/registry.d.ts +8 -0
  115. package/dist/mcp/intelligence/registry.js +160 -0
  116. package/dist/ops/health.d.ts +18 -0
  117. package/dist/ops/health.js +51 -0
  118. package/dist/platform/ai-quality/analyze.d.ts +9 -0
  119. package/dist/platform/ai-quality/analyze.js +65 -0
  120. package/dist/platform/architecture/drift.d.ts +17 -0
  121. package/dist/platform/architecture/drift.js +74 -0
  122. package/dist/platform/auth/local.d.ts +15 -0
  123. package/dist/platform/auth/local.js +58 -0
  124. package/dist/platform/context-security/analyze.d.ts +6 -0
  125. package/dist/platform/context-security/analyze.js +97 -0
  126. package/dist/platform/firewall/evaluate.d.ts +54 -0
  127. package/dist/platform/firewall/evaluate.js +308 -0
  128. package/dist/platform/graph/build.d.ts +6 -0
  129. package/dist/platform/graph/build.js +193 -0
  130. package/dist/platform/health/analyze.d.ts +7 -0
  131. package/dist/platform/health/analyze.js +186 -0
  132. package/dist/platform/index.d.ts +27 -0
  133. package/dist/platform/index.js +87 -0
  134. package/dist/platform/knowledge/analyze.d.ts +20 -0
  135. package/dist/platform/knowledge/analyze.js +96 -0
  136. package/dist/platform/provenance/build.d.ts +44 -0
  137. package/dist/platform/provenance/build.js +65 -0
  138. package/dist/platform/readiness/scorecard.d.ts +5 -0
  139. package/dist/platform/readiness/scorecard.js +98 -0
  140. package/dist/platform/refactor/impact.d.ts +20 -0
  141. package/dist/platform/refactor/impact.js +60 -0
  142. package/dist/platform/reports/export.d.ts +12 -0
  143. package/dist/platform/reports/export.js +92 -0
  144. package/dist/platform/security/redact.d.ts +15 -0
  145. package/dist/platform/security/redact.js +87 -0
  146. package/dist/platform/sessions/store.d.ts +32 -0
  147. package/dist/platform/sessions/store.js +97 -0
  148. package/dist/platform/store.d.ts +6 -0
  149. package/dist/platform/store.js +57 -0
  150. package/dist/platform/test-impact/analyze.d.ts +32 -0
  151. package/dist/platform/test-impact/analyze.js +164 -0
  152. package/dist/platform/time-machine/compare.d.ts +17 -0
  153. package/dist/platform/time-machine/compare.js +62 -0
  154. package/dist/platform/tokens/plan.d.ts +28 -0
  155. package/dist/platform/tokens/plan.js +89 -0
  156. package/dist/platform/types.d.ts +111 -0
  157. package/dist/platform/types.js +5 -0
  158. package/dist/plugins/runtime.d.ts +36 -0
  159. package/dist/plugins/runtime.js +101 -0
  160. package/dist/plugins/sdk.d.ts +29 -0
  161. package/dist/plugins/sdk.js +114 -0
  162. package/dist/policy/packs.d.ts +9 -0
  163. package/dist/policy/packs.js +59 -0
  164. package/dist/reporters/terminal/report.js +38 -5
  165. package/dist/storage/provider.d.ts +35 -0
  166. package/dist/storage/provider.js +103 -0
  167. package/dist/team/auth.d.ts +35 -0
  168. package/dist/team/auth.js +70 -0
  169. package/dist/types/index.d.ts +5 -1
  170. package/package.json +14 -7
@@ -0,0 +1,121 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
4
+ async function readJson(file) {
5
+ try {
6
+ const raw = await fs.readFile(file, "utf8");
7
+ const parsed = JSON.parse(raw);
8
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
9
+ return null;
10
+ return parsed;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ async function resolveGlobDirs(root, pattern) {
17
+ // Support simple patterns: "packages/*", "apps/*", "."
18
+ if (pattern === "." || pattern === "./") {
19
+ return [root];
20
+ }
21
+ const normalized = pattern.replace(/^\.\//, "").replace(/\/$/, "");
22
+ if (normalized.endsWith("/*")) {
23
+ const parent = path.join(root, normalized.slice(0, -2));
24
+ try {
25
+ const entries = await fs.readdir(parent, { withFileTypes: true });
26
+ return entries
27
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
28
+ .map((e) => path.join(parent, e.name));
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ const candidate = path.join(root, normalized);
35
+ try {
36
+ const st = await fs.stat(candidate);
37
+ return st.isDirectory() ? [candidate] : [];
38
+ }
39
+ catch {
40
+ return [];
41
+ }
42
+ }
43
+ async function packageFromDir(root, dir) {
44
+ const pkg = await readJson(path.join(dir, "package.json"));
45
+ if (!pkg)
46
+ return null;
47
+ const name = typeof pkg.name === "string" ? pkg.name : path.basename(dir);
48
+ return {
49
+ name,
50
+ relativePath: toPosixRelative(root, dir) || ".",
51
+ absolutePath: dir,
52
+ };
53
+ }
54
+ export async function detectMonorepo(rootInput) {
55
+ const root = resolveRepoRoot(rootInput);
56
+ const limitations = [];
57
+ const packages = [];
58
+ const rootPkg = await readJson(path.join(root, "package.json"));
59
+ let tool = "none";
60
+ const patterns = [];
61
+ if (rootPkg) {
62
+ const workspaces = rootPkg.workspaces;
63
+ if (Array.isArray(workspaces)) {
64
+ tool = "npm-workspaces";
65
+ for (const p of workspaces) {
66
+ if (typeof p === "string")
67
+ patterns.push(p);
68
+ }
69
+ }
70
+ else if (workspaces && typeof workspaces === "object" && !Array.isArray(workspaces)) {
71
+ const pkgs = workspaces.packages;
72
+ if (Array.isArray(pkgs)) {
73
+ tool = "npm-workspaces";
74
+ for (const p of pkgs) {
75
+ if (typeof p === "string")
76
+ patterns.push(p);
77
+ }
78
+ }
79
+ }
80
+ }
81
+ try {
82
+ const pnpmRaw = await fs.readFile(path.join(root, "pnpm-workspace.yaml"), "utf8");
83
+ tool = "pnpm-workspaces";
84
+ for (const line of pnpmRaw.split(/\r?\n/)) {
85
+ const m = line.match(/^\s*-\s+['"]?([^'"]+)['"]?\s*$/);
86
+ if (m?.[1])
87
+ patterns.push(m[1]);
88
+ }
89
+ }
90
+ catch {
91
+ // optional
92
+ }
93
+ const seen = new Set();
94
+ for (const pattern of patterns) {
95
+ const dirs = await resolveGlobDirs(root, pattern);
96
+ for (const dir of dirs) {
97
+ const pkg = await packageFromDir(root, dir);
98
+ if (!pkg)
99
+ continue;
100
+ if (seen.has(pkg.absolutePath))
101
+ continue;
102
+ seen.add(pkg.absolutePath);
103
+ packages.push(pkg);
104
+ }
105
+ }
106
+ if (packages.length === 0) {
107
+ const self = await packageFromDir(root, root);
108
+ if (self) {
109
+ packages.push(self);
110
+ }
111
+ limitations.push("No workspace packages discovered; treating repository as a single package");
112
+ }
113
+ packages.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
114
+ return {
115
+ root,
116
+ isMonorepo: tool !== "none",
117
+ tool,
118
+ packages,
119
+ limitations,
120
+ };
121
+ }
@@ -1,10 +1,13 @@
1
1
  import { parseProjectMcpConfigs } from "../mcp/parse.js";
2
2
  import { createIgnoreIndex, parseIgnoreFile, relativizeIgnorePatterns } from "./ignore.js";
3
3
  import { TextCache } from "./text-cache.js";
4
+ const ROOT_IGNORE_FILES = new Set([".gitignore", ".cursorignore", ".geminiignore", ".aiderignore"]);
4
5
  export async function buildRuleContext(options) {
5
6
  const textCache = new TextCache(options.root, options.maxFileSizeBytes);
6
7
  const gitignorePatterns = [];
7
8
  const cursorignorePatterns = [];
9
+ const geminiignorePatterns = [];
10
+ const aiderignorePatterns = [];
8
11
  const rootGitignore = await textCache.read(".gitignore");
9
12
  if (rootGitignore.text) {
10
13
  gitignorePatterns.push(...parseIgnoreFile(rootGitignore.text));
@@ -13,12 +16,20 @@ export async function buildRuleContext(options) {
13
16
  if (rootCursorignore.text) {
14
17
  cursorignorePatterns.push(...parseIgnoreFile(rootCursorignore.text));
15
18
  }
19
+ const rootGeminiignore = await textCache.read(".geminiignore");
20
+ if (rootGeminiignore.text) {
21
+ geminiignorePatterns.push(...parseIgnoreFile(rootGeminiignore.text));
22
+ }
23
+ const rootAiderignore = await textCache.read(".aiderignore");
24
+ if (rootAiderignore.text) {
25
+ aiderignorePatterns.push(...parseIgnoreFile(rootAiderignore.text));
26
+ }
16
27
  for (const file of options.discovery.files) {
17
28
  const base = file.relativePath.split("/").pop() ?? file.relativePath;
18
- if (base !== ".gitignore" && base !== ".cursorignore") {
29
+ if (!ROOT_IGNORE_FILES.has(base)) {
19
30
  continue;
20
31
  }
21
- if (file.relativePath === ".gitignore" || file.relativePath === ".cursorignore") {
32
+ if (ROOT_IGNORE_FILES.has(file.relativePath)) {
22
33
  continue;
23
34
  }
24
35
  const cached = await textCache.read(file.relativePath);
@@ -32,13 +43,21 @@ export async function buildRuleContext(options) {
32
43
  if (base === ".gitignore") {
33
44
  gitignorePatterns.push(...patterns);
34
45
  }
35
- else {
46
+ else if (base === ".cursorignore") {
36
47
  cursorignorePatterns.push(...patterns);
37
48
  }
49
+ else if (base === ".geminiignore") {
50
+ geminiignorePatterns.push(...patterns);
51
+ }
52
+ else if (base === ".aiderignore") {
53
+ aiderignorePatterns.push(...patterns);
54
+ }
38
55
  }
39
56
  const ignore = createIgnoreIndex({
40
57
  gitignorePatterns,
41
58
  cursorignorePatterns,
59
+ geminiignorePatterns,
60
+ aiderignorePatterns,
42
61
  });
43
62
  const mcp = await parseProjectMcpConfigs(options.root, options.maxFileSizeBytes);
44
63
  const instructionPaths = new Set();
@@ -33,6 +33,16 @@ async function agentsStillExposed(context, relativePath, configured) {
33
33
  if (agent === "codex" && (await codexDeniesPath(context, relativePath))) {
34
34
  continue;
35
35
  }
36
+ if (agent === "gemini-cli" &&
37
+ (context.ignore.matchesGeminiignore(relativePath) ||
38
+ context.ignore.matchesGeminiignore(`${relativePath}/`))) {
39
+ continue;
40
+ }
41
+ if (agent === "aider" &&
42
+ (context.ignore.matchesAiderignore(relativePath) ||
43
+ context.ignore.matchesAiderignore(`${relativePath}/`))) {
44
+ continue;
45
+ }
36
46
  exposed.push(agent);
37
47
  }
38
48
  return exposed;
@@ -78,7 +88,7 @@ export const generatedDirectoryRule = {
78
88
  title: "Generated directory may enter agent context",
79
89
  message: `${relativePath}/ (${entry.label}) is present and no project ignore pattern was detected`,
80
90
  whyItMatters: "Generated directories are usually low-value for coding agents and can bloat indexing/context when not excluded.",
81
- recommendation: `Add an ignore pattern covering ${relativePath}/ to .gitignore and agent exclusions (.cursorignore, Claude Code Read deny, and/or Codex filesystem deny).`,
91
+ recommendation: `Add an ignore pattern covering ${relativePath}/ to .gitignore and agent exclusions (.cursorignore, Claude Code Read deny, Codex filesystem deny, .geminiignore, and/or .aiderignore).`,
82
92
  affectedAgents: affected,
83
93
  evidence: { path: relativePath },
84
94
  fixability: "safe",
@@ -7,6 +7,12 @@ const INSTRUCTION_KINDS = new Set([
7
7
  "claude-md",
8
8
  "claude-local-md",
9
9
  "claude-rule-md",
10
+ "copilot-instructions",
11
+ "copilot-path-instructions",
12
+ "windsurf-rule-md",
13
+ "windsurf-legacy-rules",
14
+ "gemini-md",
15
+ "aider-conventions",
10
16
  ]);
11
17
  export const largeInstructionFileRule = {
12
18
  id: "context/large-instruction-file",
@@ -26,7 +26,15 @@ export const largeLogFileRule = {
26
26
  if (context.ignore.matchesGitignore(file.relativePath)) {
27
27
  continue;
28
28
  }
29
- const defaultAgents = ["cursor", "claude-code", "codex"];
29
+ const defaultAgents = [
30
+ "cursor",
31
+ "claude-code",
32
+ "codex",
33
+ "copilot",
34
+ "windsurf",
35
+ "gemini-cli",
36
+ "aider",
37
+ ];
30
38
  const candidates = configured.length > 0 ? configured : defaultAgents;
31
39
  const affected = [];
32
40
  for (const agent of candidates) {
@@ -39,6 +47,13 @@ export const largeLogFileRule = {
39
47
  if (agent === "codex" && (await codexDeniesPath(context, file.relativePath))) {
40
48
  continue;
41
49
  }
50
+ if (agent === "gemini-cli" && context.ignore.matchesGeminiignore(file.relativePath)) {
51
+ continue;
52
+ }
53
+ if (agent === "aider" && context.ignore.matchesAiderignore(file.relativePath)) {
54
+ continue;
55
+ }
56
+ // Copilot / Windsurf: no official project deny/ignore path.
42
57
  affected.push(agent);
43
58
  }
44
59
  if (affected.length === 0) {
@@ -1,14 +1,22 @@
1
1
  /**
2
- * Minimal gitignore-style pattern matching for .gitignore / .cursorignore.
2
+ * Minimal gitignore-style pattern matching for .gitignore / agent ignore files.
3
3
  * Supports common patterns used in agent ignore files — not a full gitignore clone.
4
4
  */
5
5
  export interface IgnoreIndex {
6
6
  gitignorePatterns: string[];
7
7
  cursorignorePatterns: string[];
8
+ geminiignorePatterns: string[];
9
+ aiderignorePatterns: string[];
8
10
  matchesGitignore(relativePath: string): boolean;
9
11
  matchesCursorignore(relativePath: string): boolean;
12
+ matchesGeminiignore(relativePath: string): boolean;
13
+ matchesAiderignore(relativePath: string): boolean;
10
14
  /** Whether a path appears excluded for Cursor agent/indexing purposes. */
11
15
  isExcludedForCursor(relativePath: string): boolean;
16
+ /** Gemini CLI respects .geminiignore and .gitignore. */
17
+ isExcludedForGemini(relativePath: string): boolean;
18
+ /** Aider respects .aiderignore and typically .gitignore. */
19
+ isExcludedForAider(relativePath: string): boolean;
12
20
  }
13
21
  export declare function parseIgnoreFile(content: string): string[];
14
22
  /**
@@ -25,4 +33,6 @@ export declare function pathMatchesAny(relativePath: string, patterns: string[])
25
33
  export declare function createIgnoreIndex(options: {
26
34
  gitignorePatterns: string[];
27
35
  cursorignorePatterns: string[];
36
+ geminiignorePatterns?: string[];
37
+ aiderignorePatterns?: string[];
28
38
  }): IgnoreIndex;
@@ -50,7 +50,6 @@ export function matchIgnorePattern(relativePath, pattern) {
50
50
  if (pat.endsWith("/")) {
51
51
  const dir = pat.slice(0, -1);
52
52
  if (!dir.includes("/")) {
53
- // `build/` matches build at any directory level (gitignore semantics)
54
53
  matched =
55
54
  pathNorm === dir ||
56
55
  pathNorm.endsWith(`/${dir}`) ||
@@ -114,15 +113,25 @@ const CURSOR_DEFAULT_ENV_PATTERNS = [".env", ".env.*", "**/.env", "**/.env.*"];
114
113
  export function createIgnoreIndex(options) {
115
114
  const gitignorePatterns = options.gitignorePatterns;
116
115
  const cursorignorePatterns = options.cursorignorePatterns;
116
+ const geminiignorePatterns = options.geminiignorePatterns ?? [];
117
+ const aiderignorePatterns = options.aiderignorePatterns ?? [];
117
118
  return {
118
119
  gitignorePatterns,
119
120
  cursorignorePatterns,
121
+ geminiignorePatterns,
122
+ aiderignorePatterns,
120
123
  matchesGitignore(relativePath) {
121
124
  return pathMatchesAny(relativePath, gitignorePatterns);
122
125
  },
123
126
  matchesCursorignore(relativePath) {
124
127
  return pathMatchesAny(relativePath, cursorignorePatterns);
125
128
  },
129
+ matchesGeminiignore(relativePath) {
130
+ return pathMatchesAny(relativePath, geminiignorePatterns);
131
+ },
132
+ matchesAiderignore(relativePath) {
133
+ return pathMatchesAny(relativePath, aiderignorePatterns);
134
+ },
126
135
  isExcludedForCursor(relativePath) {
127
136
  if (pathMatchesAny(relativePath, cursorignorePatterns)) {
128
137
  return true;
@@ -132,5 +141,17 @@ export function createIgnoreIndex(options) {
132
141
  }
133
142
  return pathMatchesAny(relativePath, CURSOR_DEFAULT_ENV_PATTERNS);
134
143
  },
144
+ isExcludedForGemini(relativePath) {
145
+ if (pathMatchesAny(relativePath, geminiignorePatterns)) {
146
+ return true;
147
+ }
148
+ return pathMatchesAny(relativePath, gitignorePatterns);
149
+ },
150
+ isExcludedForAider(relativePath) {
151
+ if (pathMatchesAny(relativePath, aiderignorePatterns)) {
152
+ return true;
153
+ }
154
+ return pathMatchesAny(relativePath, gitignorePatterns);
155
+ },
135
156
  };
136
157
  }
@@ -6,6 +6,13 @@ const EMPTYABLE_KINDS = new Set([
6
6
  "claude-md",
7
7
  "claude-local-md",
8
8
  "claude-rule-md",
9
+ "copilot-instructions",
10
+ "copilot-path-instructions",
11
+ "windsurf-rule-md",
12
+ "windsurf-legacy-rules",
13
+ "gemini-md",
14
+ "aider-conventions",
15
+ "aider-conf",
9
16
  ]);
10
17
  export const emptyInstructionsRule = {
11
18
  id: "instructions/empty-instructions",
@@ -60,6 +60,27 @@ function agentsWithoutClearExclusion(context, relativePath) {
60
60
  }
61
61
  if (agent.id === "codex") {
62
62
  affected.push("codex");
63
+ continue;
64
+ }
65
+ if (agent.id === "copilot") {
66
+ // No official project-local Copilot deny/ignore file; treat as exposed when present.
67
+ affected.push("copilot");
68
+ continue;
69
+ }
70
+ if (agent.id === "windsurf") {
71
+ affected.push("windsurf");
72
+ continue;
73
+ }
74
+ if (agent.id === "gemini-cli") {
75
+ if (!context.ignore.isExcludedForGemini(relativePath)) {
76
+ affected.push("gemini-cli");
77
+ }
78
+ continue;
79
+ }
80
+ if (agent.id === "aider") {
81
+ if (!context.ignore.isExcludedForAider(relativePath)) {
82
+ affected.push("aider");
83
+ }
63
84
  }
64
85
  }
65
86
  return affected;
@@ -95,7 +116,7 @@ export const envFileExposureRule = {
95
116
  severity: kind === "backup" ? "warning" : "critical",
96
117
  title: "Sensitive environment file present in repository",
97
118
  message: `Sensitive environment file present in the repository: ${file.relativePath}`,
98
- whyItMatters: "No supported coding-agent configuration was detected, so agent-specific exposure was not asserted. The file is still high-risk repository material if tracked or shared.",
119
+ whyItMatters: "No supported coding-agent configuration was detected, so agent-specific exposure was not asserted. Repository hygiene still applies: the file is high-risk if tracked or shared.",
99
120
  recommendation: "Keep runtime environment files out of version control and rotate credentials if the file may have been shared.",
100
121
  affectedAgents: [],
101
122
  evidence: {
@@ -127,7 +148,7 @@ export const envFileExposureRule = {
127
148
  title: "Sensitive environment file may enter agent context",
128
149
  message: `Sensitive environment file detected and no relevant exclusion was found for: ${file.relativePath}`,
129
150
  whyItMatters: "Environment files frequently hold API keys and credentials. If readable by an AI coding agent, those values may be included in prompts or logs.",
130
- recommendation: "Add an agent-specific exclusion (for example .cursorignore, a Claude Code Read deny rule, or a Codex filesystem deny), keep the file out of version control, and rotate any credentials that may have been exposed.",
151
+ recommendation: "Add an agent-specific exclusion (for example .cursorignore, a Claude Code Read deny rule, a Codex filesystem deny, .geminiignore, or .aiderignore), keep the file out of version control, and rotate any credentials that may have been exposed.",
131
152
  affectedAgents: affected,
132
153
  evidence: {
133
154
  path: file.relativePath,
@@ -58,7 +58,7 @@ export const privateKeyFileRule = {
58
58
  message: `Possible ${label} detected at ${file.relativePath}`,
59
59
  whyItMatters: agentsPresent
60
60
  ? "Credential material in the working tree can be read by AI coding agents and may leak into model context or logs. Filename heuristics only — contents were not inspected."
61
- : "Credential material in the working tree is high-risk repository content. No supported coding-agent configuration was detected, so agent-specific exposure was not asserted. Filename heuristics only — contents were not inspected.",
61
+ : "Credential material in the working tree is high-risk repository content. No supported coding-agent configuration was detected, so agent-specific exposure was not asserted. Repository hygiene still applies. Filename heuristics only — contents were not inspected.",
62
62
  recommendation: "Remove the file from the repository, rotate the credential, add ignore rules, and never commit replacements.",
63
63
  affectedAgents: affected,
64
64
  evidence: { path: file.relativePath, detail: label },
@@ -49,7 +49,7 @@ export async function scan(options = {}) {
49
49
  const agentsPresent = agents.some((a) => a.detected || a.configured);
50
50
  const agentSecurityAnalysis = agentsPresent ? "full" : "limited";
51
51
  if (agentSecurityAnalysis === "limited") {
52
- warnings.push("No supported coding-agent configuration detected; agent-specific security exposure checks are limited.");
52
+ warnings.push("Agent-specific exposure checks are limited (no supported agent config); repository hygiene still applied.");
53
53
  }
54
54
  const scoringStarted = performance.now();
55
55
  const scores = computeReadinessScores(findings);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Minimal JSON Schema subset validator for AgentDoctor v2 report schemas.
3
+ * Supports: type, required, properties, items, enum, integer/string/boolean/object/array/null unions.
4
+ * Not a full draft-2020 implementation — sufficient for shipped schemas without new deps.
5
+ */
6
+ export declare function validateAgainstSchema(schema: Record<string, unknown>, value: unknown, path?: string): string[];
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Minimal JSON Schema subset validator for AgentDoctor v2 report schemas.
3
+ * Supports: type, required, properties, items, enum, integer/string/boolean/object/array/null unions.
4
+ * Not a full draft-2020 implementation — sufficient for shipped schemas without new deps.
5
+ */
6
+ export function validateAgainstSchema(schema, value, path = "$") {
7
+ const errors = [];
8
+ const type = schema.type;
9
+ if (type !== undefined) {
10
+ const types = Array.isArray(type) ? type : [type];
11
+ if (!types.some((t) => matchesType(t, value))) {
12
+ errors.push(`${path}: expected type ${types.join("|")}`);
13
+ return errors;
14
+ }
15
+ }
16
+ if (value === null || value === undefined) {
17
+ return errors;
18
+ }
19
+ if (schema.enum && Array.isArray(schema.enum) && !schema.enum.includes(value)) {
20
+ errors.push(`${path}: value not in enum`);
21
+ }
22
+ if (typeof value === "object" && !Array.isArray(value) && schema.properties) {
23
+ const obj = value;
24
+ const props = schema.properties;
25
+ const required = Array.isArray(schema.required) ? schema.required : [];
26
+ for (const key of required) {
27
+ if (!(key in obj))
28
+ errors.push(`${path}.${key}: required`);
29
+ }
30
+ for (const [key, childSchema] of Object.entries(props)) {
31
+ if (key in obj) {
32
+ errors.push(...validateAgainstSchema(childSchema, obj[key], `${path}.${key}`));
33
+ }
34
+ }
35
+ }
36
+ if (Array.isArray(value) && schema.items && typeof schema.items === "object") {
37
+ for (let i = 0; i < value.length; i += 1) {
38
+ errors.push(...validateAgainstSchema(schema.items, value[i], `${path}[${i}]`));
39
+ }
40
+ }
41
+ return errors;
42
+ }
43
+ function matchesType(type, value) {
44
+ switch (type) {
45
+ case "string":
46
+ return typeof value === "string";
47
+ case "integer":
48
+ return typeof value === "number" && Number.isInteger(value);
49
+ case "number":
50
+ return typeof value === "number" && Number.isFinite(value);
51
+ case "boolean":
52
+ return typeof value === "boolean";
53
+ case "object":
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55
+ case "array":
56
+ return Array.isArray(value);
57
+ case "null":
58
+ return value === null;
59
+ default:
60
+ return true;
61
+ }
62
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Finding, Scores } from "../../types/index.js";
2
2
  /**
3
3
  * v1 readiness scores from post-dedupe findings.
4
- * Spec: docs/scoring.md
4
+ * Spec: docs/reference/scoring.md
5
5
  */
6
6
  export declare function computeReadinessScores(findings: readonly Finding[]): Scores;
@@ -10,7 +10,7 @@ const SEVERITY_SORT_RANK = {
10
10
  };
11
11
  /**
12
12
  * v1 readiness scores from post-dedupe findings.
13
- * Spec: docs/scoring.md
13
+ * Spec: docs/reference/scoring.md
14
14
  */
15
15
  export function computeReadinessScores(findings) {
16
16
  const overallRaw = 100 - sumDeductions(findings);
@@ -29,6 +29,10 @@ export function computeReadinessScores(findings) {
29
29
  cursor: scoreSubset(findings.filter((f) => f.affectedAgents.includes("cursor"))),
30
30
  "claude-code": scoreSubset(findings.filter((f) => f.affectedAgents.includes("claude-code"))),
31
31
  codex: scoreSubset(findings.filter((f) => f.affectedAgents.includes("codex"))),
32
+ copilot: scoreSubset(findings.filter((f) => f.affectedAgents.includes("copilot"))),
33
+ windsurf: scoreSubset(findings.filter((f) => f.affectedAgents.includes("windsurf"))),
34
+ "gemini-cli": scoreSubset(findings.filter((f) => f.affectedAgents.includes("gemini-cli"))),
35
+ aider: scoreSubset(findings.filter((f) => f.affectedAgents.includes("aider"))),
32
36
  },
33
37
  };
34
38
  }
@@ -2,6 +2,6 @@ import type { Scores } from "../../types/index.js";
2
2
  /**
3
3
  * Historical filesScanned-based stub. Kept for unit tests that document the
4
4
  * pre-v1 placeholder behavior. Production scans use computeReadinessScores
5
- * (docs/scoring.md) — do not call this from scan().
5
+ * (docs/reference/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
2
  * Historical filesScanned-based stub. Kept for unit tests that document the
3
3
  * pre-v1 placeholder behavior. Production scans use computeReadinessScores
4
- * (docs/scoring.md) — do not call this from scan().
4
+ * (docs/reference/scoring.md) — do not call this from scan().
5
5
  */
6
6
  export function computePlaceholderScores(filesScanned) {
7
7
  const base = 72;
@@ -21,6 +21,10 @@ export function computePlaceholderScores(filesScanned) {
21
21
  cursor: clamp(overall - 5),
22
22
  "claude-code": clamp(overall - 8),
23
23
  codex: clamp(overall - 6),
24
+ copilot: clamp(overall - 7),
25
+ windsurf: clamp(overall - 6),
26
+ "gemini-cli": clamp(overall - 5),
27
+ aider: clamp(overall - 7),
24
28
  },
25
29
  };
26
30
  }
@@ -0,0 +1,23 @@
1
+ export interface SecretFinding {
2
+ file: string;
3
+ line: number;
4
+ ruleId: string;
5
+ /** Always redacted — never contains the secret value */
6
+ redactedSnippet: string;
7
+ severity: "critical" | "warning";
8
+ }
9
+ export interface SecretScanReport {
10
+ root: string;
11
+ enabled: boolean;
12
+ filesScanned: number;
13
+ findings: SecretFinding[];
14
+ limitations: string[];
15
+ }
16
+ /**
17
+ * Opt-in content secret scan. Findings never include raw secret values.
18
+ */
19
+ export declare function scanSecrets(options: {
20
+ root: string;
21
+ enabled: boolean;
22
+ maxFiles?: number;
23
+ }): Promise<SecretScanReport>;