@praneeth_54/agentdoctor 0.3.0-beta → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +76 -1
  2. package/README.md +60 -30
  3. package/dist/agents/inspect.js +58 -51
  4. package/dist/cli/commands/explain.js +1 -1
  5. package/dist/cli/commands/fix.js +22 -6
  6. package/dist/cli/commands/scan.d.ts +5 -0
  7. package/dist/cli/commands/scan.js +25 -6
  8. package/dist/cli/commands/verify.d.ts +6 -0
  9. package/dist/cli/commands/verify.js +25 -11
  10. package/dist/cli/program.js +48 -7
  11. package/dist/constants.d.ts +1 -1
  12. package/dist/constants.js +1 -1
  13. package/dist/core/fix/apply.d.ts +5 -1
  14. package/dist/core/fix/apply.js +34 -14
  15. package/dist/core/fix/plan.d.ts +1 -1
  16. package/dist/core/fix/plan.js +110 -3
  17. package/dist/core/fix/render.d.ts +2 -0
  18. package/dist/core/fix/render.js +57 -9
  19. package/dist/core/fix/run.js +5 -1
  20. package/dist/core/fix/types.d.ts +1 -1
  21. package/dist/core/fix/types.js +5 -1
  22. package/dist/core/fix/writers/claude-settings.d.ts +16 -0
  23. package/dist/core/fix/writers/claude-settings.js +100 -0
  24. package/dist/core/fix/writers/codex-config.d.ts +22 -0
  25. package/dist/core/fix/writers/codex-config.js +183 -0
  26. package/dist/core/fix/writers/cursorignore.js +2 -8
  27. package/dist/core/path-resolution/index.d.ts +5 -0
  28. package/dist/core/path-resolution/index.js +5 -0
  29. package/dist/core/path-resolution/prepare.d.ts +33 -0
  30. package/dist/core/path-resolution/prepare.js +98 -0
  31. package/dist/core/policy/evaluate.d.ts +44 -0
  32. package/dist/core/policy/evaluate.js +120 -0
  33. package/dist/core/rules/claude-deny.d.ts +8 -0
  34. package/dist/core/rules/claude-deny.js +39 -0
  35. package/dist/core/rules/codex-deny.d.ts +5 -0
  36. package/dist/core/rules/codex-deny.js +42 -0
  37. package/dist/core/rules/context/generated-directory.js +43 -9
  38. package/dist/core/rules/context/large-log-file.js +27 -10
  39. package/dist/core/rules/ignore.js +2 -5
  40. package/dist/core/rules/instructions/missing-path-reference.d.ts +5 -1
  41. package/dist/core/rules/instructions/missing-path-reference.js +24 -6
  42. package/dist/core/rules/path-kind.d.ts +15 -2
  43. package/dist/core/rules/path-kind.js +113 -10
  44. package/dist/core/rules/security/env-file-exposure.js +9 -24
  45. package/dist/core/rules/text-cache.js +46 -40
  46. package/dist/discovery/files.js +7 -2
  47. package/dist/discovery/log-like.d.ts +2 -0
  48. package/dist/discovery/log-like.js +7 -0
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.js +1 -0
  51. package/dist/reporters/github/annotations.d.ts +8 -0
  52. package/dist/reporters/github/annotations.js +42 -0
  53. package/dist/reporters/github/emit.d.ts +23 -0
  54. package/dist/reporters/github/emit.js +31 -0
  55. package/dist/reporters/github/summary.d.ts +20 -0
  56. package/dist/reporters/github/summary.js +111 -0
  57. package/dist/reporters/terminal/report.d.ts +5 -0
  58. package/dist/reporters/terminal/report.js +68 -5
  59. package/dist/reporters/verify/terminal.d.ts +4 -0
  60. package/dist/reporters/verify/terminal.js +26 -0
  61. package/dist/utils/fs.d.ts +5 -0
  62. package/dist/utils/fs.js +34 -0
  63. package/dist/utils/path.d.ts +5 -0
  64. package/dist/utils/path.js +16 -0
  65. package/package.json +11 -1
@@ -1,15 +1,16 @@
1
- import { Command } from "commander";
1
+ import { Command, InvalidArgumentError } from "commander";
2
2
  import { PACKAGE_VERSION } from "../constants.js";
3
+ import { parseFailOnRules, parseSeverityGate } from "../core/policy/evaluate.js";
4
+ import { EXIT_CODES } from "../types/index.js";
3
5
  import { runDoctorCommand } from "./commands/doctor.js";
