@praneeth_54/agentdoctor 0.3.0-beta → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/CHANGELOG.md +105 -1
  2. package/README.md +64 -31
  3. package/dist/agents/inspect.js +58 -51
  4. package/dist/cli/commands/brain-mcp.d.ts +9 -0
  5. package/dist/cli/commands/brain-mcp.js +38 -0
  6. package/dist/cli/commands/explain.js +1 -1
  7. package/dist/cli/commands/fix.js +22 -6
  8. package/dist/cli/commands/scan.d.ts +5 -0
  9. package/dist/cli/commands/scan.js +25 -6
  10. package/dist/cli/commands/verify.d.ts +6 -0
  11. package/dist/cli/commands/verify.js +25 -11
  12. package/dist/cli/program.js +61 -7
  13. package/dist/constants.d.ts +1 -1
  14. package/dist/constants.js +1 -1
  15. package/dist/core/fix/apply.d.ts +5 -1
  16. package/dist/core/fix/apply.js +34 -14
  17. package/dist/core/fix/plan.d.ts +1 -1
  18. package/dist/core/fix/plan.js +110 -3
  19. package/dist/core/fix/render.d.ts +2 -0
  20. package/dist/core/fix/render.js +57 -9
  21. package/dist/core/fix/run.js +5 -1
  22. package/dist/core/fix/types.d.ts +1 -1
  23. package/dist/core/fix/types.js +5 -1
  24. package/dist/core/fix/writers/claude-settings.d.ts +16 -0
  25. package/dist/core/fix/writers/claude-settings.js +100 -0
  26. package/dist/core/fix/writers/codex-config.d.ts +22 -0
  27. package/dist/core/fix/writers/codex-config.js +183 -0
  28. package/dist/core/fix/writers/cursorignore.js +2 -8
  29. package/dist/core/path-resolution/index.d.ts +5 -0
  30. package/dist/core/path-resolution/index.js +5 -0
  31. package/dist/core/path-resolution/prepare.d.ts +33 -0
  32. package/dist/core/path-resolution/prepare.js +98 -0
  33. package/dist/core/policy/evaluate.d.ts +44 -0
  34. package/dist/core/policy/evaluate.js +120 -0
  35. package/dist/core/rules/claude-deny.d.ts +8 -0
  36. package/dist/core/rules/claude-deny.js +39 -0
  37. package/dist/core/rules/codex-deny.d.ts +5 -0
  38. package/dist/core/rules/codex-deny.js +42 -0
  39. package/dist/core/rules/context/generated-directory.js +43 -9
  40. package/dist/core/rules/context/large-log-file.js +27 -10
  41. package/dist/core/rules/ignore.js +2 -5
  42. package/dist/core/rules/instructions/missing-path-reference.d.ts +5 -1
  43. package/dist/core/rules/instructions/missing-path-reference.js +24 -6
  44. package/dist/core/rules/path-kind.d.ts +15 -2
  45. package/dist/core/rules/path-kind.js +113 -10
  46. package/dist/core/rules/security/env-file-exposure.js +9 -24
  47. package/dist/core/rules/text-cache.js +46 -40
  48. package/dist/core/understanding/architecture/index.d.ts +4 -0
  49. package/dist/core/understanding/architecture/index.js +3 -0
  50. package/dist/core/understanding/architecture/infer.d.ts +6 -0
  51. package/dist/core/understanding/architecture/infer.js +39 -0
  52. package/dist/core/understanding/architecture/models.d.ts +23 -0
  53. package/dist/core/understanding/architecture/models.js +127 -0
  54. package/dist/core/understanding/architecture/patterns/distribution.d.ts +2 -0
  55. package/dist/core/understanding/architecture/patterns/distribution.js +115 -0
  56. package/dist/core/understanding/architecture/patterns/index.d.ts +2 -0
  57. package/dist/core/understanding/architecture/patterns/index.js +8 -0
  58. package/dist/core/understanding/architecture/patterns/layered.d.ts +2 -0
  59. package/dist/core/understanding/architecture/patterns/layered.js +279 -0
  60. package/dist/core/understanding/architecture/patterns/structural.d.ts +2 -0
  61. package/dist/core/understanding/architecture/patterns/structural.js +197 -0
  62. package/dist/core/understanding/architecture/rules.d.ts +4 -0
  63. package/dist/core/understanding/architecture/rules.js +6 -0
  64. package/dist/core/understanding/architecture/types.d.ts +52 -0
  65. package/dist/core/understanding/architecture/types.js +1 -0
  66. package/dist/core/understanding/brain/build.d.ts +13 -0
  67. package/dist/core/understanding/brain/build.js +158 -0
  68. package/dist/core/understanding/brain/claims/index.d.ts +3 -0
  69. package/dist/core/understanding/brain/claims/index.js +2 -0
  70. package/dist/core/understanding/brain/claims/lifecycle.d.ts +16 -0
  71. package/dist/core/understanding/brain/claims/lifecycle.js +78 -0
  72. package/dist/core/understanding/brain/claims/types.d.ts +32 -0
  73. package/dist/core/understanding/brain/claims/types.js +25 -0
  74. package/dist/core/understanding/brain/components/index.d.ts +17 -0
  75. package/dist/core/understanding/brain/components/index.js +97 -0
  76. package/dist/core/understanding/brain/components/types.d.ts +16 -0
  77. package/dist/core/understanding/brain/components/types.js +8 -0
  78. package/dist/core/understanding/brain/confidence.d.ts +19 -0
  79. package/dist/core/understanding/brain/confidence.js +39 -0
  80. package/dist/core/understanding/brain/contract.d.ts +17 -0
  81. package/dist/core/understanding/brain/contract.js +100 -0
  82. package/dist/core/understanding/brain/contradictions/index.d.ts +12 -0
  83. package/dist/core/understanding/brain/contradictions/index.js +57 -0
  84. package/dist/core/understanding/brain/contradictions/types.d.ts +12 -0
  85. package/dist/core/understanding/brain/contradictions/types.js +8 -0
  86. package/dist/core/understanding/brain/delta.d.ts +25 -0
  87. package/dist/core/understanding/brain/delta.js +116 -0
  88. package/dist/core/understanding/brain/evidence/index.d.ts +3 -0
  89. package/dist/core/understanding/brain/evidence/index.js +2 -0
  90. package/dist/core/understanding/brain/evidence/redact.d.ts +4 -0
  91. package/dist/core/understanding/brain/evidence/redact.js +36 -0
  92. package/dist/core/understanding/brain/evidence/types.d.ts +34 -0
  93. package/dist/core/understanding/brain/evidence/types.js +35 -0
  94. package/dist/core/understanding/brain/explain.d.ts +17 -0
  95. package/dist/core/understanding/brain/explain.js +33 -0
  96. package/dist/core/understanding/brain/index.d.ts +28 -0
  97. package/dist/core/understanding/brain/index.js +16 -0
  98. package/dist/core/understanding/brain/migrate.d.ts +21 -0
  99. package/dist/core/understanding/brain/migrate.js +47 -0
  100. package/dist/core/understanding/brain/query.d.ts +60 -0
  101. package/dist/core/understanding/brain/query.js +92 -0
  102. package/dist/core/understanding/brain/security.d.ts +3 -0
  103. package/dist/core/understanding/brain/security.js +43 -0
  104. package/dist/core/understanding/brain/storage/index.d.ts +2 -0
  105. package/dist/core/understanding/brain/storage/index.js +1 -0
  106. package/dist/core/understanding/brain/storage/store.d.ts +53 -0
  107. package/dist/core/understanding/brain/storage/store.js +305 -0
  108. package/dist/core/understanding/brain/trace.d.ts +24 -0
  109. package/dist/core/understanding/brain/trace.js +207 -0
  110. package/dist/core/understanding/brain/types.d.ts +32 -0
  111. package/dist/core/understanding/brain/types.js +8 -0
  112. package/dist/core/understanding/brain/version.d.ts +13 -0
  113. package/dist/core/understanding/brain/version.js +7 -0
  114. package/dist/core/understanding/delta/compare.d.ts +26 -0
  115. package/dist/core/understanding/delta/compare.js +74 -0
  116. package/dist/core/understanding/delta/index.d.ts +2 -0
  117. package/dist/core/understanding/delta/index.js +1 -0
  118. package/dist/core/understanding/dependencies/discover.d.ts +7 -0
  119. package/dist/core/understanding/dependencies/discover.js +550 -0
  120. package/dist/core/understanding/dependencies/extract.d.ts +10 -0
  121. package/dist/core/understanding/dependencies/extract.js +189 -0
  122. package/dist/core/understanding/dependencies/index.d.ts +4 -0
  123. package/dist/core/understanding/dependencies/index.js +3 -0
  124. package/dist/core/understanding/dependencies/models.d.ts +12 -0
  125. package/dist/core/understanding/dependencies/models.js +125 -0
  126. package/dist/core/understanding/dependencies/types.d.ts +27 -0
  127. package/dist/core/understanding/dependencies/types.js +1 -0
  128. package/dist/core/understanding/domain/discover.d.ts +14 -0
  129. package/dist/core/understanding/domain/discover.js +90 -0
  130. package/dist/core/understanding/domain/index.d.ts +2 -0
  131. package/dist/core/understanding/domain/index.js +1 -0
  132. package/dist/core/understanding/entrypoints/discover.d.ts +6 -0
  133. package/dist/core/understanding/entrypoints/discover.js +62 -0
  134. package/dist/core/understanding/entrypoints/extract.d.ts +19 -0
  135. package/dist/core/understanding/entrypoints/extract.js +43 -0
  136. package/dist/core/understanding/entrypoints/index.d.ts +4 -0
  137. package/dist/core/understanding/entrypoints/index.js +3 -0
  138. package/dist/core/understanding/entrypoints/models.d.ts +26 -0
  139. package/dist/core/understanding/entrypoints/models.js +216 -0
  140. package/dist/core/understanding/entrypoints/types.d.ts +20 -0
  141. package/dist/core/understanding/entrypoints/types.js +1 -0
  142. package/dist/core/understanding/index.d.ts +32 -0
  143. package/dist/core/understanding/index.js +18 -0
  144. package/dist/core/understanding/mind/build.d.ts +16 -0
  145. package/dist/core/understanding/mind/build.js +134 -0
  146. package/dist/core/understanding/mind/index.d.ts +6 -0
  147. package/dist/core/understanding/mind/index.js +3 -0
  148. package/dist/core/understanding/mind/query.d.ts +33 -0
  149. package/dist/core/understanding/mind/query.js +39 -0
  150. package/dist/core/understanding/mind/types.d.ts +24 -0
  151. package/dist/core/understanding/mind/types.js +7 -0
  152. package/dist/core/understanding/model/builder.d.ts +9 -0
  153. package/dist/core/understanding/model/builder.js +209 -0
  154. package/dist/core/understanding/model/ids.d.ts +7 -0
  155. package/dist/core/understanding/model/ids.js +48 -0
  156. package/dist/core/understanding/model/index.d.ts +7 -0
  157. package/dist/core/understanding/model/index.js +6 -0
  158. package/dist/core/understanding/model/schema.d.ts +12 -0
  159. package/dist/core/understanding/model/schema.js +33 -0
  160. package/dist/core/understanding/model/serializer.d.ts +9 -0
  161. package/dist/core/understanding/model/serializer.js +46 -0
  162. package/dist/core/understanding/model/types.d.ts +110 -0
  163. package/dist/core/understanding/model/types.js +1 -0
  164. package/dist/core/understanding/model/validator.d.ts +5 -0
  165. package/dist/core/understanding/model/validator.js +126 -0
  166. package/dist/core/understanding/model/version.d.ts +9 -0
  167. package/dist/core/understanding/model/version.js +7 -0
  168. package/dist/core/understanding/ownership/discover.d.ts +12 -0
  169. package/dist/core/understanding/ownership/discover.js +242 -0
  170. package/dist/core/understanding/ownership/index.d.ts +2 -0
  171. package/dist/core/understanding/ownership/index.js +1 -0
  172. package/dist/core/understanding/ownership/types.d.ts +23 -0
  173. package/dist/core/understanding/ownership/types.js +1 -0
  174. package/dist/core/understanding/query/engine.d.ts +24 -0
  175. package/dist/core/understanding/query/engine.js +35 -0
  176. package/dist/core/understanding/query/errors.d.ts +13 -0
  177. package/dist/core/understanding/query/errors.js +26 -0
  178. package/dist/core/understanding/query/executor.d.ts +8 -0
  179. package/dist/core/understanding/query/executor.js +34 -0
  180. package/dist/core/understanding/query/index.d.ts +7 -0
  181. package/dist/core/understanding/query/index.js +5 -0
  182. package/dist/core/understanding/query/models.d.ts +102 -0
  183. package/dist/core/understanding/query/models.js +1 -0
  184. package/dist/core/understanding/query/query.d.ts +68 -0
  185. package/dist/core/understanding/query/query.js +145 -0
  186. package/dist/core/understanding/query/registry.d.ts +8 -0
  187. package/dist/core/understanding/query/registry.js +340 -0
  188. package/dist/core/understanding/query/types.d.ts +63 -0
  189. package/dist/core/understanding/query/types.js +1 -0
  190. package/dist/core/understanding/relationships/discover.d.ts +11 -0
  191. package/dist/core/understanding/relationships/discover.js +517 -0
  192. package/dist/core/understanding/relationships/extract.d.ts +14 -0
  193. package/dist/core/understanding/relationships/extract.js +165 -0
  194. package/dist/core/understanding/relationships/index.d.ts +4 -0
  195. package/dist/core/understanding/relationships/index.js +3 -0
  196. package/dist/core/understanding/relationships/models.d.ts +21 -0
  197. package/dist/core/understanding/relationships/models.js +185 -0
  198. package/dist/core/understanding/relationships/types.d.ts +42 -0
  199. package/dist/core/understanding/relationships/types.js +1 -0
  200. package/dist/core/understanding/risks/discover.d.ts +8 -0
  201. package/dist/core/understanding/risks/discover.js +155 -0
  202. package/dist/core/understanding/risks/index.d.ts +2 -0
  203. package/dist/core/understanding/risks/index.js +1 -0
  204. package/dist/core/understanding/risks/types.d.ts +26 -0
  205. package/dist/core/understanding/risks/types.js +1 -0
  206. package/dist/core/understanding/shared/domain-lexicon.d.ts +9 -0
  207. package/dist/core/understanding/shared/domain-lexicon.js +107 -0
  208. package/dist/core/understanding/shared/index.d.ts +2 -0
  209. package/dist/core/understanding/shared/index.js +2 -0
  210. package/dist/core/understanding/shared/tokens.d.ts +7 -0
  211. package/dist/core/understanding/shared/tokens.js +141 -0
  212. package/dist/core/understanding/snapshot/identity.d.ts +24 -0
  213. package/dist/core/understanding/snapshot/identity.js +79 -0
  214. package/dist/core/understanding/snapshot/index.d.ts +2 -0
  215. package/dist/core/understanding/snapshot/index.js +1 -0
  216. package/dist/core/understanding/types/index.d.ts +22 -0
  217. package/dist/core/understanding/types/index.js +1 -0
  218. package/dist/core/understanding/understand/formatter.d.ts +10 -0
  219. package/dist/core/understanding/understand/formatter.js +115 -0
  220. package/dist/core/understanding/understand/index.d.ts +4 -0
  221. package/dist/core/understanding/understand/index.js +3 -0
  222. package/dist/core/understanding/understand/service.d.ts +17 -0
  223. package/dist/core/understanding/understand/service.js +76 -0
  224. package/dist/core/understanding/understand/summary.d.ts +14 -0
  225. package/dist/core/understanding/understand/summary.js +39 -0
  226. package/dist/core/understanding/understand/types.d.ts +31 -0
  227. package/dist/core/understanding/understand/types.js +1 -0
  228. package/dist/discovery/files.js +7 -2
  229. package/dist/discovery/log-like.d.ts +2 -0
  230. package/dist/discovery/log-like.js +7 -0
  231. package/dist/index.d.ts +2 -0
  232. package/dist/index.js +1 -0
  233. package/dist/mcp/brain/compile.d.ts +9 -0
  234. package/dist/mcp/brain/compile.js +42 -0
  235. package/dist/mcp/brain/errors.d.ts +12 -0
  236. package/dist/mcp/brain/errors.js +23 -0
  237. package/dist/mcp/brain/index.d.ts +11 -0
  238. package/dist/mcp/brain/index.js +8 -0
  239. package/dist/mcp/brain/provenance.d.ts +29 -0
  240. package/dist/mcp/brain/provenance.js +59 -0
  241. package/dist/mcp/brain/schemas.d.ts +13 -0
  242. package/dist/mcp/brain/schemas.js +105 -0
  243. package/dist/mcp/brain/security/root.d.ts +6 -0
  244. package/dist/mcp/brain/security/root.js +66 -0
  245. package/dist/mcp/brain/server.d.ts +12 -0
  246. package/dist/mcp/brain/server.js +46 -0
  247. package/dist/mcp/brain/session.d.ts +33 -0
  248. package/dist/mcp/brain/session.js +117 -0
  249. package/dist/mcp/brain/tools/handlers.d.ts +13 -0
  250. package/dist/mcp/brain/tools/handlers.js +483 -0
  251. package/dist/mcp/brain/tools/registry.d.ts +9 -0
  252. package/dist/mcp/brain/tools/registry.js +191 -0
  253. package/dist/reporters/github/annotations.d.ts +8 -0
  254. package/dist/reporters/github/annotations.js +42 -0
  255. package/dist/reporters/github/emit.d.ts +23 -0
  256. package/dist/reporters/github/emit.js +31 -0
  257. package/dist/reporters/github/summary.d.ts +20 -0
  258. package/dist/reporters/github/summary.js +111 -0
  259. package/dist/reporters/terminal/report.d.ts +5 -0
  260. package/dist/reporters/terminal/report.js +68 -5
  261. package/dist/reporters/verify/terminal.d.ts +4 -0
  262. package/dist/reporters/verify/terminal.js +26 -0
  263. package/dist/utils/fs.d.ts +5 -0
  264. package/dist/utils/fs.js +34 -0
  265. package/dist/utils/path.d.ts +5 -0
  266. package/dist/utils/path.js +16 -0
  267. package/package.json +15 -1
