@praneeth_54/agentdoctor 1.1.1 → 2.0.1

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 (224) hide show
  1. package/CHANGELOG.md +164 -7
  2. package/README.md +263 -513
  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/architecture/contract.d.ts +64 -0
  24. package/dist/architecture/contract.js +373 -0
  25. package/dist/assurance/change.d.ts +211 -0
  26. package/dist/assurance/change.js +624 -0
  27. package/dist/assurance/proof.d.ts +136 -0
  28. package/dist/assurance/proof.js +338 -0
  29. package/dist/auth/index.d.ts +81 -0
  30. package/dist/auth/index.js +130 -0
  31. package/dist/auth/rbac.d.ts +9 -0
  32. package/dist/auth/rbac.js +26 -0
  33. package/dist/cli/commands/architecture.d.ts +6 -0
  34. package/dist/cli/commands/architecture.js +109 -0
  35. package/dist/cli/commands/assurance.d.ts +67 -0
  36. package/dist/cli/commands/assurance.js +210 -0
  37. package/dist/cli/commands/brain.d.ts +12 -0
  38. package/dist/cli/commands/brain.js +162 -0
  39. package/dist/cli/commands/complete.d.ts +23 -0
  40. package/dist/cli/commands/complete.js +232 -0
  41. package/dist/cli/commands/explain.js +1 -1
  42. package/dist/cli/commands/fix.js +22 -2
  43. package/dist/cli/commands/mcp.d.ts +8 -0
  44. package/dist/cli/commands/mcp.js +14 -0
  45. package/dist/cli/commands/platform.d.ts +17 -0
  46. package/dist/cli/commands/platform.js +195 -0
  47. package/dist/cli/commands/policy-graph-run.d.ts +40 -0
  48. package/dist/cli/commands/policy-graph-run.js +200 -0
  49. package/dist/cli/commands/v2.d.ts +55 -0
  50. package/dist/cli/commands/v2.js +278 -0
  51. package/dist/cli/commands/workspace.d.ts +9 -0
  52. package/dist/cli/commands/workspace.js +82 -0
  53. package/dist/cli/program.js +1328 -3
  54. package/dist/constants.d.ts +3 -2
  55. package/dist/constants.js +5 -1
  56. package/dist/contracts/adapters.d.ts +7 -0
  57. package/dist/contracts/adapters.js +60 -0
  58. package/dist/contracts/index.d.ts +162 -0
  59. package/dist/contracts/index.js +18 -0
  60. package/dist/core/baseline/store.d.ts +48 -0
  61. package/dist/core/baseline/store.js +163 -0
  62. package/dist/core/brain-cli/service.d.ts +26 -0
  63. package/dist/core/brain-cli/service.js +175 -0
  64. package/dist/core/brain-product/init.d.ts +45 -0
  65. package/dist/core/brain-product/init.js +235 -0
  66. package/dist/core/changes/analyze.d.ts +41 -0
  67. package/dist/core/changes/analyze.js +223 -0
  68. package/dist/core/context-health/analyze.d.ts +15 -0
  69. package/dist/core/context-health/analyze.js +234 -0
  70. package/dist/core/fix/apply.d.ts +21 -4
  71. package/dist/core/fix/apply.js +118 -18
  72. package/dist/core/fix/backup.d.ts +30 -0
  73. package/dist/core/fix/backup.js +193 -0
  74. package/dist/core/fix/plan.d.ts +6 -3
  75. package/dist/core/fix/plan.js +113 -9
  76. package/dist/core/fix/render.d.ts +2 -0
  77. package/dist/core/fix/render.js +36 -21
  78. package/dist/core/fix/run.js +5 -1
  79. package/dist/core/fix/safe-target.d.ts +14 -0
  80. package/dist/core/fix/safe-target.js +81 -0
  81. package/dist/core/fix/types.d.ts +12 -1
  82. package/dist/core/fix/types.js +2 -0
  83. package/dist/core/fix/writers/claude-settings.js +3 -1
  84. package/dist/core/fix/writers/codex-config.d.ts +3 -0
  85. package/dist/core/fix/writers/codex-config.js +24 -3
  86. package/dist/core/fix/writers/cursorignore.js +3 -1
  87. package/dist/core/fix/writers/simple-ignore.d.ts +16 -0
  88. package/dist/core/fix/writers/simple-ignore.js +68 -0
  89. package/dist/core/monorepo/detect.d.ts +13 -0
  90. package/dist/core/monorepo/detect.js +121 -0
  91. package/dist/core/rules/build-context.js +22 -3
  92. package/dist/core/rules/context/generated-directory.js +11 -1
  93. package/dist/core/rules/context/large-instruction-file.js +6 -0
  94. package/dist/core/rules/context/large-log-file.js +16 -1
  95. package/dist/core/rules/ignore.d.ts +11 -1
  96. package/dist/core/rules/ignore.js +22 -1
  97. package/dist/core/rules/instructions/empty-instructions.js +7 -0
  98. package/dist/core/rules/security/env-file-exposure.js +23 -2
  99. package/dist/core/rules/security/private-key-file.js +1 -1
  100. package/dist/core/scanner/scan.js +1 -1
  101. package/dist/core/schemas/validate.d.ts +6 -0
  102. package/dist/core/schemas/validate.js +62 -0
  103. package/dist/core/scoring/compute-scores.d.ts +1 -1
  104. package/dist/core/scoring/compute-scores.js +5 -1
  105. package/dist/core/scoring/placeholder.d.ts +1 -1
  106. package/dist/core/scoring/placeholder.js +5 -1
  107. package/dist/core/secrets/scan.d.ts +29 -0
  108. package/dist/core/secrets/scan.js +172 -0
  109. package/dist/coverage/cobertura.d.ts +6 -0
  110. package/dist/coverage/cobertura.js +52 -0
  111. package/dist/coverage/istanbul.d.ts +7 -0
  112. package/dist/coverage/istanbul.js +134 -0
  113. package/dist/coverage/lcov.d.ts +7 -0
  114. package/dist/coverage/lcov.js +59 -0
  115. package/dist/coverage/load.d.ts +9 -0
  116. package/dist/coverage/load.js +39 -0
  117. package/dist/coverage/types.d.ts +23 -0
  118. package/dist/coverage/types.js +5 -0
  119. package/dist/dashboard/server.d.ts +24 -0
  120. package/dist/dashboard/server.js +402 -0
  121. package/dist/enforcement/runner.d.ts +67 -0
  122. package/dist/enforcement/runner.js +460 -0
  123. package/dist/index.d.ts +34 -0
  124. package/dist/index.js +25 -0
  125. package/dist/integrations/github/pr-analyze.d.ts +28 -0
  126. package/dist/integrations/github/pr-analyze.js +98 -0
  127. package/dist/integrations/local-ai/provider.d.ts +45 -0
  128. package/dist/integrations/local-ai/provider.js +104 -0
  129. package/dist/intelligence/git/analyze.d.ts +34 -0
  130. package/dist/intelligence/git/analyze.js +136 -0
  131. package/dist/intelligence/graph/build.d.ts +15 -0
  132. package/dist/intelligence/graph/build.js +259 -0
  133. package/dist/intelligence/graph/incremental.d.ts +68 -0
  134. package/dist/intelligence/graph/incremental.js +234 -0
  135. package/dist/intelligence/resolve/imports.d.ts +36 -0
  136. package/dist/intelligence/resolve/imports.js +245 -0
  137. package/dist/knowledge/store.d.ts +25 -0
  138. package/dist/knowledge/store.js +91 -0
  139. package/dist/languages/go.d.ts +9 -0
  140. package/dist/languages/go.js +45 -0
  141. package/dist/languages/index.d.ts +11 -0
  142. package/dist/languages/index.js +76 -0
  143. package/dist/languages/php.d.ts +7 -0
  144. package/dist/languages/php.js +193 -0
  145. package/dist/languages/python.d.ts +7 -0
  146. package/dist/languages/python.js +150 -0
  147. package/dist/languages/types.d.ts +42 -0
  148. package/dist/languages/types.js +26 -0
  149. package/dist/languages/typescript.d.ts +3 -0
  150. package/dist/languages/typescript.js +95 -0
  151. package/dist/mcp/agentdoctor/server.d.ts +16 -0
  152. package/dist/mcp/agentdoctor/server.js +67 -0
  153. package/dist/mcp/brain/session.js +7 -7
  154. package/dist/mcp/intelligence/handlers.d.ts +16 -0
  155. package/dist/mcp/intelligence/handlers.js +364 -0
  156. package/dist/mcp/intelligence/path-safety.d.ts +9 -0
  157. package/dist/mcp/intelligence/path-safety.js +63 -0
  158. package/dist/mcp/intelligence/registry.d.ts +8 -0
  159. package/dist/mcp/intelligence/registry.js +228 -0
  160. package/dist/ops/health.d.ts +18 -0
  161. package/dist/ops/health.js +51 -0
  162. package/dist/platform/ai-quality/analyze.d.ts +9 -0
  163. package/dist/platform/ai-quality/analyze.js +65 -0
  164. package/dist/platform/architecture/drift.d.ts +17 -0
  165. package/dist/platform/architecture/drift.js +74 -0
  166. package/dist/platform/auth/local.d.ts +15 -0
  167. package/dist/platform/auth/local.js +58 -0
  168. package/dist/platform/context-security/analyze.d.ts +6 -0
  169. package/dist/platform/context-security/analyze.js +97 -0
  170. package/dist/platform/firewall/evaluate.d.ts +57 -0
  171. package/dist/platform/firewall/evaluate.js +359 -0
  172. package/dist/platform/graph/build.d.ts +6 -0
  173. package/dist/platform/graph/build.js +189 -0
  174. package/dist/platform/health/analyze.d.ts +7 -0
  175. package/dist/platform/health/analyze.js +192 -0
  176. package/dist/platform/index.d.ts +27 -0
  177. package/dist/platform/index.js +89 -0
  178. package/dist/platform/knowledge/analyze.d.ts +20 -0
  179. package/dist/platform/knowledge/analyze.js +96 -0
  180. package/dist/platform/provenance/build.d.ts +44 -0
  181. package/dist/platform/provenance/build.js +65 -0
  182. package/dist/platform/readiness/scorecard.d.ts +5 -0
  183. package/dist/platform/readiness/scorecard.js +98 -0
  184. package/dist/platform/refactor/impact.d.ts +20 -0
  185. package/dist/platform/refactor/impact.js +60 -0
  186. package/dist/platform/reports/export.d.ts +12 -0
  187. package/dist/platform/reports/export.js +92 -0
  188. package/dist/platform/security/redact.d.ts +15 -0
  189. package/dist/platform/security/redact.js +87 -0
  190. package/dist/platform/sessions/store.d.ts +32 -0
  191. package/dist/platform/sessions/store.js +97 -0
  192. package/dist/platform/store.d.ts +6 -0
  193. package/dist/platform/store.js +57 -0
  194. package/dist/platform/test-impact/analyze.d.ts +50 -0
  195. package/dist/platform/test-impact/analyze.js +350 -0
  196. package/dist/platform/time-machine/compare.d.ts +17 -0
  197. package/dist/platform/time-machine/compare.js +62 -0
  198. package/dist/platform/tokens/plan.d.ts +28 -0
  199. package/dist/platform/tokens/plan.js +108 -0
  200. package/dist/platform/types.d.ts +120 -0
  201. package/dist/platform/types.js +5 -0
  202. package/dist/plugins/runtime.d.ts +36 -0
  203. package/dist/plugins/runtime.js +101 -0
  204. package/dist/plugins/sdk.d.ts +29 -0
  205. package/dist/plugins/sdk.js +114 -0
  206. package/dist/policy/compose.d.ts +34 -0
  207. package/dist/policy/compose.js +118 -0
  208. package/dist/policy/packs.d.ts +9 -0
  209. package/dist/policy/packs.js +91 -0
  210. package/dist/reporters/terminal/report.js +38 -5
  211. package/dist/security/paths.d.ts +21 -0
  212. package/dist/security/paths.js +105 -0
  213. package/dist/storage/postgres.d.ts +26 -0
  214. package/dist/storage/postgres.js +90 -0
  215. package/dist/storage/provider.d.ts +30 -0
  216. package/dist/storage/provider.js +109 -0
  217. package/dist/storage/sqlite.d.ts +33 -0
  218. package/dist/storage/sqlite.js +77 -0
  219. package/dist/team/auth.d.ts +35 -0
  220. package/dist/team/auth.js +70 -0
  221. package/dist/types/index.d.ts +5 -1
  222. package/dist/workspace/index.d.ts +69 -0
  223. package/dist/workspace/index.js +220 -0
  224. package/package.json +15 -5