4
6
  import { runExplainCommand } from "./commands/explain.js";
5
7
  import { runFixCommand } from "./commands/fix.js";
6
8
  import { resolveTargetArgument, runScanCommand } from "./commands/scan.js";
7
9
  import { runVerifyCommand } from "./commands/verify.js";
8
- import { EXIT_CODES } from "../types/index.js";
9
10
  function parseMinScore(value) {
10
11
  const parsed = Number(value);
11
12
  if (!Number.isFinite(parsed) || parsed < 0 || parsed > 100) {
12
- throw new Error("--min-score must be a number between 0 and 100");
13
+ throw new InvalidArgumentError("--min-score must be a number between 0 and 100");
13
14
  }
14
15
  return parsed;
15
16
  }
@@ -22,6 +23,24 @@ function readMinScore(options) {
22
23
  }
23
24
  return undefined;
24
25
  }
26
+ function collectFailOnRules(value, previous) {
27
+ return [...previous, ...parseFailOnRules(value)];
28
+ }
29
+ function readFailOnSeverity(options) {
30
+ if (typeof options.failOnSeverity !== "string" || options.failOnSeverity.length === 0) {
31
+ return undefined;
32
+ }
33
+ return parseSeverityGate(options.failOnSeverity);
34
+ }
35
+ function readFailOnRules(options) {
36
+ if (Array.isArray(options.failOnRule)) {
37
+ return options.failOnRule.filter((item) => typeof item === "string");
38
+ }
39
+ if (typeof options.failOnRule === "string") {
40
+ return parseFailOnRules(options.failOnRule);
41
+ }
42
+ return [];
43
+ }
25
44
  /**
26
45
  * Scan flags are declared on both the root program and the `scan` subcommand
27
46
  * so `--help` stays accurate. Commander stores overlapping flags on the parent
@@ -30,19 +49,29 @@ function readMinScore(options) {
30
49
  function addScanOptions(command) {
31
50
  return command
32
51
  .option("--json", "Emit machine-readable JSON (no decorative output)", false)
33
- .option("--ci", "CI mode (non-interactive; report-only unless --min-score is set)", false)
52
+ .option("--ci", "CI mode: exit 1 when any critical finding exists (override with --fail-on-severity)", false)
34
53
  .option("--verbose", "Show timing and extra diagnostics", false)
35
- .option("--min-score <number>", "Exit 1 when overall readiness score is below this (0-100)", parseMinScore);
54
+ .option("--min-score <number>", "Exit 1 when overall readiness score is below this (0-100)", parseMinScore)
55
+ .option("--fail-on-severity <level>", "Exit 1 when any finding has this severity or higher (critical|warning|info)", parseSeverityGate)
56
+ .option("--fail-on-rule <id>", "Exit 1 when a finding matches this rule id (repeatable or comma-separated)", collectFailOnRules, [])
57
+ .option("--summary", "Write a GitHub Actions step summary when GITHUB_STEP_SUMMARY is set", false)
58
+ .option("--annotations", "Emit GitHub Actions annotations for findings (stderr)", false);
36
59
  }
37
60
  async function runScanFromCli(pathArg, command) {
38
61
  const options = command.optsWithGlobals();
39
62
  const minScore = readMinScore(options);
63
+ const failOnSeverity = readFailOnSeverity(options);
64
+ const failOnRules = readFailOnRules(options);
40
65
  const code = await runScanCommand({
41
66
  targetPath: resolveTargetArgument(pathArg),
42
67
  json: Boolean(options.json),
43
68
  ci: Boolean(options.ci),
44
69
  verbose: Boolean(options.verbose),
70
+ summary: Boolean(options.summary),
71
+ annotations: Boolean(options.annotations),
45
72
  ...(minScore !== undefined ? { minScore } : {}),
73
+ ...(failOnSeverity !== undefined ? { failOnSeverity } : {}),
74
+ ...(failOnRules.length > 0 ? { failOnRules } : {}),
46
75
  });
47
76
  process.exitCode = code;
48
77
  }
@@ -63,7 +92,7 @@ export function createProgram() {
63
92
  });
64
93
  program
65
94
  .command("fix")
66
- .description("Apply safe automatic fixes (Cursor .cursorignore for safe context findings)")
95
+ .description("Apply safe automatic fixes (Cursor .cursorignore, Claude Code Read deny, and Codex filesystem deny for safe context findings)")
67
96
  .argument("[path]", "Repository path (default: current directory)")
68
97
  .option("--dry-run", "Show proposed fixes without writing files", false)
69
98
  .option("-y, --yes", "Skip confirmation prompts", false)
@@ -80,20 +109,32 @@ export function createProgram() {
80
109
  .description("Re-scan and compare against a prior scan JSON baseline (Scan → Fix → Verify)")
81
110
  .argument("[path]", "Repository path (default: current directory)")
82
111
  .option("--json", "Emit machine-readable JSON", false)
83
- .option("--ci", "CI mode: exit 1 when new findings appear (also honors --min-score)", false)
112
+ .option("--ci", "CI mode: exit 1 when new findings appear (also honors other policy flags)", false)
113
+ .option("--fail-on-new", "Exit 1 when new findings appear relative to the baseline", false)
84
114
  .option("--verbose", "Show timing and extra diagnostics", false)
85
115
  .option("--baseline <file>", "Prior scan JSON report (default: agentdoctor-report.json or .agentdoctor-baseline.json)")
86
116
  .option("--min-score <number>", "Exit 1 when overall readiness score is below this (0-100)", parseMinScore)
117
+ .option("--fail-on-severity <level>", "Exit 1 when any finding has this severity or higher (critical|warning|info)", parseSeverityGate)
118
+ .option("--fail-on-rule <id>", "Exit 1 when a finding matches this rule id (repeatable or comma-separated)", collectFailOnRules, [])
119
+ .option("--summary", "Write a GitHub Actions step summary when GITHUB_STEP_SUMMARY is set", false)
120
+ .option("--annotations", "Emit GitHub Actions annotations for findings (stderr)", false)
87
121
  .action(async (pathArg, _options, command) => {
88
122
  const options = command.optsWithGlobals();
89
123
  const minScore = readMinScore(options);
124
+ const failOnSeverity = readFailOnSeverity(options);
125
+ const failOnRules = readFailOnRules(options);
90
126
  const code = await runVerifyCommand({
91
127
  targetPath: resolveTargetArgument(pathArg),
92
128
  json: Boolean(options.json),
93
129
  ci: Boolean(options.ci),
130
+ failOnNew: Boolean(options.failOnNew),
94
131
  verbose: Boolean(options.verbose),
132
+ summary: Boolean(options.summary),
133
+ annotations: Boolean(options.annotations),
95
134
  ...(typeof options.baseline === "string" ? { baselinePath: options.baseline } : {}),
96
135
  ...(minScore !== undefined ? { minScore } : {}),
136
+ ...(failOnSeverity !== undefined ? { failOnSeverity } : {}),
137
+ ...(failOnRules.length > 0 ? { failOnRules } : {}),
97
138
  });
98
139
  process.exitCode = code;
99
140
  });
@@ -1,4 +1,4 @@
1
- export declare const PACKAGE_VERSION = "0.3.0-beta";
1
+ export declare const PACKAGE_VERSION = "1.0.0";
2
2
  export declare const DEFAULT_MAX_FILE_SIZE_BYTES: number;
3
3
  /** Directories skipped during normal discovery (unless a rule needs them later). */