@@ -0,0 +1,100 @@
1
+ import path from "node:path";
2
+ import { atomicWriteTextFile, readTextFile } from "../../../utils/fs.js";
3
+ import { claudeReadDenyRule } from "../../rules/claude-deny.js";
4
+ const MAX_BYTES = 512 * 1024;
5
+ const SETTINGS_RELATIVE = ".claude/settings.json";
6
+ export function denyRuleForFixPattern(pattern, evidencePath) {
7
+ const isDirectory = pattern.endsWith("/");
8
+ return claudeReadDenyRule(evidencePath, isDirectory ? "directory" : "file");
9
+ }
10
+ export function missingClaudeDenyRules(currentContent, denyRules) {
11
+ const existing = parseDenyList(currentContent);
12
+ const missing = [];
13
+ for (const rule of denyRules) {
14
+ if (existing.includes(rule)) {
15
+ continue;
16
+ }
17
+ missing.push(rule);
18
+ }
19
+ return missing;
20
+ }
21
+ export function buildClaudeSettingsContent(currentContent, denyRulesToAdd) {
22
+ if (denyRulesToAdd.length === 0) {
23
+ return currentContent ?? `${JSON.stringify({ permissions: { deny: [] } }, null, 2)}\n`;
24
+ }
25
+ let parsed = {};
26
+ if (currentContent && currentContent.trim().length > 0) {
27
+ try {
28
+ parsed = JSON.parse(currentContent);
29
+ }
30
+ catch {
31
+ throw new Error(`${SETTINGS_RELATIVE} is not valid JSON; refusing to apply Claude Fix`);
32
+ }
33
+ }
34
+ const permissions = parsed.permissions && typeof parsed.permissions === "object" ? { ...parsed.permissions } : {};
35
+ const deny = Array.isArray(permissions.deny)
36
+ ? permissions.deny.filter((item) => typeof item === "string")
37
+ : [];
38
+ for (const rule of denyRulesToAdd) {
39
+ if (!deny.includes(rule)) {
40
+ deny.push(rule);
41
+ }
42
+ }
43
+ permissions.deny = deny;
44
+ parsed.permissions = permissions;
45
+ return `${JSON.stringify(parsed, null, 2)}\n`;
46
+ }
47
+ export function previewClaudeSettingsActions(currentContent, actions) {
48
+ const claudeActions = actions.filter((a) => a.agent === "claude-code" && a.targetRelativePath === SETTINGS_RELATIVE);
49
+ if (claudeActions.length === 0) {
50
+ return null;
51
+ }
52
+ const requested = [
53
+ ...new Set(claudeActions.map((a) => denyRuleForFixPattern(a.pattern, a.evidencePath))),
54
+ ];
55
+ const denyRulesToAdd = missingClaudeDenyRules(currentContent, requested);
56
+ if (denyRulesToAdd.length === 0) {
57
+ return null;
58
+ }
59
+ const before = currentContent ?? "";
60
+ const after = buildClaudeSettingsContent(currentContent, denyRulesToAdd);
61
+ return {
62
+ targetRelativePath: SETTINGS_RELATIVE,
63
+ denyRulesToAdd,
64
+ before,
65
+ after,
66
+ preview: formatSimpleDiff(SETTINGS_RELATIVE, before, after),
67
+ };
68
+ }
69
+ export async function readClaudeSettings(root) {
70
+ return readTextFile(path.join(root, SETTINGS_RELATIVE), MAX_BYTES);
71
+ }
72
+ export async function writeClaudeSettings(root, content) {
73
+ await atomicWriteTextFile(path.join(root, SETTINGS_RELATIVE), content);
74
+ }
75
+ function parseDenyList(currentContent) {
76
+ if (!currentContent || currentContent.trim().length === 0) {
77
+ return [];
78
+ }
79
+ try {
80
+ const parsed = JSON.parse(currentContent);
81
+ const deny = parsed.permissions?.deny;
82
+ if (!Array.isArray(deny)) {
83
+ return [];
84
+ }
85
+ return deny.filter((item) => typeof item === "string");
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ }
91
+ function formatSimpleDiff(fileLabel, before, after) {
92
+ const lines = [];
93
+ lines.push(`--- a/${fileLabel}`);
94
+ lines.push(`+++ b/${fileLabel}`);
95
+ lines.push(before.trim().length === 0 ? "@@ new file @@" : "@@ update @@");
96
+ for (const line of after.split(/\r?\n/)) {
97
+ lines.push(`+${line}`);
98
+ }
99
+ return lines.join("\n");
100
+ }
@@ -0,0 +1,22 @@
1
+ import type { FixAction } from "../types.js";
2
+ declare const CONFIG_RELATIVE = ".codex/config.toml";
3
+ export interface CodexConfigWritePreview {
4
+ targetRelativePath: typeof CONFIG_RELATIVE;
5
+ denyKeysToAdd: string[];
6
+ before: string;
7
+ after: string;
8
+ preview: string;
9
+ }
10
+ export declare function denyKeyForFixPattern(_pattern: string, evidencePath: string): string;
11
+ export declare function missingCodexDenyKeys(currentContent: string | null, denyKeys: string[]): string[];
12
+ /**
13
+ * Merge filesystem deny keys into project `.codex/config.toml`.
14
+ * Refuses when sandbox_mode is present (permission profiles do not compose).
15
+ * Refuses when default_permissions selects a built-in `:…` profile.
16
+ */
17
+ export declare function buildCodexConfigContent(currentContent: string | null, denyKeysToAdd: string[]): string;
18
+ export declare function previewCodexConfigActions(currentContent: string | null, actions: FixAction[]): CodexConfigWritePreview | null;
19
+ export declare function readCodexConfig(root: string): Promise<string | null>;
20
+ export declare function writeCodexConfig(root: string, content: string): Promise<void>;
21
+ export declare function assertWritableCodexConfig(content: string): void;
22
+ export {};
@@ -0,0 +1,183 @@
1
+ import path from "node:path";
2
+ import { atomicWriteTextFile, readTextFile } from "../../../utils/fs.js";
3
+ import { codexDenyKey, configTextDeniesPath } from "../../rules/codex-deny.js";
4
+ const MAX_BYTES = 512 * 1024;
5
+ const CONFIG_RELATIVE = ".codex/config.toml";
6
+ const AGENTDOCTOR_PROFILE = "agentdoctor_context";
7
+ const BEGIN_MARKER = "# BEGIN AgentDoctor context exclusions";
8
+ const END_MARKER = "# END AgentDoctor context exclusions";
9
+ export function denyKeyForFixPattern(_pattern, evidencePath) {
10
+ return codexDenyKey(evidencePath);
11
+ }
12
+ export function missingCodexDenyKeys(currentContent, denyKeys) {
13
+ if (!currentContent || currentContent.trim().length === 0) {
14
+ return [...new Set(denyKeys)];
15
+ }
16
+ const missing = [];
17
+ for (const key of denyKeys) {
18
+ if (configTextDeniesPath(currentContent, key)) {
19
+ continue;
20
+ }
21
+ missing.push(key);
22
+ }
23
+ return [...new Set(missing)];
24
+ }
25
+ /**
26
+ * Merge filesystem deny keys into project `.codex/config.toml`.
27
+ * Refuses when sandbox_mode is present (permission profiles do not compose).
28
+ * Refuses when default_permissions selects a built-in `:…` profile.
29
+ */
30
+ export function buildCodexConfigContent(currentContent, denyKeysToAdd) {
31
+ if (denyKeysToAdd.length === 0) {
32
+ return currentContent ?? "";
33
+ }
34
+ const existing = currentContent ?? "";
35
+ assertWritableCodexConfig(existing);
36
+ const profile = resolveTargetProfile(existing);
37
+ const sectionHeader = `[permissions.${profile}.filesystem.":workspace_roots"]`;
38
+ let next = existing;
39
+ if (!hasDefaultPermissions(next)) {
40
+ next = ensureTrailingNewline(next);
41
+ if (next.trim().length > 0) {
42
+ next += "\n";
43
+ }
44
+ next += `${BEGIN_MARKER}\n`;
45
+ next += `default_permissions = "${profile}"\n`;
46
+ }
47
+ const section = findTomlTableRange(next, sectionHeader);
48
+ if (!section) {
49
+ next = ensureTrailingNewline(next);
50
+ if (!next.includes(BEGIN_MARKER)) {
51
+ next += `\n${BEGIN_MARKER}\n`;
52
+ }
53
+ next += `${sectionHeader}\n`;
54
+ if (profile === AGENTDOCTOR_PROFILE) {
55
+ next += `"." = "write"\n`;
56
+ }
57
+ for (const key of denyKeysToAdd) {
58
+ next += `"${key}" = "deny"\n`;
59
+ }
60
+ if (!next.includes(END_MARKER)) {
61
+ next += `${END_MARKER}\n`;
62
+ }
63
+ return next;
64
+ }
65
+ const body = next.slice(section.bodyStart, section.end);
66
+ const linesToInsert = [];
67
+ for (const key of denyKeysToAdd) {
68
+ if (configTextDeniesPath(body, key) || configTextDeniesPath(next, key)) {
69
+ continue;
70
+ }
71
+ linesToInsert.push(`"${key}" = "deny"`);
72
+ }
73
+ if (linesToInsert.length === 0) {
74
+ return next;
75
+ }
76
+ const beforeBodyEnd = next.slice(0, section.end);
77
+ const afterBodyEnd = next.slice(section.end);
78
+ const trimmedBodyEnd = beforeBodyEnd.endsWith("\n") ? beforeBodyEnd : `${beforeBodyEnd}\n`;
79
+ const suffix = afterBodyEnd.startsWith("\n") || afterBodyEnd.length === 0 ? afterBodyEnd : `\n${afterBodyEnd}`;
80
+ return `${trimmedBodyEnd}${linesToInsert.join("\n")}\n${suffix}`;
81
+ }
82
+ export function previewCodexConfigActions(currentContent, actions) {
83
+ const codexActions = actions.filter((a) => a.agent === "codex" && a.targetRelativePath === CONFIG_RELATIVE);
84
+ if (codexActions.length === 0) {
85
+ return null;
86
+ }
87
+ const requested = [
88
+ ...new Set(codexActions.map((a) => denyKeyForFixPattern(a.pattern, a.evidencePath))),
89
+ ];
90
+ const denyKeysToAdd = missingCodexDenyKeys(currentContent, requested);
91
+ if (denyKeysToAdd.length === 0) {
92
+ return null;
93
+ }
94
+ const before = currentContent ?? "";
95
+ const after = buildCodexConfigContent(currentContent, denyKeysToAdd);
96
+ return {
97
+ targetRelativePath: CONFIG_RELATIVE,
98
+ denyKeysToAdd,
99
+ before,
100
+ after,
101
+ preview: formatSimpleDiff(CONFIG_RELATIVE, before, after),
102
+ };
103
+ }
104
+ export async function readCodexConfig(root) {
105
+ return readTextFile(path.join(root, CONFIG_RELATIVE), MAX_BYTES);
106
+ }
107
+ export async function writeCodexConfig(root, content) {
108
+ await atomicWriteTextFile(path.join(root, CONFIG_RELATIVE), content);
109
+ }
110
+ export function assertWritableCodexConfig(content) {
111
+ if (/^\s*sandbox_mode\s*=/m.test(content)) {
112
+ throw new Error(`${CONFIG_RELATIVE} uses sandbox_mode; refusing Codex Fix (permission profiles do not compose with sandbox_mode)`);
113
+ }
114
+ const builtin = readDefaultPermissions(content);
115
+ if (builtin && builtin.startsWith(":")) {
116
+ throw new Error(`${CONFIG_RELATIVE} default_permissions is built-in "${builtin}"; refusing Codex Fix (create a custom permissions profile first)`);
117
+ }
118
+ if (content.trim().length > 0 && !isRecognizableCodexConfig(content)) {
119
+ throw new Error(`${CONFIG_RELATIVE} does not look like valid Codex config TOML; refusing Codex Fix`);
120
+ }
121
+ }
122
+ /** Allow empty/comment-only files, AgentDoctor-managed blocks, or known Codex tables. */
123
+ function isRecognizableCodexConfig(content) {
124
+ if (content.includes(BEGIN_MARKER) || content.includes(END_MARKER)) {
125
+ return true;
126
+ }
127
+ if (readDefaultPermissions(content) !== null) {
128
+ return true;
129
+ }
130
+ if (/^\s*\[(permissions\.|mcp_servers\.|projects\.)/m.test(content)) {
131
+ return true;
132
+ }
133
+ // Comment-only / blank files are safe to initialize.
134
+ for (const line of content.split(/\r?\n/)) {
135
+ const trimmed = line.trim();
136
+ if (trimmed.length === 0 || trimmed.startsWith("#")) {
137
+ continue;
138
+ }
139
+ return false;
140
+ }
141
+ return true;
142
+ }
143
+ function resolveTargetProfile(content) {
144
+ const existing = readDefaultPermissions(content);
145
+ if (existing && !existing.startsWith(":")) {
146
+ return existing;
147
+ }
148
+ return AGENTDOCTOR_PROFILE;
149
+ }
150
+ function hasDefaultPermissions(content) {
151
+ return readDefaultPermissions(content) !== null;
152
+ }
153
+ function readDefaultPermissions(content) {
154
+ const match = content.match(/^\s*default_permissions\s*=\s*["']([^"']+)["']\s*$/m);
155
+ return match?.[1] ?? null;
156
+ }
157
+ function findTomlTableRange(content, header) {
158
+ const start = content.indexOf(header);
159
+ if (start < 0) {
160
+ return null;
161
+ }
162
+ const bodyStart = start + header.length;
163
+ const rest = content.slice(bodyStart);
164
+ const nextHeader = rest.search(/\n\s*\[/);
165
+ const end = nextHeader < 0 ? content.length : bodyStart + nextHeader;
166
+ return { start, bodyStart, end };
167
+ }
168
+ function ensureTrailingNewline(value) {
169
+ if (value.length === 0 || value.endsWith("\n")) {
170
+ return value;
171
+ }
172
+ return `${value}\n`;
173
+ }
174
+ function formatSimpleDiff(fileLabel, before, after) {
175
+ const lines = [];
176
+ lines.push(`--- a/${fileLabel}`);
177
+ lines.push(`+++ b/${fileLabel}`);
178
+ lines.push(before.trim().length === 0 ? "@@ new file @@" : "@@ update @@");
179
+ for (const line of after.split(/\r?\n/)) {
180
+ lines.push(`+${line}`);
181
+ }
182
+ return lines.join("\n");
183
+ }
@@ -1,6 +1,5 @@
1
- import fs from "node:fs/promises";
2
1
  import path from "node:path";
3
- import { readTextFile } from "../../../utils/fs.js";
2
+ import { atomicWriteTextFile, readTextFile } from "../../../utils/fs.js";
4
3
  import { missingPatternsForCursorignore } from "../plan.js";
5
4
  const MAX_BYTES = 512 * 1024;
6
5
  const BANNER = "# Added by AgentDoctor";
@@ -41,12 +40,7 @@ export async function readCursorignore(root) {
41
40
  return readTextFile(path.join(root, ".cursorignore"), MAX_BYTES);
42
41
  }
43
42
  export async function writeCursorignore(root, content) {
44
- const target = path.join(root, ".cursorignore");
45
- const dir = path.dirname(target);
46
- await fs.mkdir(dir, { recursive: true });
47
- const tmp = path.join(dir, `.cursorignore.agentdoctor.${process.pid}.tmp`);
48
- await fs.writeFile(tmp, content, "utf8");
49
- await fs.rename(tmp, target);
43
+ await atomicWriteTextFile(path.join(root, ".cursorignore"), content);
50
44
  }
51
45
  function formatSimpleDiff(fileLabel, before, after) {
52
46
  const beforeLines = before.length === 0 ? [] : before.split(/\r?\n/);
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Path Resolution Engine — shared path reference preparation/resolution.
3
+ * Stage 1 exports preparation only; later stages extend this surface.
4
+ */
5
+ export { isConcretePathReference, normalizePathReference, preparePathReference, type PathConcreteRejectReason, type PreparedPathReference, } from "./prepare.js";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Path Resolution Engine — shared path reference preparation/resolution.
3
+ * Stage 1 exports preparation only; later stages extend this surface.
4
+ */
5
+ export { isConcretePathReference, normalizePathReference, preparePathReference, } from "./prepare.js";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Path Resolution Engine — Stage 1: prepare candidates for deterministic resolution.
3
+ *
4
+ * Shared infrastructure. Future stages add lattice resolution; Stage 1 only:
5
+ * - concrete-only filtering
6
+ * - URI decoding
7
+ * - path normalization
8
+ *
9
+ * No repository-specific or framework-specific logic.
10
+ */
11
+ export type PathConcreteRejectReason = "empty" | "placeholder" | "ellipsis" | "home" | "git-ref" | "bundler-module-type" | "undecodable";
12
+ export type PreparedPathReference = {
13
+ status: "concrete";
14
+ /** Raw reference as extracted from source text. */
15
+ original: string;
16
+ /** Decoded, separator-normalized form used for resolution/existence checks. */
17
+ normalized: string;
18
+ } | {
19
+ status: "reject";
20
+ original: string;
21
+ reason: PathConcreteRejectReason;
22
+ };
23
+ /**
24
+ * Normalize a path reference for filesystem checks:
25
+ * URI-decode percent-encoding, unify separators to `/`.
26
+ * Does not decide concreteness.
27
+ */
28
+ export declare function normalizePathReference(raw: string): string | null;
29
+ /**
30
+ * Stage 1 entry point: filter non-concrete references and normalize survivors.
31
+ */
32
+ export declare function preparePathReference(raw: string): PreparedPathReference;
33
+ export declare function isConcretePathReference(raw: string): boolean;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Path Resolution Engine — Stage 1: prepare candidates for deterministic resolution.
3
+ *
4
+ * Shared infrastructure. Future stages add lattice resolution; Stage 1 only:
5
+ * - concrete-only filtering
6
+ * - URI decoding
7
+ * - path normalization
8
+ *
9
+ * No repository-specific or framework-specific logic.
10
+ */
11
+ /** Angle-bracket or `{name}` template slots — intentional non-paths. */
12
+ const PLACEHOLDER_TOKEN = /(?:<[^<>\s]+>|\{[A-Za-z_][A-Za-z0-9_]*\})/;
13
+ /** Ellipsis segments used as documentation wildcards (`bases/base/...`). */
14
+ const ELLIPSIS_SEGMENT = /(^|\/)\.\.\.(\/|$)/;
15
+ /**
16
+ * Git-ref shaped tokens that appear in backticks but are not filesystem paths.
17
+ * Kept narrow: remote refs + conventional-commit branch prefixes.
18
+ * Does not treat `docs/` as a branch prefix (collides with documentation trees).
19
+ * Branch-like prefixes are ignored when the reference ends with a file extension.
20
+ */
21
+ const REMOTE_REF_SHAPE = /^(?:origin|upstream|HEAD)\/[A-Za-z0-9._/-]+$/;
22
+ const BRANCH_PREFIX_SHAPE = /^(?:feature|fix|chore|hotfix|release)\/[A-Za-z0-9._/-]+$/;
23
+ const FILE_EXTENSION_SUFFIX = /\.[A-Za-z0-9]{1,16}$/;
24
+ function looksLikeGitRef(normalized) {
25
+ if (REMOTE_REF_SHAPE.test(normalized)) {
26
+ return true;
27
+ }
28
+ if (!BRANCH_PREFIX_SHAPE.test(normalized)) {
29
+ return false;
30
+ }
31
+ // `fix/login.ts` is a path; `fix/bug-in-worker-threads` is a branch token
32
+ return !FILE_EXTENSION_SUFFIX.test(normalized);
33
+ }
34
+ /**
35
+ * Bundler module-type tokens (`asset/resource`, `javascript/auto`) — not paths.
36
+ * Generalized type/name pattern; no bundler product names.
37
+ */
38
+ const BUNDLER_MODULE_TYPE = /^(?:asset|javascript|css|json|wasm|webassembly|runtime)\/[A-Za-z0-9_-]+$/;
39
+ /**
40
+ * Normalize a path reference for filesystem checks:
41
+ * URI-decode percent-encoding, unify separators to `/`.
42
+ * Does not decide concreteness.
43
+ */
44
+ export function normalizePathReference(raw) {
45
+ if (!raw) {
46
+ return null;
47
+ }
48
+ let decoded;
49
+ try {
50
+ decoded = decodeURIComponent(raw);
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ return decoded.replace(/\\/g, "/");
56
+ }
57
+ function concretenessRejectReason(normalized) {
58
+ if (!normalized) {
59
+ return "empty";
60
+ }
61
+ if (PLACEHOLDER_TOKEN.test(normalized)) {
62
+ return "placeholder";
63
+ }
64
+ if (ELLIPSIS_SEGMENT.test(normalized)) {
65
+ return "ellipsis";
66
+ }
67
+ if (normalized === "~" || normalized.startsWith("~/") || normalized.startsWith("~\\")) {
68
+ return "home";
69
+ }
70
+ if (looksLikeGitRef(normalized)) {
71
+ return "git-ref";
72
+ }
73
+ if (BUNDLER_MODULE_TYPE.test(normalized)) {
74
+ return "bundler-module-type";
75
+ }
76
+ return null;
77
+ }
78
+ /**
79
+ * Stage 1 entry point: filter non-concrete references and normalize survivors.
80
+ */
81
+ export function preparePathReference(raw) {
82
+ const original = raw;
83
+ if (!original || !original.trim()) {
84
+ return { status: "reject", original, reason: "empty" };
85
+ }
86
+ const normalized = normalizePathReference(original.trim());
87
+ if (normalized === null) {
88
+ return { status: "reject", original, reason: "undecodable" };
89
+ }
90
+ const reason = concretenessRejectReason(normalized);
91
+ if (reason) {
92
+ return { status: "reject", original, reason };
93
+ }
94
+ return { status: "concrete", original, normalized };
95
+ }
96
+ export function isConcretePathReference(raw) {
97
+ return preparePathReference(raw).status === "concrete";
98
+ }
@@ -0,0 +1,44 @@
1
+ import type { Finding, ScanResult, Scores, Severity } from "../../types/index.js";
2
+ export type PolicyViolationCode = "minimum-score" | "fail-on-severity" | "fail-on-rule" | "fail-on-new";
3
+ export interface PolicyOptions {
4
+ /** Fail when overall score is strictly below this value (0–100). */
5
+ minimumScore?: number;
6
+ /** Fail when any finding has this severity or higher. */
7
+ failOnSeverity?: Severity;
8
+ /** Fail when any finding matches one of these rule IDs. */
9
+ failOnRules?: string[];
10
+ /** Fail when verify reports new findings not present in the baseline. */
11
+ failOnNew?: boolean;
12
+ }
13
+ export interface PolicyViolation {
14
+ code: PolicyViolationCode;
15
+ message: string;
16
+ /** Rule IDs or finding IDs related to this violation, when applicable. */
17
+ details?: string[];
18
+ }
19
+ export interface PolicyInput {
20
+ findings: Finding[];
21
+ scores: Scores | null;
22
+ /** Count of new findings from verify; omit for scan-only. */
23
+ newFindingCount?: number;
24
+ /** When limited, --min-score cannot be treated as agent-readiness enforcement. */
25
+ agentSecurityAnalysis?: "full" | "limited";
26
+ }
27
+ export declare function severityRank(severity: Severity): number;
28
+ export declare function parseSeverityGate(value: string): Severity;
29
+ export declare function parseFailOnRules(value: string): string[];
30
+ /**
31
+ * Evaluate CI / Action policy against scan or verify results.
32
+ * Deterministic: same input → same violations in stable order.
33
+ */
34
+ export declare function evaluatePolicy(input: PolicyInput, options: PolicyOptions): PolicyViolation[];
35
+ export declare function evaluateScanPolicy(result: ScanResult, options: PolicyOptions): PolicyViolation[];
36
+ export declare function evaluateVerifyPolicy(result: {
37
+ findings?: Finding[];
38
+ after: ScanResult;
39
+ scores: Scores | null;
40
+ summary: {
41
+ new: number;
42
+ };
43
+ newFindings?: Finding[];
44
+ }, options: PolicyOptions): PolicyViolation[];
@@ -0,0 +1,120 @@
1
+ import { InvalidArgumentError } from "commander";
2
+ const SEVERITY_RANK = {
3
+ critical: 3,
4
+ warning: 2,
5
+ info: 1,
6
+ };
7
+ export function severityRank(severity) {
8
+ return SEVERITY_RANK[severity];
9
+ }
10
+ export function parseSeverityGate(value) {
11
+ const normalized = value.trim().toLowerCase();
12
+ if (normalized === "critical" || normalized === "warning" || normalized === "info") {
13
+ return normalized;
14
+ }
15
+ throw new InvalidArgumentError(`--fail-on-severity must be critical, warning, or info (got "${value}")`);
16
+ }
17
+ export function parseFailOnRules(value) {
18
+ return value
19
+ .split(",")
20
+ .map((part) => part.trim())
21
+ .filter((part) => part.length > 0);
22
+ }
23
+ /**
24
+ * Evaluate CI / Action policy against scan or verify results.
25
+ * Deterministic: same input → same violations in stable order.
26
+ */
27
+ export function evaluatePolicy(input, options) {
28
+ const violations = [];
29
+ if (options.minimumScore !== undefined) {
30
+ if (input.agentSecurityAnalysis === "limited") {
31
+ violations.push({
32
+ code: "minimum-score",
33
+ message: `CI check failed: no supported coding-agent configuration detected; cannot enforce --min-score ${options.minimumScore}`,
34
+ });
35
+ }
36
+ else if (input.scores === null) {
37
+ violations.push({
38
+ code: "minimum-score",
39
+ message: `CI check failed: readiness scores unavailable; cannot enforce --min-score ${options.minimumScore}`,
40
+ });
41
+ }
42
+ else if (input.scores.overall < options.minimumScore) {
43
+ violations.push({
44
+ code: "minimum-score",
45
+ message: `CI check failed: overall score ${input.scores.overall} is below --min-score ${options.minimumScore}`,
46
+ });
47
+ }
48
+ }
49
+ if (options.failOnSeverity !== undefined) {
50
+ const threshold = severityRank(options.failOnSeverity);
51
+ const matched = input.findings.filter((f) => severityRank(f.severity) >= threshold);
52
+ if (matched.length > 0) {
53
+ const bySeverity = countBySeverity(matched);
54
+ violations.push({
55
+ code: "fail-on-severity",
56
+ message: `CI check failed: ${matched.length} finding(s) at or above ${options.failOnSeverity} (${formatSeverityCounts(bySeverity)})`,
57
+ details: [...new Set(matched.map((f) => f.ruleId))].sort(),
58
+ });
59
+ }
60
+ }
61
+ if (options.failOnRules && options.failOnRules.length > 0) {
62
+ const wanted = new Set(options.failOnRules);
63
+ const matched = input.findings.filter((f) => wanted.has(f.ruleId));
64
+ if (matched.length > 0) {
65
+ const rules = [...new Set(matched.map((f) => f.ruleId))].sort();
66
+ violations.push({
67
+ code: "fail-on-rule",
68
+ message: `CI check failed: ${matched.length} finding(s) match fail-on-rule (${rules.join(", ")})`,
69
+ details: rules,
70
+ });
71
+ }
72
+ }
73
+ if (options.failOnNew === true) {
74
+ const newCount = input.newFindingCount ?? 0;
75
+ if (newCount > 0) {
76
+ violations.push({
77
+ code: "fail-on-new",
78
+ message: `CI check failed: verify found ${newCount} new finding(s) not present in the baseline`,
79
+ });
80
+ }
81
+ }
82
+ return violations;
83
+ }
84
+ export function evaluateScanPolicy(result, options) {
85
+ const { failOnNew: _ignored, ...scanOptions } = options;
86
+ return evaluatePolicy({
87
+ findings: result.findings,
88
+ scores: result.scores,
89
+ agentSecurityAnalysis: result.agentSecurityAnalysis,
90
+ }, scanOptions);
91
+ }
92
+ export function evaluateVerifyPolicy(result, options) {
93
+ const findings = result.after.findings;
94
+ return evaluatePolicy({
95
+ findings,
96
+ scores: result.scores,
97
+ newFindingCount: result.summary.new,
98
+ agentSecurityAnalysis: result.after.agentSecurityAnalysis,
99
+ }, options);
100
+ }
101
+ function countBySeverity(findings) {
102
+ const counts = { critical: 0, warning: 0, info: 0 };
103
+ for (const finding of findings) {
104
+ counts[finding.severity] += 1;
105
+ }
106
+ return counts;
107
+ }
108
+ function formatSeverityCounts(counts) {
109
+ const parts = [];
110
+ if (counts.critical > 0) {
111
+ parts.push(`${counts.critical} critical`);
112
+ }
113
+ if (counts.warning > 0) {
114
+ parts.push(`${counts.warning} warning`);
115
+ }
116
+ if (counts.info > 0) {
117
+ parts.push(`${counts.info} info`);
118
+ }
119
+ return parts.join(", ");
120
+ }
@@ -0,0 +1,8 @@
1
+ import type { RuleContext } from "./types.js";
2
+ /**
3
+ * Claude Code Read deny rule for a repository-relative path.
4
+ * Directories use a recursive Read(./path/**) form.
5
+ */
6
+ export declare function claudeReadDenyRule(relativePath: string, kind?: "file" | "directory"): string;
7
+ export declare function settingsTextDeniesPath(settingsText: string, relativePath: string): boolean;
8
+ export declare function claudeDeniesPath(context: RuleContext, relativePath: string): Promise<boolean>;