@@ -0,0 +1,36 @@
1
+ export interface PluginAnalyzerNote {
2
+ pluginId: string;
3
+ severity: "info" | "warning";
4
+ message: string;
5
+ }
6
+ export interface PluginAnalyzerResult {
7
+ pluginId: string;
8
+ ok: boolean;
9
+ notes: PluginAnalyzerNote[];
10
+ error?: string;
11
+ }
12
+ export interface PluginAnalyzerContext {
13
+ repoRoot: string;
14
+ /** Relative paths the host already knows about — plugins must not scan arbitrary FS */
15
+ knownRelativePaths: readonly string[];
16
+ }
17
+ export type PluginAnalyzeFn = (ctx: PluginAnalyzerContext) => Promise<{
18
+ notes?: Array<{
19
+ severity?: string;
20
+ message: string;
21
+ }>;
22
+ }> | {
23
+ notes?: Array<{
24
+ severity?: string;
25
+ message: string;
26
+ }>;
27
+ };
28
+ /**
29
+ * Run analyzer-capability plugins with per-plugin error isolation and timeouts.
30
+ * Declarative JSON entry (`hooks.json`) or JS/MJS `analyze` export.
31
+ * No network APIs are provided; crashes do not abort other plugins.
32
+ */
33
+ export declare function runPluginAnalyzers(rootInput: string, options?: {
34
+ knownRelativePaths?: string[];
35
+ timeoutMs?: number;
36
+ }): Promise<PluginAnalyzerResult[]>;
@@ -0,0 +1,101 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { discoverPlugins } from "./sdk.js";
5
+ import { isPathInsideRoot, resolveRepoRoot } from "../utils/path.js";
6
+ /**
7
+ * Run analyzer-capability plugins with per-plugin error isolation and timeouts.
8
+ * Declarative JSON entry (`hooks.json`) or JS/MJS `analyze` export.
9
+ * No network APIs are provided; crashes do not abort other plugins.
10
+ */
11
+ export async function runPluginAnalyzers(rootInput, options) {
12
+ const repoRoot = resolveRepoRoot(rootInput);
13
+ const plugins = await discoverPlugins(repoRoot);
14
+ const known = options?.knownRelativePaths ?? [];
15
+ const timeoutMs = options?.timeoutMs ?? 3_000;
16
+ const results = [];
17
+ for (const plugin of plugins) {
18
+ if (!plugin.ok) {
19
+ results.push({
20
+ pluginId: plugin.manifest.id,
21
+ ok: false,
22
+ notes: [],
23
+ error: plugin.errors.join("; ") || "invalid plugin",
24
+ });
25
+ continue;
26
+ }
27
+ if (!plugin.manifest.capabilities.includes("analyzer")) {
28
+ continue;
29
+ }
30
+ try {
31
+ results.push(await runOneAnalyzer(repoRoot, plugin.manifest, plugin.root, known, timeoutMs));
32
+ }
33
+ catch (error) {
34
+ results.push({
35
+ pluginId: plugin.manifest.id,
36
+ ok: false,
37
+ notes: [],
38
+ error: error instanceof Error ? error.message : String(error),
39
+ });
40
+ }
41
+ }
42
+ return results.sort((a, b) => a.pluginId.localeCompare(b.pluginId));
43
+ }
44
+ async function runOneAnalyzer(repoRoot, manifest, pluginRoot, knownRelativePaths, timeoutMs) {
45
+ const entry = manifest.entry ?? "hooks.json";
46
+ const entryPath = path.resolve(pluginRoot, entry);
47
+ if (!isPathInsideRoot(pluginRoot, entryPath)) {
48
+ return {
49
+ pluginId: manifest.id,
50
+ ok: false,
51
+ notes: [],
52
+ error: "plugin entry escapes plugin root",
53
+ };
54
+ }
55
+ const ctx = {
56
+ repoRoot,
57
+ knownRelativePaths,
58
+ };
59
+ if (entry.endsWith(".json")) {
60
+ const raw = JSON.parse(await fs.readFile(entryPath, "utf8"));
61
+ return normalizeNotes(manifest, raw.notes ?? []);
62
+ }
63
+ const mod = await withTimeout(import(pathToFileURL(entryPath).href), timeoutMs);
64
+ const analyze = mod.analyze ??
65
+ mod.default;
66
+ if (typeof analyze !== "function") {
67
+ return {
68
+ pluginId: manifest.id,
69
+ ok: false,
70
+ notes: [],
71
+ error: "plugin entry missing analyze() export",
72
+ };
73
+ }
74
+ const out = await withTimeout(Promise.resolve(analyze(ctx)), timeoutMs);
75
+ return normalizeNotes(manifest, out?.notes ?? []);
76
+ }
77
+ function normalizeNotes(manifest, notes) {
78
+ return {
79
+ pluginId: manifest.id,
80
+ ok: true,
81
+ notes: notes
82
+ .filter((n) => typeof n.message === "string" && n.message.trim().length > 0)
83
+ .map((n) => ({
84
+ pluginId: manifest.id,
85
+ severity: n.severity === "warning" ? "warning" : "info",
86
+ message: n.message.slice(0, 500),
87
+ })),
88
+ };
89
+ }
90
+ function withTimeout(promise, ms) {
91
+ return new Promise((resolve, reject) => {
92
+ const timer = setTimeout(() => reject(new Error(`plugin timed out after ${ms}ms`)), ms);
93
+ promise.then((value) => {
94
+ clearTimeout(timer);
95
+ resolve(value);
96
+ }, (error) => {
97
+ clearTimeout(timer);
98
+ reject(error);
99
+ });
100
+ });
101
+ }
@@ -0,0 +1,29 @@
1
+ export type PluginCapability = "rule" | "reporter" | "analyzer";
2
+ export interface AgentDoctorPluginManifest {
3
+ id: string;
4
+ name: string;
5
+ version: string;
6
+ apiVersion: "2.0";
7
+ capabilities: PluginCapability[];
8
+ /** Declared only — runtime does not grant network/fs beyond the sandbox root. */
9
+ requestedPermissions?: Array<"read-repo" | "none">;
10
+ entry?: string;
11
+ }
12
+ export interface PluginLoadResult {
13
+ manifest: AgentDoctorPluginManifest;
14
+ root: string;
15
+ ok: boolean;
16
+ errors: string[];
17
+ }
18
+ export interface PluginSandbox {
19
+ repoRoot: string;
20
+ pluginRoot: string;
21
+ }
22
+ export declare function validatePluginManifest(raw: unknown): {
23
+ ok: boolean;
24
+ manifest?: AgentDoctorPluginManifest;
25
+ errors: string[];
26
+ };
27
+ /** Discover plugins under .agentdoctor/plugins/<id>/plugin.json (validated, no network). */
28
+ export declare function discoverPlugins(rootInput: string): Promise<PluginLoadResult[]>;
29
+ export declare function createPluginSandbox(repoRoot: string, pluginRoot: string): PluginSandbox;
@@ -0,0 +1,114 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolveRepoRoot, isPathInsideRoot } from "../utils/path.js";
4
+ const ALLOWED_CAPABILITIES = new Set(["rule", "reporter", "analyzer"]);
5
+ export function validatePluginManifest(raw) {
6
+ const errors = [];
7
+ if (!raw || typeof raw !== "object") {
8
+ return { ok: false, errors: ["manifest must be an object"] };
9
+ }
10
+ const m = raw;
11
+ if (typeof m.id !== "string" || !m.id.trim())
12
+ errors.push("id required");
13
+ if (typeof m.name !== "string" || !m.name.trim())
14
+ errors.push("name required");
15
+ if (typeof m.version !== "string" || !m.version.trim())
16
+ errors.push("version required");
17
+ if (m.apiVersion !== "2.0")
18
+ errors.push('apiVersion must be "2.0"');
19
+ if (!Array.isArray(m.capabilities) || m.capabilities.length === 0) {
20
+ errors.push("capabilities required");
21
+ }
22
+ else {
23
+ for (const c of m.capabilities) {
24
+ if (!ALLOWED_CAPABILITIES.has(c)) {
25
+ errors.push(`unsupported capability: ${String(c)}`);
26
+ }
27
+ }
28
+ }
29
+ if (errors.length > 0)
30
+ return { ok: false, errors };
31
+ const manifest = {
32
+ id: String(m.id),
33
+ name: String(m.name),
34
+ version: String(m.version),
35
+ apiVersion: "2.0",
36
+ capabilities: m.capabilities,
37
+ };
38
+ if (Array.isArray(m.requestedPermissions)) {
39
+ manifest.requestedPermissions = m.requestedPermissions;
40
+ }
41
+ if (typeof m.entry === "string") {
42
+ manifest.entry = m.entry;
43
+ }
44
+ return {
45
+ ok: true,
46
+ errors: [],
47
+ manifest,
48
+ };
49
+ }
50
+ /** Discover plugins under .agentdoctor/plugins/<id>/plugin.json (validated, no network). */
51
+ export async function discoverPlugins(rootInput) {
52
+ const root = resolveRepoRoot(rootInput);
53
+ const pluginsRoot = path.join(root, ".agentdoctor", "plugins");
54
+ let dirs = [];
55
+ try {
56
+ const entries = await fs.readdir(pluginsRoot, { withFileTypes: true });
57
+ dirs = entries.filter((e) => e.isDirectory()).map((e) => path.join(pluginsRoot, e.name));
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ const results = [];
63
+ for (const dir of dirs) {
64
+ if (!isPathInsideRoot(pluginsRoot, dir))
65
+ continue;
66
+ const manifestPath = path.join(dir, "plugin.json");
67
+ try {
68
+ const raw = JSON.parse(await fs.readFile(manifestPath, "utf8"));
69
+ const validated = validatePluginManifest(raw);
70
+ if (!validated.ok || !validated.manifest) {
71
+ results.push({
72
+ manifest: {
73
+ id: path.basename(dir),
74
+ name: path.basename(dir),
75
+ version: "0.0.0",
76
+ apiVersion: "2.0",
77
+ capabilities: ["analyzer"],
78
+ },
79
+ root: dir,
80
+ ok: false,
81
+ errors: validated.errors,
82
+ });
83
+ continue;
84
+ }
85
+ results.push({
86
+ manifest: validated.manifest,
87
+ root: dir,
88
+ ok: true,
89
+ errors: [],
90
+ });
91
+ }
92
+ catch (error) {
93
+ results.push({
94
+ manifest: {
95
+ id: path.basename(dir),
96
+ name: path.basename(dir),
97
+ version: "0.0.0",
98
+ apiVersion: "2.0",
99
+ capabilities: ["analyzer"],
100
+ },
101
+ root: dir,
102
+ ok: false,
103
+ errors: [error instanceof Error ? error.message : String(error)],
104
+ });
105
+ }
106
+ }
107
+ return results.sort((a, b) => a.manifest.id.localeCompare(b.manifest.id));
108
+ }
109
+ export function createPluginSandbox(repoRoot, pluginRoot) {
110
+ return {
111
+ repoRoot: resolveRepoRoot(repoRoot),
112
+ pluginRoot: path.resolve(pluginRoot),
113
+ };
114
+ }
@@ -0,0 +1,34 @@
1
+ import type { FirewallPolicyDocument } from "../platform/firewall/evaluate.js";
2
+ export interface ComposedPolicyResult {
3
+ policy: FirewallPolicyDocument;
4
+ sources: Array<"builtin" | "repo">;
5
+ packName: string | null;
6
+ validationError?: string;
7
+ failClosed: boolean;
8
+ }
9
+ /**
10
+ * Merge builtin (global pack) + repo `.agentdoctor/policy.json`.
11
+ * Precedence: repo rules are prepended (evaluated first); repo defaultDecision /
12
+ * failClosed / shellAllowlist override builtin when present.
13
+ * Malformed repo policy → fail-closed deny-all (never silently ignore).
14
+ */
15
+ export declare function composePolicy(options: {
16
+ root: string;
17
+ packName?: string;
18
+ }): Promise<ComposedPolicyResult>;
19
+ export declare function explainComposedDecision(options: {
20
+ command: string;
21
+ decision: string;
22
+ reason: string;
23
+ policyId?: string | null;
24
+ sources: Array<"builtin" | "repo">;
25
+ failClosed: boolean;
26
+ }): {
27
+ command: string;
28
+ decision: string;
29
+ reason: string;
30
+ policyId: string | null;
31
+ sources: Array<"builtin" | "repo">;
32
+ failClosed: boolean;
33
+ meaning: string;
34
+ };
@@ -0,0 +1,118 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getPolicyPack, POLICY_PACKS } from "./packs.js";
4
+ import { resolveRepoRoot } from "../utils/path.js";
5
+ const REPO_POLICY_PATH = ".agentdoctor/policy.json";
6
+ function denyAll(reason) {
7
+ return {
8
+ version: "2.0",
9
+ defaultDecision: "block",
10
+ failClosed: true,
11
+ rules: [
12
+ {
13
+ id: "compose-fail-closed",
14
+ decision: "block",
15
+ reason,
16
+ riskLevel: "critical",
17
+ },
18
+ ],
19
+ };
20
+ }
21
+ function isFirewallPolicyDocument(value) {
22
+ if (!value || typeof value !== "object")
23
+ return false;
24
+ const doc = value;
25
+ if (doc.version !== "2.0")
26
+ return false;
27
+ if (typeof doc.defaultDecision !== "string")
28
+ return false;
29
+ if (!Array.isArray(doc.rules))
30
+ return false;
31
+ return true;
32
+ }
33
+ /**
34
+ * Merge builtin (global pack) + repo `.agentdoctor/policy.json`.
35
+ * Precedence: repo rules are prepended (evaluated first); repo defaultDecision /
36
+ * failClosed / shellAllowlist override builtin when present.
37
+ * Malformed repo policy → fail-closed deny-all (never silently ignore).
38
+ */
39
+ export async function composePolicy(options) {
40
+ const root = resolveRepoRoot(options.root);
41
+ const packName = options.packName ?? "baseline-safe";
42
+ const builtin = getPolicyPack(packName) ?? POLICY_PACKS["baseline-safe"];
43
+ const sources = ["builtin"];
44
+ const repoPath = path.join(root, REPO_POLICY_PATH);
45
+ let raw = null;
46
+ try {
47
+ raw = await fs.readFile(repoPath, "utf8");
48
+ }
49
+ catch {
50
+ raw = null;
51
+ }
52
+ if (raw === null) {
53
+ return {
54
+ policy: structuredClone(builtin),
55
+ sources,
56
+ packName,
57
+ failClosed: Boolean(builtin.failClosed),
58
+ };
59
+ }
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(raw);
63
+ }
64
+ catch {
65
+ return {
66
+ policy: denyAll("Malformed repo .agentdoctor/policy.json; fail-closed"),
67
+ sources: ["builtin", "repo"],
68
+ packName,
69
+ validationError: "Malformed JSON",
70
+ failClosed: true,
71
+ };
72
+ }
73
+ if (!isFirewallPolicyDocument(parsed)) {
74
+ return {
75
+ policy: denyAll("Invalid repo .agentdoctor/policy.json shape; fail-closed"),
76
+ sources: ["builtin", "repo"],
77
+ packName,
78
+ validationError: "Invalid policy document",
79
+ failClosed: true,
80
+ };
81
+ }
82
+ sources.push("repo");
83
+ const repoRules = Array.isArray(parsed.rules) ? parsed.rules : [];
84
+ const merged = {
85
+ version: "2.0",
86
+ defaultDecision: parsed.defaultDecision ?? builtin.defaultDecision,
87
+ failClosed: parsed.failClosed === true || builtin.failClosed === true,
88
+ ...(parsed.shellAllowlist
89
+ ? { shellAllowlist: parsed.shellAllowlist }
90
+ : builtin.shellAllowlist
91
+ ? { shellAllowlist: builtin.shellAllowlist }
92
+ : {}),
93
+ // Repo rules first (higher precedence), then builtin.
94
+ rules: [...repoRules, ...builtin.rules],
95
+ };
96
+ return {
97
+ policy: merged,
98
+ sources,
99
+ packName,
100
+ failClosed: Boolean(merged.failClosed),
101
+ };
102
+ }
103
+ export function explainComposedDecision(options) {
104
+ const meaning = options.decision === "allow"
105
+ ? "Allowlisted or matched an allow rule — still evaluate-only unless agentdoctor run executes it."
106
+ : options.decision === "block" || options.decision === "deny-network"
107
+ ? "Would be blocked by the AgentDoctor-controlled runner if executed through it."
108
+ : "Requires human approval or further policy before controlled execution.";
109
+ return {
110
+ command: options.command,
111
+ decision: options.decision,
112
+ reason: options.reason,
113
+ policyId: options.policyId ?? null,
114
+ sources: options.sources,
115
+ failClosed: options.failClosed,
116
+ meaning,
117
+ };
118
+ }
@@ -0,0 +1,9 @@
1
+ import type { FirewallPolicyDocument } from "../platform/firewall/evaluate.js";
2
+ /**
3
+ * Named policy packs for local evaluation. Packs are declarative documents —
4
+ * loading a pack does not enable runtime interception unless an AgentDoctor
5
+ * enforcement boundary (controlled runner / CI wrapper) is used.
6
+ */
7
+ export declare const POLICY_PACKS: Record<string, FirewallPolicyDocument>;
8
+ export declare function listPolicyPacks(): string[];
9
+ export declare function getPolicyPack(name: string): FirewallPolicyDocument | null;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Named policy packs for local evaluation. Packs are declarative documents —
3
+ * loading a pack does not enable runtime interception unless an AgentDoctor
4
+ * enforcement boundary (controlled runner / CI wrapper) is used.
5
+ */
6
+ export const POLICY_PACKS = {
7
+ "baseline-safe": {
8
+ version: "2.0",
9
+ defaultDecision: "require-approval",
10
+ failClosed: false,
11
+ shellAllowlist: ["npm test", "npm run test", "vitest", "eslint", "tsc", "prettier"],
12
+ rules: [
13
+ {
14
+ id: "pack-block-secrets",
15
+ actionTypes: ["file-modify", "file-delete", "secret-access"],
16
+ match: { pathContains: [".env", "id_rsa", "credentials", ".pem"] },
17
+ decision: "block",
18
+ reason: "baseline-safe: secret-sensitive path",
19
+ riskLevel: "critical",
20
+ },
21
+ {
22
+ id: "pack-block-destructive",
23
+ actionTypes: ["shell"],
24
+ // Force-push is gated as require-approval (human gate), not a hard block —
25
+ // builtin shell blocks already cover rm -rf; keep drop table here.
26
+ match: { commandContains: ["rm -rf", "drop table"] },
27
+ decision: "block",
28
+ reason: "baseline-safe: destructive operation",
29
+ riskLevel: "critical",
30
+ },
31
+ {
32
+ id: "pack-force-push-approval",
33
+ actionTypes: ["shell"],
34
+ match: { commandContains: ["git push --force", "git push -f"] },
35
+ decision: "require-approval",
36
+ reason: "baseline-safe: force-push requires approval",
37
+ riskLevel: "high",
38
+ },
39
+ {
40
+ id: "pack-allow-safe-dev-paths",
41
+ actionTypes: ["file-modify", "file-create"],
42
+ match: { pathPrefix: ["src/", "tests/", "docs/"] },
43
+ decision: "allow",
44
+ reason: "baseline-safe: allowlisted development paths",
45
+ riskLevel: "low",
46
+ },
47
+ {
48
+ id: "pack-deny-prod-deploy",
49
+ actionTypes: ["deploy", "infra"],
50
+ decision: "block",
51
+ reason: "baseline-safe: production/infra changes require out-of-band process",
52
+ riskLevel: "critical",
53
+ },
54
+ {
55
+ id: "pack-network-default-deny",
56
+ actionTypes: ["network"],
57
+ decision: "deny-network",
58
+ reason: "baseline-safe: network access denied by default",
59
+ riskLevel: "high",
60
+ },
61
+ ],
62
+ },
63
+ "fail-closed-ci": {
64
+ version: "2.0",
65
+ defaultDecision: "block",
66
+ failClosed: true,
67
+ shellAllowlist: ["npm test", "npm run verify", "vitest"],
68
+ rules: [
69
+ {
70
+ id: "ci-deny-network",
71
+ actionTypes: ["network"],
72
+ decision: "block",
73
+ reason: "fail-closed-ci: network blocked",
74
+ riskLevel: "high",
75
+ },
76
+ {
77
+ id: "ci-deny-deploy",
78
+ actionTypes: ["deploy", "infra"],
79
+ decision: "block",
80
+ reason: "fail-closed-ci: deploy blocked",
81
+ riskLevel: "critical",
82
+ },
83
+ ],
84
+ },
85
+ };
86
+ export function listPolicyPacks() {
87
+ return Object.keys(POLICY_PACKS);
88
+ }
89
+ export function getPolicyPack(name) {
90
+ return POLICY_PACKS[name] ?? null;
91
+ }
@@ -7,7 +7,39 @@ import { formatPackageManager, formatPackageManagers } from "../../detectors/pac
7
7
  import { colors, symbolFail, symbolInfo, symbolOk, symbolWarn } from "../../utils/colors.js";