4
4
  export declare const DEFAULT_IGNORE_DIRECTORIES: Set<string>;
package/dist/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- export const PACKAGE_VERSION = "0.3.0-beta";
1
+ export const PACKAGE_VERSION = "1.0.0";
2
2
  export const DEFAULT_MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024; // 2 MiB
3
3
  /** Directories skipped during normal discovery (unless a rule needs them later). */
4
4
  export const DEFAULT_IGNORE_DIRECTORIES = new Set([
@@ -1,10 +1,14 @@
1
1
  import type { FixApplyResult, FixPlan } from "./types.js";
2
+ import { previewClaudeSettingsActions, readClaudeSettings } from "./writers/claude-settings.js";
3
+ import { previewCodexConfigActions, readCodexConfig } from "./writers/codex-config.js";
2
4
  import { previewCursorignoreActions, readCursorignore } from "./writers/cursorignore.js";
3
5
  /**
4
6
  * Apply (or dry-run) a fix plan.
5
- * Week 1: only `.cursorignore` mutations are supported.
7
+ * Safe writers: `.cursorignore`, Claude settings deny, Codex config.toml deny.
6
8
  */
7
9
  export declare function applyFixPlan(plan: FixPlan, options: {
8
10
  dryRun: boolean;
9
11
  }): Promise<FixApplyResult>;
10
12
  export { previewCursorignoreActions, readCursorignore };
13
+ export { previewClaudeSettingsActions, readClaudeSettings };
14
+ export { previewCodexConfigActions, readCodexConfig };
@@ -1,27 +1,47 @@
1
+ import { previewClaudeSettingsActions, readClaudeSettings, writeClaudeSettings, } from "./writers/claude-settings.js";
2
+ import { previewCodexConfigActions, readCodexConfig, writeCodexConfig, } from "./writers/codex-config.js";
1
3
  import { previewCursorignoreActions, readCursorignore, writeCursorignore, } from "./writers/cursorignore.js";
2
4
  /**
3
5
  * Apply (or dry-run) a fix plan.
4
- * Week 1: only `.cursorignore` mutations are supported.
6
+ * Safe writers: `.cursorignore`, Claude settings deny, Codex config.toml deny.
5
7
  */
6
8
  export async function applyFixPlan(plan, options) {
7
- const current = await readCursorignore(plan.root);
8
- const preview = previewCursorignoreActions(current, plan.actions);
9
- if (!preview) {
10
- return {
11
- plan,
12
- dryRun: options.dryRun,
13
- writtenFiles: [],
14
- changedFiles: [],
15
- };
9
+ const cursorContent = await readCursorignore(plan.root);
10
+ const claudeContent = await readClaudeSettings(plan.root);
11
+ const codexContent = await readCodexConfig(plan.root);
12
+ const cursorPreview = previewCursorignoreActions(cursorContent, plan.actions);
13
+ const claudePreview = previewClaudeSettingsActions(claudeContent, plan.actions);
14
+ const codexPreview = previewCodexConfigActions(codexContent, plan.actions);
15
+ const writtenFiles = [];
16
+ const changedFiles = [];
17
+ if (cursorPreview) {
18
+ changedFiles.push(cursorPreview.targetRelativePath);
19
+ if (!options.dryRun) {
20
+ await writeCursorignore(plan.root, cursorPreview.after);
21
+ writtenFiles.push(cursorPreview.targetRelativePath);
22
+ }
16
23
  }
17
- if (!options.dryRun) {
18
- await writeCursorignore(plan.root, preview.after);
24
+ if (claudePreview) {
25
+ changedFiles.push(claudePreview.targetRelativePath);
26
+ if (!options.dryRun) {
27
+ await writeClaudeSettings(plan.root, claudePreview.after);
28
+ writtenFiles.push(claudePreview.targetRelativePath);
29
+ }
30
+ }
31
+ if (codexPreview) {
32
+ changedFiles.push(codexPreview.targetRelativePath);
33
+ if (!options.dryRun) {
34
+ await writeCodexConfig(plan.root, codexPreview.after);
35
+ writtenFiles.push(codexPreview.targetRelativePath);
36
+ }
19
37
  }
20
38
  return {
21
39
  plan,
22
40
  dryRun: options.dryRun,
23
- writtenFiles: options.dryRun ? [] : [preview.targetRelativePath],
24
- changedFiles: [preview.targetRelativePath],
41
+ writtenFiles,
42
+ changedFiles,
25
43
  };
26
44
  }
27
45
  export { previewCursorignoreActions, readCursorignore };
46
+ export { previewClaudeSettingsActions, readClaudeSettings };
47
+ export { previewCodexConfigActions, readCodexConfig };
@@ -2,7 +2,7 @@ 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
- * Week 1: Cursor `.cursorignore` appends for safe context findings only.
5
+ * Safe context exclusions: Cursor `.cursorignore`, Claude Code Read deny, Codex filesystem deny.
6
6
  */
7
7
  export declare function buildFixPlan(result: ScanResult): Promise<FixPlan>;
8
8
  /** Patterns that would be newly appended given current file contents. */
@@ -1,11 +1,21 @@
1
1
  import path from "node:path";
2
2
  import { readTextFile } from "../../utils/fs.js";
3
+ import { settingsTextDeniesPath } from "../rules/claude-deny.js";
4
+ import { configTextDeniesPath } from "../rules/codex-deny.js";
3
5
  import { createIgnoreIndex, parseIgnoreFile, pathMatchesAny } from "../rules/ignore.js";
4
6
  import { isSafeContextFixRule, patternForFinding } from "./patterns.js";
7
+ import { denyRuleForFixPattern } from "./writers/claude-settings.js";
8
+ import { assertWritableCodexConfig, denyKeyForFixPattern } from "./writers/codex-config.js";
5
9
  const DEFAULT_MAX_IGNORE_BYTES = 512 * 1024;
6
10
  function cursorConfigured(result) {
7
11
  return result.agents.some((a) => a.id === "cursor" && (a.detected || a.configured));
8
12
  }
13
+ function claudeConfigured(result) {
14
+ return result.agents.some((a) => a.id === "claude-code" && (a.detected || a.configured));
15
+ }
16
+ function codexConfigured(result) {
17
+ return result.agents.some((a) => a.id === "codex" && (a.detected || a.configured));
18
+ }
9
19
  async function loadCursorignorePatterns(root) {
10
20
  const filePath = path.join(root, ".cursorignore");
11
21
  const text = await readTextFile(filePath, DEFAULT_MAX_IGNORE_BYTES);
@@ -14,9 +24,15 @@ async function loadCursorignorePatterns(root) {
14
24
  }
15
25
  return parseIgnoreFile(text);
16
26
  }
27
+ async function loadClaudeSettingsText(root) {
28
+ return readTextFile(path.join(root, ".claude/settings.json"), DEFAULT_MAX_IGNORE_BYTES);
29
+ }
30
+ async function loadCodexConfigText(root) {
31
+ return readTextFile(path.join(root, ".codex/config.toml"), DEFAULT_MAX_IGNORE_BYTES);
32
+ }
17
33
  /**
18
34
  * Build a fix plan from a scan result.
19
- * Week 1: Cursor `.cursorignore` appends for safe context findings only.
35
+ * Safe context exclusions: Cursor `.cursorignore`, Claude Code Read deny, Codex filesystem deny.
20
36
  */
21
37
  export async function buildFixPlan(result) {
22
38
  const root = result.repository.root;
@@ -27,7 +43,11 @@ export async function buildFixPlan(result) {
27
43
  gitignorePatterns: [],
28
44
  cursorignorePatterns: existingCursorPatterns,
29
45
  });
46
+ const claudeSettingsText = await loadClaudeSettingsText(root);
47
+ const codexConfigText = await loadCodexConfigText(root);
30
48
  const hasCursor = cursorConfigured(result);
49
+ const hasClaude = claudeConfigured(result);
50
+ const hasCodex = codexConfigured(result);
31
51
  for (const finding of result.findings) {
32
52
  if (finding.fixability !== "safe") {
33
53
  skipped.push({
@@ -60,6 +80,8 @@ export async function buildFixPlan(result) {
60
80
  continue;
61
81
  }
62
82
  const wantsCursor = finding.affectedAgents.includes("cursor");
83
+ const wantsClaude = finding.affectedAgents.includes("claude-code");
84
+ const wantsCodex = finding.affectedAgents.includes("codex");
63
85
  if (wantsCursor && hasCursor) {
64
86
  if (cursorIndex.matchesCursorignore(evidencePath) ||
65
87
  cursorIndex.matchesCursorignore(pattern)) {
@@ -80,8 +102,54 @@ export async function buildFixPlan(result) {
80
102
  reason: "Cursor not detected; skipping .cursorignore fix",
81
103
  });
82
104
  }
105
+ if (wantsClaude && hasClaude) {
106
+ const denyRule = denyRuleForFixPattern(pattern, evidencePath);
107
+ if (claudeSettingsText && settingsTextDeniesPath(claudeSettingsText, evidencePath)) {
108
+ skipped.push({
109
+ findingId: finding.id,
110
+ ruleId: finding.ruleId,
111
+ reason: "Already excluded by Claude Code Read deny",
112
+ });
113
+ }
114
+ else {
115
+ mergeClaudeAction(actionsByKey, finding, pattern, evidencePath, denyRule);
116
+ }
117
+ }
118
+ else if (wantsClaude && !hasClaude) {
119
+ skipped.push({
120
+ findingId: finding.id,
121
+ ruleId: finding.ruleId,
122
+ reason: "Claude Code not detected; skipping settings deny fix",
123
+ });
124
+ }
125
+ if (wantsCodex && hasCodex) {
126
+ const denyKey = denyKeyForFixPattern(pattern, evidencePath);
127
+ if (codexConfigText && configTextDeniesPath(codexConfigText, evidencePath)) {
128
+ skipped.push({
129
+ findingId: finding.id,
130
+ ruleId: finding.ruleId,
131
+ reason: "Already excluded by Codex filesystem deny",
132
+ });
133
+ }
134
+ else if (codexConfigText) {
135
+ // Refuse unwritable/invalid config the same way Claude Fix refuses bad JSON —
136
+ // do not silently skip and leave Codex findings uncleared after a partial Cursor write.
137
+ assertWritableCodexConfig(codexConfigText);
138
+ mergeCodexAction(actionsByKey, finding, pattern, evidencePath, denyKey);
139
+ }
140
+ else {
141
+ mergeCodexAction(actionsByKey, finding, pattern, evidencePath, denyKey);
142
+ }
143
+ }
144
+ else if (wantsCodex && !hasCodex) {
145
+ skipped.push({
146
+ findingId: finding.id,
147
+ ruleId: finding.ruleId,
148
+ reason: "Codex not detected; skipping config.toml deny fix",
149
+ });
150
+ }
83
151
  for (const agent of finding.affectedAgents) {
84
- if (agent === "cursor") {
152
+ if (agent === "cursor" || agent === "claude-code" || agent === "codex") {
85
153
  continue;
86
154
  }
87
155
  skipped.push({
@@ -117,6 +185,46 @@ function mergeCursorAction(actionsByKey, finding, pattern, evidencePath) {
117
185
  description: `Exclude ${evidencePath} from Cursor agent context via .cursorignore`,
118
186
  });
119
187
  }
188
+ function mergeClaudeAction(actionsByKey, finding, pattern, evidencePath, denyRule) {
189
+ const key = `claude-code:.claude/settings.json:${denyRule}`;
190
+ const existing = actionsByKey.get(key);
191
+ if (existing) {
192
+ if (!existing.findingIds.includes(finding.id)) {
193
+ existing.findingIds.push(finding.id);
194
+ }
195
+ return;
196
+ }
197
+ actionsByKey.set(key, {
198
+ id: key,
199
+ kind: "append-ignore-pattern",
200
+ agent: "claude-code",
201
+ targetRelativePath: ".claude/settings.json",
202
+ pattern,
203
+ evidencePath,
204
+ findingIds: [finding.id],
205
+ description: `Exclude ${evidencePath} from Claude Code via permissions.deny ${denyRule}`,
206
+ });
207
+ }
208
+ function mergeCodexAction(actionsByKey, finding, pattern, evidencePath, denyKey) {
209
+ const key = `codex:.codex/config.toml:${denyKey}`;
210
+ const existing = actionsByKey.get(key);
211
+ if (existing) {
212
+ if (!existing.findingIds.includes(finding.id)) {
213
+ existing.findingIds.push(finding.id);
214
+ }
215
+ return;
216
+ }
217
+ actionsByKey.set(key, {
218
+ id: key,
219
+ kind: "append-ignore-pattern",
220
+ agent: "codex",
221
+ targetRelativePath: ".codex/config.toml",
222
+ pattern,
223
+ evidencePath,
224
+ findingIds: [finding.id],
225
+ description: `Exclude ${evidencePath} from Codex via filesystem deny "${denyKey}"`,
226
+ });
227
+ }
120
228
  /** Patterns that would be newly appended given current file contents. */
121
229
  export function missingPatternsForCursorignore(currentContent, patterns) {
122
230
  const existing = currentContent === null ? [] : parseIgnoreFile(currentContent);
@@ -129,7 +237,6 @@ export function missingPatternsForCursorignore(currentContent, patterns) {
129
237
  existing.some((p) => p === pattern)) {
130
238
  continue;
131
239
  }
132
- // Also skip if an existing pattern already excludes this path
133
240
  const evidenceGuess = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
134
241
  if (pathMatchesAny(evidenceGuess, existing) || pathMatchesAny(pattern, existing)) {
135
242
  continue;
@@ -2,5 +2,7 @@ import type { FixApplyResult, FixPlan } from "./types.js";
2
2
  export declare function renderFixPlanTerminal(plan: FixPlan, options: {
3
3
  dryRun: boolean;
4
4
  cursorContent: string | null;
5
+ claudeSettingsContent?: string | null;
6
+ codexConfigContent?: string | null;
5
7
  applyResult?: FixApplyResult;
6
8
  }): string;
@@ -1,3 +1,5 @@
1
+ import { previewClaudeSettingsActions } from "./writers/claude-settings.js";
2
+ import { previewCodexConfigActions } from "./writers/codex-config.js";
1
3
  import { previewCursorignoreActions } from "./writers/cursorignore.js";
2
4
  export function renderFixPlanTerminal(plan, options) {
3
5
  const lines = [];
@@ -14,10 +16,16 @@ export function renderFixPlanTerminal(plan, options) {
14
16
  lines.push(` - ${reason} (${count})`);
15
17
  }
16
18
  lines.push("");
17
- lines.push(" Next: address remaining findings, then run `agentdoctor verify`.");
19
+ lines.push("Next");
20
+ lines.push(" Inspect a finding: agentdoctor explain <rule-id> (ids in scan --json)");
21
+ lines.push(" Confirm with verify: agentdoctor verify --baseline agentdoctor-report.json");
22
+ lines.push(" No baseline yet? agentdoctor scan --json > agentdoctor-report.json");
18
23
  }
19
24
  else {
20
25
  lines.push(" Scan found nothing that Fix can change.");
26
+ lines.push("");
27
+ lines.push("Next");
28
+ lines.push(" Re-scan: agentdoctor scan");
21
29
  }
22
30
  lines.push("");
23
31
  return lines.join("\n");
@@ -28,14 +36,41 @@ export function renderFixPlanTerminal(plan, options) {
28
36
  lines.push(` pattern: ${action.pattern}`);
29
37
  lines.push(` findings: ${action.findingIds.length}`);
30
38
  }
31
- const preview = previewCursorignoreActions(options.cursorContent, plan.actions);
32
- if (preview) {
39
+ const cursorPreview = previewCursorignoreActions(options.cursorContent, plan.actions);
40
+ const claudePreview = previewClaudeSettingsActions(options.claudeSettingsContent ?? null, plan.actions);
41
+ const codexPreview = previewCodexConfigActions(options.codexConfigContent ?? null, plan.actions);
42
+ if (cursorPreview || claudePreview || codexPreview) {
33
43
  lines.push("");
34
44
  lines.push(" File changes:");
35
- lines.push(` ${preview.targetRelativePath} (+${preview.patternsToAdd.length} pattern(s))`);
36
- lines.push("");
37
- for (const previewLine of preview.preview.split("\n")) {
38
- lines.push(` ${previewLine}`);
45
+ let shown = false;
46
+ if (cursorPreview) {
47
+ lines.push(` ${cursorPreview.targetRelativePath} (+${cursorPreview.patternsToAdd.length} pattern(s))`);
48
+ lines.push("");
49
+ for (const previewLine of cursorPreview.preview.split("\n")) {
50
+ lines.push(` ${previewLine}`);
51
+ }
52
+ shown = true;
53
+ }
54
+ if (claudePreview) {
55
+ if (shown) {
56
+ lines.push("");
57
+ }
58
+ lines.push(` ${claudePreview.targetRelativePath} (+${claudePreview.denyRulesToAdd.length} deny rule(s))`);
59
+ lines.push("");
60
+ for (const previewLine of claudePreview.preview.split("\n")) {
61
+ lines.push(` ${previewLine}`);
62
+ }
63
+ shown = true;
64
+ }
65
+ if (codexPreview) {
66
+ if (shown) {
67
+ lines.push("");
68
+ }
69
+ lines.push(` ${codexPreview.targetRelativePath} (+${codexPreview.denyKeysToAdd.length} deny key(s))`);
70
+ lines.push("");
71
+ for (const previewLine of codexPreview.preview.split("\n")) {
72
+ lines.push(` ${previewLine}`);
73
+ }
39
74
  }
40
75
  }
41
76
  if (options.applyResult && !options.dryRun) {
@@ -50,12 +85,25 @@ export function renderFixPlanTerminal(plan, options) {
50
85
  else if (options.dryRun) {
51
86
  lines.push("");
52
87
  lines.push(" No files were modified (dry-run).");
53
- lines.push(" Re-run without --dry-run to apply.");
54
88
  }
55
89
  if (plan.skipped.length > 0) {
56
90
  lines.push("");
57
- lines.push(` Skipped: ${plan.skipped.length} finding(s) (writers pending or already fixed)`);
91
+ lines.push(` Skipped: ${plan.skipped.length} finding(s):`);
92
+ const reasons = summarizeSkipReasons(plan.skipped.map((s) => s.reason));
93
+ for (const [reason, count] of reasons) {
94
+ lines.push(` - ${reason} (${count})`);
95
+ }
96
+ }
97
+ lines.push("");
98
+ lines.push("Next");
99
+ if (options.dryRun && plan.actions.length > 0) {
100
+ lines.push(" Apply these changes: agentdoctor fix -y");
101
+ }
102
+ if (plan.skipped.length > 0) {
103
+ lines.push(" Review/manual findings stay open — inspect with: agentdoctor explain <rule-id>");
58
104
  }
105
+ lines.push(" Confirm with verify: agentdoctor verify --baseline agentdoctor-report.json");
106
+ lines.push(" No baseline yet? agentdoctor scan --json > agentdoctor-report.json");
59
107
  lines.push("");
60
108
  return lines.join("\n");
61
109
  }
@@ -1,7 +1,7 @@
1
1
  import readline from "node:readline/promises";
2
2
  import { stdin as input, stdout as output } from "node:process";
3
3
  import { scan } from "../scanner/scan.js";
4
- import { applyFixPlan, readCursorignore } from "./apply.js";
4
+ import { applyFixPlan, readClaudeSettings, readCodexConfig, readCursorignore } from "./apply.js";
5
5
  import { buildFixPlan } from "./plan.js";
6
6
  import { renderFixPlanTerminal } from "./render.js";
7
7
  /**
@@ -11,6 +11,8 @@ export async function runFix(options) {
11
11
  const result = await scan({ cwd: options.cwd });
12
12
  const plan = await buildFixPlan(result);
13
13
  const cursorContent = await readCursorignore(plan.root);
14
+ const claudeSettingsContent = await readClaudeSettings(plan.root);
15
+ const codexConfigContent = await readCodexConfig(plan.root);
14
16
  if (plan.actions.length === 0 || options.dryRun) {
15
17
  const applyResult = await applyFixPlan(plan, { dryRun: true });
16
18
  return { plan, applyResult, cancelled: false };
@@ -19,6 +21,8 @@ export async function runFix(options) {
19
21
  const previewText = renderFixPlanTerminal(plan, {
20
22
  dryRun: true,
21
23
  cursorContent,
24
+ claudeSettingsContent,
25
+ codexConfigContent,
22
26
  });
23
27
  process.stdout.write(previewText);
24
28
  const confirmed = await confirm("Apply these changes?");
@@ -1,7 +1,7 @@
1
1
  import type { AgentId } from "../../types/index.js";
2
2
  export type FixKind = "append-ignore-pattern";
3
3
  /** Allowlisted relative paths fix may create or modify. */
4
- export declare const FIX_PATH_ALLOWLIST: readonly [".cursorignore"];
4
+ export declare const FIX_PATH_ALLOWLIST: readonly [".cursorignore", ".claude/settings.json", ".codex/config.toml"];
5
5
  export type FixAllowlistedPath = (typeof FIX_PATH_ALLOWLIST)[number];
6
6
  export interface FixAction {
7
7
  id: string;
@@ -1,2 +1,6 @@
1
1
  /** Allowlisted relative paths fix may create or modify. */
2
- export const FIX_PATH_ALLOWLIST = [".cursorignore"];
2
+ export const FIX_PATH_ALLOWLIST = [
3
+ ".cursorignore",
4
+ ".claude/settings.json",
5
+ ".codex/config.toml",
6
+ ];
@@ -0,0 +1,16 @@
1
+ import type { FixAction } from "../types.js";
2
+ declare const SETTINGS_RELATIVE = ".claude/settings.json";
3
+ export interface ClaudeSettingsWritePreview {
4
+ targetRelativePath: typeof SETTINGS_RELATIVE;
5
+ denyRulesToAdd: string[];
6
+ before: string;
7
+ after: string;
8
+ preview: string;
9
+ }
10
+ export declare function denyRuleForFixPattern(pattern: string, evidencePath: string): string;
11
+ export declare function missingClaudeDenyRules(currentContent: string | null, denyRules: string[]): string[];
12
+ export declare function buildClaudeSettingsContent(currentContent: string | null, denyRulesToAdd: string[]): string;
13
+ export declare function previewClaudeSettingsActions(currentContent: string | null, actions: FixAction[]): ClaudeSettingsWritePreview | null;
14
+ export declare function readClaudeSettings(root: string): Promise<string | null>;
15
+ export declare function writeClaudeSettings(root: string, content: string): Promise<void>;
16
+ export {};