@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,234 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
4
+ const INSTRUCTION_CANDIDATES = [
5
+ "AGENTS.md",
6
+ "CLAUDE.md",
7
+ "GEMINI.md",
8
+ "CONVENTIONS.md",
9
+ ".cursorrules",
10
+ ".windsurfrules",
11
+ ".github/copilot-instructions.md",
12
+ ];
13
+ const IGNORE_CANDIDATES = [
14
+ ".cursorignore",
15
+ ".claudeignore",
16
+ ".codexignore",
17
+ ".geminiignore",
18
+ ".aiderignore",
19
+ ];
20
+ const OVERSIZE_BYTES = 100_000;
21
+ const PATH_REF = /(?:^|[\s`"'(])((?:\.\.?\/|[A-Za-z0-9_.-]+\/)[A-Za-z0-9_./-]+\.[A-Za-z0-9]+)(?=[\s`"'')]|$)/gm;
22
+ const AMBIGUOUS = /\b(TODO|FIXME|TBD|XXX)\b/g;
23
+ const YEAR_REF = /\b(20[0-1][0-9]|202[0-3])\b/g;
24
+ async function existsFile(root, rel) {
25
+ try {
26
+ const st = await fs.lstat(path.join(root, rel));
27
+ return st.isFile() || st.isSymbolicLink();
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ async function readText(root, rel) {
34
+ try {
35
+ return await fs.readFile(path.join(root, rel), "utf8");
36
+ }
37
+ catch {
38
+ return "";
39
+ }
40
+ }
41
+ async function fileSize(root, rel) {
42
+ try {
43
+ const st = await fs.stat(path.join(root, rel));
44
+ return st.size;
45
+ }
46
+ catch {
47
+ return 0;
48
+ }
49
+ }
50
+ function hasContradictoryTone(a, b) {
51
+ const always = /\balways\b/i;
52
+ const never = /\bnever\b/i;
53
+ return (always.test(a) && never.test(b)) || (never.test(a) && always.test(b));
54
+ }
55
+ function extractTerms(text) {
56
+ const terms = new Set();
57
+ for (const m of text.matchAll(/\b(repo|repository|codebase|project|workspace)\b/gi)) {
58
+ terms.add(m[1].toLowerCase());
59
+ }
60
+ return terms;
61
+ }
62
+ export async function analyzeContextHealth(rootInput) {
63
+ const root = resolveRepoRoot(rootInput);
64
+ const instructionFiles = [];
65
+ const ignoreFiles = [];
66
+ const conflicts = [];
67
+ for (const rel of INSTRUCTION_CANDIDATES) {
68
+ if (await existsFile(root, rel)) {
69
+ instructionFiles.push(rel);
70
+ }
71
+ }
72
+ try {
73
+ const instructionsDir = path.join(root, ".github", "instructions");
74
+ const entries = await fs.readdir(instructionsDir);
75
+ for (const entry of entries) {
76
+ if (entry.endsWith(".md")) {
77
+ instructionFiles.push(toPosixRelative(root, path.join(instructionsDir, entry)));
78
+ }
79
+ }
80
+ }
81
+ catch {
82
+ // optional
83
+ }
84
+ for (const rel of IGNORE_CANDIDATES) {
85
+ if (await existsFile(root, rel)) {
86
+ ignoreFiles.push(rel);
87
+ }
88
+ }
89
+ if (instructionFiles.length >= 2) {
90
+ conflicts.push({
91
+ kind: "duplicate-instruction-surface",
92
+ severity: "info",
93
+ files: [...instructionFiles],
94
+ message: "Multiple agent instruction surfaces detected; prefer one primary source of truth per agent.",
95
+ confidence: "high",
96
+ });
97
+ }
98
+ const currentYear = new Date().getUTCFullYear();
99
+ const termUnion = new Set();
100
+ const termByFile = new Map();
101
+ for (const rel of instructionFiles) {
102
+ const text = await readText(root, rel);
103
+ if (text.trim().length === 0) {
104
+ conflicts.push({
105
+ kind: "empty-instruction-file",
106
+ severity: "warning",
107
+ files: [rel],
108
+ message: `Instruction file is empty: ${rel}`,
109
+ confidence: "high",
110
+ });
111
+ continue;
112
+ }
113
+ const size = await fileSize(root, rel);
114
+ if (size > OVERSIZE_BYTES) {
115
+ conflicts.push({
116
+ kind: "oversized-instruction-file",
117
+ severity: "warning",
118
+ files: [rel],
119
+ message: `Instruction file exceeds ${OVERSIZE_BYTES} bytes (${size}): ${rel}`,
120
+ confidence: "high",
121
+ });
122
+ }
123
+ AMBIGUOUS.lastIndex = 0;
124
+ const ambiguousHits = text.match(AMBIGUOUS);
125
+ if (ambiguousHits && ambiguousHits.length > 0) {
126
+ conflicts.push({
127
+ kind: "ambiguous-marker",
128
+ severity: "info",
129
+ files: [rel],
130
+ message: `Ambiguous markers (${[...new Set(ambiguousHits)].join(", ")}) in ${rel}`,
131
+ confidence: "medium",
132
+ });
133
+ }
134
+ YEAR_REF.lastIndex = 0;
135
+ for (const m of text.matchAll(YEAR_REF)) {
136
+ const year = Number(m[1]);
137
+ if (Number.isFinite(year) && year < currentYear - 1) {
138
+ conflicts.push({
139
+ kind: "outdated-year-reference",
140
+ severity: "info",
141
+ files: [rel],
142
+ message: `Possibly outdated year reference ${year} in ${rel}`,
143
+ confidence: "low",
144
+ });
145
+ break;
146
+ }
147
+ }
148
+ PATH_REF.lastIndex = 0;
149
+ const seenRefs = new Set();
150
+ for (const m of text.matchAll(PATH_REF)) {
151
+ const ref = (m[1] ?? "").replace(/^\.\//, "");
152
+ if (!ref || seenRefs.has(ref) || ref.includes("http"))
153
+ continue;
154
+ seenRefs.add(ref);
155
+ if (ref.startsWith("node_modules/"))
156
+ continue;
157
+ const exists = await existsFile(root, ref);
158
+ if (!exists) {
159
+ conflicts.push({
160
+ kind: "invalid-path-reference",
161
+ severity: "warning",
162
+ files: [rel],
163
+ message: `Referenced path not found: ${ref} (from ${rel})`,
164
+ confidence: "medium",
165
+ });
166
+ }
167
+ }
168
+ const terms = extractTerms(text);
169
+ termByFile.set(rel, terms);
170
+ for (const t of terms)
171
+ termUnion.add(t);
172
+ }
173
+ if (termUnion.has("repo") && termUnion.has("repository") && termByFile.size >= 2) {
174
+ const filesUsing = [];
175
+ for (const [file, terms] of termByFile) {
176
+ if (terms.has("repo") || terms.has("repository"))
177
+ filesUsing.push(file);
178
+ }
179
+ if (filesUsing.length >= 2) {
180
+ conflicts.push({
181
+ kind: "terminology-inconsistency",
182
+ severity: "info",
183
+ files: filesUsing.sort(),
184
+ message: 'Mixed terminology "repo" vs "repository" across instruction files',
185
+ confidence: "low",
186
+ });
187
+ }
188
+ }
189
+ for (let i = 0; i < instructionFiles.length; i += 1) {
190
+ for (let j = i + 1; j < instructionFiles.length; j += 1) {
191
+ const a = instructionFiles[i];
192
+ const b = instructionFiles[j];
193
+ const textA = await readText(root, a);
194
+ const textB = await readText(root, b);
195
+ if (hasContradictoryTone(textA, textB)) {
196
+ conflicts.push({
197
+ kind: "contradictory-tone",
198
+ severity: "warning",
199
+ files: [a, b],
200
+ message: `Potential always/never contradiction between ${a} and ${b}`,
201
+ confidence: "medium",
202
+ });
203
+ }
204
+ }
205
+ }
206
+ if (ignoreFiles.length >= 2) {
207
+ conflicts.push({
208
+ kind: "overlapping-ignore",
209
+ severity: "info",
210
+ files: [...ignoreFiles],
211
+ message: "Multiple ignore surfaces present; keep patterns aligned across agents.",
212
+ confidence: "high",
213
+ });
214
+ }
215
+ // Empty ignore files are unused-candidate signals
216
+ for (const rel of ignoreFiles) {
217
+ const text = await readText(root, rel);
218
+ if (text.trim().length === 0) {
219
+ conflicts.push({
220
+ kind: "unused-ignore-candidate",
221
+ severity: "info",
222
+ files: [rel],
223
+ message: `Ignore file exists but is empty: ${rel}`,
224
+ confidence: "medium",
225
+ });
226
+ }
227
+ }
228
+ return {
229
+ root,
230
+ conflicts,
231
+ instructionFiles: instructionFiles.sort(),
232
+ ignoreFiles: ignoreFiles.sort(),
233
+ };
234
+ }
@@ -2,13 +2,30 @@ import type { FixApplyResult, FixPlan } from "./types.js";
2
2
  import { previewClaudeSettingsActions, readClaudeSettings } from "./writers/claude-settings.js";
3
3
  import { previewCodexConfigActions, readCodexConfig } from "./writers/codex-config.js";
4
4
  import { previewCursorignoreActions, readCursorignore } from "./writers/cursorignore.js";
5
+ import { previewSimpleIgnoreActions, readSimpleIgnore } from "./writers/simple-ignore.js";
6
+ /** Supported options for Safe Fix apply / dry-run. */
7
+ export interface ApplyFixPlanOptions {
8
+ dryRun: boolean;
9
+ }
10
+ /**
11
+ * Internal test-only options. Not a CLI feature and not part of the supported
12
+ * public consumer API — use only from unit tests.
13
+ */
14
+ export interface ApplyFixPlanInternalOptions extends ApplyFixPlanOptions {
15
+ /** Inject a failure after earlier writers succeed (partial-apply tests). */
16
+ simulateWriteFailureAt?: string;
17
+ }
5
18
  /**
6
19
  * Apply (or dry-run) a fix plan.
7
- * Safe writers: `.cursorignore`, Claude settings deny, Codex config.toml deny.
20
+ * Safe writers: `.cursorignore`, Claude settings deny, Codex config.toml deny,
21
+ * `.geminiignore`, `.aiderignore`.
22
+ *
23
+ * All planned targets are preflighted before any write. Writes still run
24
+ * sequentially; if a later write fails, earlier files may already be updated
25
+ * (`partial: true` on the result).
8
26
  */
9
- export declare function applyFixPlan(plan: FixPlan, options: {
10
- dryRun: boolean;
11
- }): Promise<FixApplyResult>;
27
+ export declare function applyFixPlan(plan: FixPlan, options: ApplyFixPlanOptions | ApplyFixPlanInternalOptions): Promise<FixApplyResult>;
12
28
  export { previewCursorignoreActions, readCursorignore };
13
29
  export { previewClaudeSettingsActions, readClaudeSettings };
14
30
  export { previewCodexConfigActions, readCodexConfig };
31
+ export { previewSimpleIgnoreActions, readSimpleIgnore };
@@ -1,47 +1,147 @@
1
+ import { assertTargetsUnchangedSinceBackup, createFixBackup } from "./backup.js";
2
+ import { preflightSafeFixTargets } from "./safe-target.js";
1
3
  import { previewClaudeSettingsActions, readClaudeSettings, writeClaudeSettings, } from "./writers/claude-settings.js";
2
4
  import { previewCodexConfigActions, readCodexConfig, writeCodexConfig, } from "./writers/codex-config.js";
3
5
  import { previewCursorignoreActions, readCursorignore, writeCursorignore, } from "./writers/cursorignore.js";
6
+ import { previewSimpleIgnoreActions, readSimpleIgnore, writeSimpleIgnore, } from "./writers/simple-ignore.js";
4
7
  /**
5
8
  * Apply (or dry-run) a fix plan.
6
- * Safe writers: `.cursorignore`, Claude settings deny, Codex config.toml deny.
9
+ * Safe writers: `.cursorignore`, Claude settings deny, Codex config.toml deny,
10
+ * `.geminiignore`, `.aiderignore`.
11
+ *
12
+ * All planned targets are preflighted before any write. Writes still run
13
+ * sequentially; if a later write fails, earlier files may already be updated
14
+ * (`partial: true` on the result).
7
15
  */
8
16
  export async function applyFixPlan(plan, options) {
17
+ const simulateWriteFailureAt = "simulateWriteFailureAt" in options ? options.simulateWriteFailureAt : undefined;
9
18
  const cursorContent = await readCursorignore(plan.root);
10
19
  const claudeContent = await readClaudeSettings(plan.root);
11
20
  const codexContent = await readCodexConfig(plan.root);
21
+ const geminiContent = await readSimpleIgnore(plan.root, ".geminiignore");
22
+ const aiderContent = await readSimpleIgnore(plan.root, ".aiderignore");
12
23
  const cursorPreview = previewCursorignoreActions(cursorContent, plan.actions);
13
24
  const claudePreview = previewClaudeSettingsActions(claudeContent, plan.actions);
14
25
  const codexPreview = previewCodexConfigActions(codexContent, plan.actions);
15
- const writtenFiles = [];
16
- const changedFiles = [];
26
+ const geminiPreview = previewSimpleIgnoreActions(geminiContent, plan.actions, {
27
+ agent: "gemini-cli",
28
+ targetRelativePath: ".geminiignore",
29
+ });
30
+ const aiderPreview = previewSimpleIgnoreActions(aiderContent, plan.actions, {
31
+ agent: "aider",
32
+ targetRelativePath: ".aiderignore",
33
+ });
34
+ const steps = [];
17
35
  if (cursorPreview) {
18
- changedFiles.push(cursorPreview.targetRelativePath);
19
- if (!options.dryRun) {
20
- await writeCursorignore(plan.root, cursorPreview.after);
21
- writtenFiles.push(cursorPreview.targetRelativePath);
22
- }
36
+ steps.push({
37
+ targetRelativePath: cursorPreview.targetRelativePath,
38
+ write: async () => writeCursorignore(plan.root, cursorPreview.after),
39
+ });
23
40
  }
24
41
  if (claudePreview) {
25
- changedFiles.push(claudePreview.targetRelativePath);
26
- if (!options.dryRun) {
27
- await writeClaudeSettings(plan.root, claudePreview.after);
28
- writtenFiles.push(claudePreview.targetRelativePath);
29
- }
42
+ steps.push({
43
+ targetRelativePath: claudePreview.targetRelativePath,
44
+ write: async () => writeClaudeSettings(plan.root, claudePreview.after),
45
+ });
30
46
  }
31
47
  if (codexPreview) {
32
- changedFiles.push(codexPreview.targetRelativePath);
33
- if (!options.dryRun) {
34
- await writeCodexConfig(plan.root, codexPreview.after);
35
- writtenFiles.push(codexPreview.targetRelativePath);
48
+ steps.push({
49
+ targetRelativePath: codexPreview.targetRelativePath,
50
+ write: async () => writeCodexConfig(plan.root, codexPreview.after),
51
+ });
52
+ }
53
+ if (geminiPreview) {
54
+ steps.push({
55
+ targetRelativePath: geminiPreview.targetRelativePath,
56
+ write: async () => writeSimpleIgnore(plan.root, ".geminiignore", geminiPreview.after),
57
+ });
58
+ }
59
+ if (aiderPreview) {
60
+ steps.push({
61
+ targetRelativePath: aiderPreview.targetRelativePath,
62
+ write: async () => writeSimpleIgnore(plan.root, ".aiderignore", aiderPreview.after),
63
+ });
64
+ }
65
+ const changedFiles = steps.map((s) => s.targetRelativePath);
66
+ try {
67
+ await preflightSafeFixTargets(plan.root, steps.map((s) => s.targetRelativePath));
68
+ }
69
+ catch (error) {
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ return {
72
+ plan,
73
+ dryRun: options.dryRun,
74
+ writtenFiles: [],
75
+ changedFiles,
76
+ partial: false,
77
+ ...(steps[0]?.targetRelativePath ? { failedTarget: steps[0].targetRelativePath } : {}),
78
+ error: message,
79
+ };
80
+ }
81
+ if (options.dryRun) {
82
+ return {
83
+ plan,
84
+ dryRun: true,
85
+ writtenFiles: [],
86
+ changedFiles,
87
+ };
88
+ }
89
+ let auditId;
90
+ let backupRecord;
91
+ try {
92
+ backupRecord = await createFixBackup({
93
+ root: plan.root,
94
+ relativePaths: steps.map((s) => s.targetRelativePath),
95
+ note: "safe-fix-apply",
96
+ });
97
+ auditId = backupRecord.id;
98
+ }
99
+ catch (error) {
100
+ const message = error instanceof Error ? error.message : String(error);
101
+ return {
102
+ plan,
103
+ dryRun: false,
104
+ writtenFiles: [],
105
+ changedFiles,
106
+ partial: false,
107
+ error: `backup failed before apply: ${message}`,
108
+ };
109
+ }
110
+ const writtenFiles = [];
111
+ for (const step of steps) {
112
+ try {
113
+ if (backupRecord) {
114
+ await assertTargetsUnchangedSinceBackup(plan.root, backupRecord, [step.targetRelativePath]);
115
+ }
116
+ if (simulateWriteFailureAt && simulateWriteFailureAt === step.targetRelativePath) {
117
+ throw new Error("simulated later writer failure");
118
+ }
119
+ await step.write();
120
+ writtenFiles.push(step.targetRelativePath);
121
+ }
122
+ catch (error) {
123
+ const message = error instanceof Error ? error.message : String(error);
124
+ return {
125
+ plan,
126
+ dryRun: false,
127
+ writtenFiles,
128
+ changedFiles,
129
+ partial: writtenFiles.length > 0,
130
+ failedTarget: step.targetRelativePath,
131
+ error: message,
132
+ ...(auditId ? { auditId } : {}),
133
+ };
36
134
  }
37
135
  }
38
136
  return {
39
137
  plan,
40
- dryRun: options.dryRun,
138
+ dryRun: false,
41
139
  writtenFiles,
42
140
  changedFiles,
141
+ ...(auditId ? { auditId } : {}),
43
142
  };
44
143
  }
45
144
  export { previewCursorignoreActions, readCursorignore };
46
145
  export { previewClaudeSettingsActions, readClaudeSettings };
47
146
  export { previewCodexConfigActions, readCodexConfig };
147
+ export { previewSimpleIgnoreActions, readSimpleIgnore };
@@ -0,0 +1,30 @@
1
+ export interface FixBackupEntry {
2
+ relativePath: string;
3
+ existed: boolean;
4
+ backupRelativePath: string | null;
5
+ sha256Before: string | null;
6
+ mtimeMsBefore: number | null;
7
+ }
8
+ export interface FixAuditRecord {
9
+ id: string;
10
+ createdAt: string;
11
+ root: string;
12
+ mode: "apply" | "undo";
13
+ entries: FixBackupEntry[];
14
+ note?: string;
15
+ }
16
+ export declare function createFixBackup(options: {
17
+ root: string;
18
+ relativePaths: string[];
19
+ note?: string;
20
+ }): Promise<FixAuditRecord>;
21
+ export declare function listFixAudits(rootInput: string): Promise<FixAuditRecord[]>;
22
+ export declare function undoFix(options: {
23
+ root: string;
24
+ auditId: string;
25
+ }): Promise<FixAuditRecord>;
26
+ /**
27
+ * Ensure a target still matches the backup snapshot (detect concurrent modification).
28
+ * For files that did not exist at backup time, refuses if the path now exists.
29
+ */
30
+ export declare function assertTargetsUnchangedSinceBackup(rootInput: string, audit: FixAuditRecord, relativePaths: string[]): Promise<void>;
@@ -0,0 +1,193 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { atomicWriteTextFile } from "../../utils/fs.js";
5
+ import { isPathInsideRoot, resolveRepoRoot } from "../../utils/path.js";
6
+ function resolveSafePath(root, relativePath) {
7
+ const absolute = path.resolve(root, relativePath);
8
+ if (!isPathInsideRoot(root, absolute)) {
9
+ throw new Error(`path escapes repository root: ${relativePath}`);
10
+ }
11
+ return absolute;
12
+ }
13
+ function auditDir(root) {
14
+ return path.join(root, ".agentdoctor", "fix-audit");
15
+ }
16
+ function backupsDir(root, auditId) {
17
+ return path.join(auditDir(root), auditId, "backups");
18
+ }
19
+ async function sha256File(absolute) {
20
+ try {
21
+ const buf = await fs.readFile(absolute);
22
+ return createHash("sha256").update(buf).digest("hex");
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ export async function createFixBackup(options) {
29
+ const root = resolveRepoRoot(options.root);
30
+ const id = randomUUID();
31
+ const createdAt = new Date().toISOString();
32
+ const backupRoot = backupsDir(root, id);
33
+ await fs.mkdir(backupRoot, { recursive: true });
34
+ const entries = [];
35
+ for (const rel of options.relativePaths) {
36
+ const absolute = resolveSafePath(root, rel);
37
+ let existed = false;
38
+ let backupRelativePath = null;
39
+ let sha = null;
40
+ let mtimeMsBefore = null;
41
+ try {
42
+ const st = await fs.lstat(absolute);
43
+ if (st.isSymbolicLink()) {
44
+ throw new Error(`refusing to backup symlink: ${rel}`);
45
+ }
46
+ if (st.isDirectory()) {
47
+ throw new Error(`refusing to backup directory: ${rel}`);
48
+ }
49
+ existed = true;
50
+ mtimeMsBefore = st.mtimeMs;
51
+ sha = await sha256File(absolute);
52
+ const dest = path.join(backupRoot, rel);
53
+ await fs.mkdir(path.dirname(dest), { recursive: true });
54
+ await fs.copyFile(absolute, dest);
55
+ backupRelativePath = path.relative(root, dest).split(path.sep).join("/");
56
+ }
57
+ catch (error) {
58
+ const code = error.code;
59
+ if (code === "ENOENT") {
60
+ existed = false;
61
+ }
62
+ else {
63
+ throw error;
64
+ }
65
+ }
66
+ entries.push({
67
+ relativePath: rel,
68
+ existed,
69
+ backupRelativePath,
70
+ sha256Before: sha,
71
+ mtimeMsBefore,
72
+ });
73
+ }
74
+ const record = {
75
+ id,
76
+ createdAt,
77
+ root,
78
+ mode: "apply",
79
+ entries,
80
+ ...(options.note ? { note: options.note } : {}),
81
+ };
82
+ await atomicWriteTextFile(path.join(auditDir(root), id, "audit.json"), `${JSON.stringify(record, null, 2)}\n`);
83
+ return record;
84
+ }
85
+ export async function listFixAudits(rootInput) {
86
+ const root = resolveRepoRoot(rootInput);
87
+ const dir = auditDir(root);
88
+ let names;
89
+ try {
90
+ names = await fs.readdir(dir);
91
+ }
92
+ catch {
93
+ return [];
94
+ }
95
+ const records = [];
96
+ for (const name of names) {
97
+ try {
98
+ const raw = await fs.readFile(path.join(dir, name, "audit.json"), "utf8");
99
+ records.push(JSON.parse(raw));
100
+ }
101
+ catch {
102
+ // skip corrupt
103
+ }
104
+ }
105
+ return records.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
106
+ }
107
+ export async function undoFix(options) {
108
+ const root = resolveRepoRoot(options.root);
109
+ const auditPath = path.join(auditDir(root), options.auditId, "audit.json");
110
+ const raw = await fs.readFile(auditPath, "utf8");
111
+ const original = JSON.parse(raw);
112
+ const undoEntries = [];
113
+ for (const entry of original.entries) {
114
+ const target = resolveSafePath(root, entry.relativePath);
115
+ if (entry.existed && entry.backupRelativePath) {
116
+ const backupAbs = resolveSafePath(root, entry.backupRelativePath);
117
+ await fs.mkdir(path.dirname(target), { recursive: true });
118
+ await fs.copyFile(backupAbs, target);
119
+ undoEntries.push({
120
+ relativePath: entry.relativePath,
121
+ existed: true,
122
+ backupRelativePath: entry.backupRelativePath,
123
+ sha256Before: await sha256File(target),
124
+ mtimeMsBefore: null,
125
+ });
126
+ }
127
+ else if (!entry.existed) {
128
+ try {
129
+ await fs.unlink(target);
130
+ }
131
+ catch (error) {
132
+ const code = error.code;
133
+ if (code !== "ENOENT")
134
+ throw error;
135
+ }
136
+ undoEntries.push({
137
+ relativePath: entry.relativePath,
138
+ existed: false,
139
+ backupRelativePath: null,
140
+ sha256Before: null,
141
+ mtimeMsBefore: null,
142
+ });
143
+ }
144
+ }
145
+ const undoRecord = {
146
+ id: randomUUID(),
147
+ createdAt: new Date().toISOString(),
148
+ root,
149
+ mode: "undo",
150
+ entries: undoEntries,
151
+ note: `undo of ${original.id}`,
152
+ };
153
+ await fs.mkdir(path.join(auditDir(root), undoRecord.id), { recursive: true });
154
+ await atomicWriteTextFile(path.join(auditDir(root), undoRecord.id, "audit.json"), `${JSON.stringify(undoRecord, null, 2)}\n`);
155
+ return undoRecord;
156
+ }
157
+ /**
158
+ * Ensure a target still matches the backup snapshot (detect concurrent modification).
159
+ * For files that did not exist at backup time, refuses if the path now exists.
160
+ */
161
+ export async function assertTargetsUnchangedSinceBackup(rootInput, audit, relativePaths) {
162
+ const root = resolveRepoRoot(rootInput);
163
+ const byPath = new Map(audit.entries.map((e) => [e.relativePath, e]));
164
+ for (const rel of relativePaths) {
165
+ const entry = byPath.get(rel);
166
+ if (!entry)
167
+ continue;
168
+ const absolute = resolveSafePath(root, rel);
169
+ try {
170
+ const st = await fs.lstat(absolute);
171
+ if (st.isSymbolicLink() || st.isDirectory()) {
172
+ throw new Error(`concurrent modification risk on non-file target: ${rel}`);
173
+ }
174
+ if (!entry.existed) {
175
+ throw new Error(`concurrent modification detected: ${rel} was created after backup (refusing write)`);
176
+ }
177
+ const sha = await sha256File(absolute);
178
+ if (entry.sha256Before && sha !== entry.sha256Before) {
179
+ throw new Error(`concurrent modification detected: ${rel} changed since backup (hash mismatch)`);
180
+ }
181
+ }
182
+ catch (error) {
183
+ const code = error.code;
184
+ if (code === "ENOENT") {
185
+ if (entry.existed) {
186
+ throw new Error(`concurrent modification detected: ${rel} deleted since backup (refusing write)`);
187
+ }
188
+ continue;
189
+ }
190
+ throw error;
191
+ }
192
+ }
193
+ }
@@ -2,8 +2,11 @@ import type { ScanResult } from "../../types/index.js";
2
2
  import type { FixPlan } from "./types.js";
3
3
  /**
4
4
  * Build a fix plan from a scan result.
5
- * Safe context exclusions: Cursor `.cursorignore`, Claude Code Read deny, Codex filesystem deny.
5
+ * Safe context exclusions: Cursor `.cursorignore`, Claude Code Read deny, Codex filesystem deny,
6
+ * Gemini CLI `.geminiignore`, Aider `.aiderignore`.
6
7
  */
7
8
  export declare function buildFixPlan(result: ScanResult): Promise<FixPlan>;
8
- /** Patterns that would be newly appended given current file contents. */
9
- export declare function missingPatternsForCursorignore(currentContent: string | null, patterns: string[]): string[];
9
+ /** Patterns that would be newly appended given current ignore-file contents. */
10
+ export declare function missingPatternsForIgnoreFile(currentContent: string | null, patterns: string[]): string[];
11
+ /** @deprecated Prefer missingPatternsForIgnoreFile */
12
+ export declare const missingPatternsForCursorignore: typeof missingPatternsForIgnoreFile;