8
8
  import { sanitizeTerminalText } from "../../security/redaction.js";
9
9
  const DEFAULT_FINDING_LIMIT = 12;
10
- const LIMITED_ANALYSIS_NOTE = "No supported coding-agent configuration detected; agent-specific security exposure checks are limited.";
10
+ const LIMITED_ANALYSIS_NOTE = "Agent-specific exposure checks are limited (no supported agent config); repository hygiene still applied.";
11
+ /** Agents with an implemented Safe Fix writer (detect-only agents are excluded). */
12
+ const SAFE_FIX_AGENT_IDS = new Set([
13
+ "cursor",
14
+ "claude-code",
15
+ "codex",
16
+ "gemini-cli",
17
+ "aider",
18
+ ]);
19
+ function renderAgentCoverageSummary(result) {
20
+ const configured = result.agents.filter((a) => a.configured);
21
+ const detectedOnly = result.agents.filter((a) => a.detected && !a.configured);
22
+ const withFix = configured.filter((a) => SAFE_FIX_AGENT_IDS.has(a.id));
23
+ const detectOnlyConfigured = configured.filter((a) => !SAFE_FIX_AGENT_IDS.has(a.id));
24
+ const lines = [];
25
+ lines.push("");
26
+ if (result.agentSecurityAnalysis === "limited") {
27
+ lines.push(colors.dim(" Coverage: 0 configured · universal hygiene still runs (limited agent analysis)"));
28
+ return lines;
29
+ }
30
+ const parts = [`${configured.length} configured`];
31
+ if (detectedOnly.length > 0) {
32
+ parts.push(`${detectedOnly.length} detected-only`);
33
+ }
34
+ if (withFix.length > 0) {
35
+ parts.push(`Safe Fix: ${withFix.map((a) => a.displayName).join(", ")}`);
36
+ }
37
+ if (detectOnlyConfigured.length > 0) {
38
+ parts.push(`detect-only (no Safe Fix writer): ${detectOnlyConfigured.map((a) => a.displayName).join(", ")}`);
39
+ }
40
+ lines.push(colors.dim(` Coverage: ${parts.join(" · ")}`));
41
+ return lines;
42
+ }
11
43
  function groupFindings(findings) {
12
44
  return {
13
45
  critical: findings.filter((f) => f.severity === "critical"),
@@ -152,6 +184,7 @@ export function renderTerminalReport(result, options = {}) {
152
184
  }
153
185
  }
154
186
  }
187
+ lines.push(...renderAgentCoverageSummary(result));
155
188
  lines.push("");
156
189
  const grouped = groupFindings(result.findings);
157
190
  const total = result.summary.total;
@@ -164,10 +197,10 @@ export function renderTerminalReport(result, options = {}) {
164
197
  }
165
198
  if (total === 0) {
166
199
  if (limited) {
167
- lines.push(` ${symbolWarn()} Nothing to audit yet — no Cursor, Claude Code, or Codex config found`);
200
+ lines.push(` ${symbolOk()} No repository-hygiene findings — agent readiness not scored (no supported agent config)`);
168
201
  lines.push("");
169
- lines.push(colors.dim(" Next: add project agent config (for example `.cursor/`, `CLAUDE.md` / `.claude/`, or `AGENTS.md`),"));
170
- lines.push(colors.dim(" then re-run `agentdoctor` in this repository."));
202
+ lines.push(colors.dim(" Optional next: add project agent config (`.cursor/`, `CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`, `.windsurf/rules/`, `GEMINI.md`, or `.aider.conf.yml`)"));
203
+ lines.push(colors.dim(" so agent exposure checks run fully. CLI, Action, and Project Brain MCP still work without an agent."));
171
204
  }
172
205
  else {
173
206
  lines.push(` ${symbolOk()} No findings`);
@@ -217,7 +250,7 @@ export function renderTerminalReport(result, options = {}) {
217
250
  lines.push("");
218
251
  if (result.scoringAvailable && result.scores) {
219
252
  if (limited) {
220
- lines.push(` Readiness: n/a — configure Cursor, Claude Code, or Codex before treating scores as agent readiness`);
253
+ lines.push(` Readiness: n/a — configure a supported agent (Cursor, Claude Code, Codex, Copilot, Windsurf, Gemini CLI, or Aider) before treating scores as agent readiness`);
221
254
  lines.push(colors.dim(` Numeric scores remain in JSON (overall ${result.scores.overall}/100) for repository-risk findings only`));
222
255
  }
223
256
  else {
@@ -0,0 +1,21 @@
1
+ export declare class PathEscapeError extends Error {
2
+ readonly code = "PATH_ESCAPE";
3
+ constructor(message?: string);
4
+ }
5
+ declare function tryDecodeUri(value: string): string | null;
6
+ /**
7
+ * Lexical hostility checks shared by resolve/assert helpers.
8
+ * Does not leak the candidate path in thrown messages.
9
+ */
10
+ export declare function rejectHostilePathInput(candidate: string): void;
11
+ /**
12
+ * Resolve a user-supplied path and require it to remain inside the repository root.
13
+ * Rejects `..`, URI-encoded traversal, null bytes, Windows-drive absolutes, and
14
+ * symlink realpath escapes. Error messages never echo the hostile candidate.
15
+ */
16
+ export declare function resolveSafeRepoPath(rootInput: string, candidate: string): string;
17
+ /** Assert `candidate` resolves inside `root`; throws PathEscapeError otherwise. */
18
+ export declare function assertInsideRepo(rootInput: string, candidate: string): void;
19
+ /** Resolve safely and return a POSIX path relative to the repo root. */
20
+ export declare function safeRelPath(rootInput: string, absoluteOrRel: string): string;
21
+ export { tryDecodeUri as tryDecodeUriComponent };