@praneeth_54/agentdoctor 0.2.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 (80) hide show
  1. package/CHANGELOG.md +107 -3
  2. package/README.md +86 -35
  3. package/dist/agents/inspect.js +58 -51
  4. package/dist/cli/commands/doctor.js +1 -1
  5. package/dist/cli/commands/explain.js +2 -2
  6. package/dist/cli/commands/fix.d.ts +7 -5
  7. package/dist/cli/commands/fix.js +73 -12
  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 +19 -0
  11. package/dist/cli/commands/verify.js +70 -0
  12. package/dist/cli/program.js +97 -33
  13. package/dist/constants.d.ts +1 -1
  14. package/dist/constants.js +1 -1
  15. package/dist/core/fix/apply.d.ts +14 -0
  16. package/dist/core/fix/apply.js +47 -0
  17. package/dist/core/fix/patterns.d.ts +7 -0
  18. package/dist/core/fix/patterns.js +21 -0
  19. package/dist/core/fix/plan.d.ts +9 -0
  20. package/dist/core/fix/plan.js +247 -0
  21. package/dist/core/fix/render.d.ts +8 -0
  22. package/dist/core/fix/render.js +116 -0
  23. package/dist/core/fix/run.d.ts +15 -0
  24. package/dist/core/fix/run.js +58 -0
  25. package/dist/core/fix/types.d.ts +34 -0
  26. package/dist/core/fix/types.js +6 -0
  27. package/dist/core/fix/writers/claude-settings.d.ts +16 -0
  28. package/dist/core/fix/writers/claude-settings.js +100 -0
  29. package/dist/core/fix/writers/codex-config.d.ts +22 -0
  30. package/dist/core/fix/writers/codex-config.js +183 -0
  31. package/dist/core/fix/writers/cursorignore.d.ts +13 -0
  32. package/dist/core/fix/writers/cursorignore.js +68 -0
  33. package/dist/core/path-resolution/index.d.ts +5 -0
  34. package/dist/core/path-resolution/index.js +5 -0
  35. package/dist/core/path-resolution/prepare.d.ts +33 -0
  36. package/dist/core/path-resolution/prepare.js +98 -0
  37. package/dist/core/policy/evaluate.d.ts +44 -0
  38. package/dist/core/policy/evaluate.js +120 -0
  39. package/dist/core/rules/claude-deny.d.ts +8 -0
  40. package/dist/core/rules/claude-deny.js +39 -0
  41. package/dist/core/rules/codex-deny.d.ts +5 -0
  42. package/dist/core/rules/codex-deny.js +42 -0
  43. package/dist/core/rules/context/generated-directory.js +52 -8
  44. package/dist/core/rules/context/large-log-file.js +27 -10
  45. package/dist/core/rules/ignore.js +2 -5
  46. package/dist/core/rules/instructions/missing-path-reference.d.ts +5 -1
  47. package/dist/core/rules/instructions/missing-path-reference.js +114 -7
  48. package/dist/core/rules/path-kind.d.ts +27 -0
  49. package/dist/core/rules/path-kind.js +180 -0
  50. package/dist/core/rules/security/env-file-exposure.js +23 -38
  51. package/dist/core/rules/security/private-key-file.js +4 -0
  52. package/dist/core/rules/text-cache.js +46 -40
  53. package/dist/core/verify/compare.d.ts +29 -0
  54. package/dist/core/verify/compare.js +56 -0
  55. package/dist/core/verify/load-baseline.d.ts +15 -0
  56. package/dist/core/verify/load-baseline.js +71 -0
  57. package/dist/core/verify/verify.d.ts +26 -0
  58. package/dist/core/verify/verify.js +40 -0
  59. package/dist/discovery/files.js +7 -2
  60. package/dist/discovery/log-like.d.ts +2 -0
  61. package/dist/discovery/log-like.js +7 -0
  62. package/dist/index.d.ts +10 -0
  63. package/dist/index.js +6 -0
  64. package/dist/reporters/github/annotations.d.ts +8 -0
  65. package/dist/reporters/github/annotations.js +42 -0
  66. package/dist/reporters/github/emit.d.ts +23 -0
  67. package/dist/reporters/github/emit.js +31 -0
  68. package/dist/reporters/github/summary.d.ts +20 -0
  69. package/dist/reporters/github/summary.js +111 -0
  70. package/dist/reporters/terminal/report.d.ts +5 -0
  71. package/dist/reporters/terminal/report.js +73 -4
  72. package/dist/reporters/verify/json.d.ts +5 -0
  73. package/dist/reporters/verify/json.js +51 -0
  74. package/dist/reporters/verify/terminal.d.ts +6 -0
  75. package/dist/reporters/verify/terminal.js +71 -0
  76. package/dist/utils/fs.d.ts +5 -0
  77. package/dist/utils/fs.js +34 -0
  78. package/dist/utils/path.d.ts +5 -0
  79. package/dist/utils/path.js +16 -0
  80. package/package.json +11 -1
@@ -0,0 +1,20 @@
1
+ import type { Finding, ScanResult } from "../../types/index.js";
2
+ import type { PolicyViolation } from "../../core/policy/evaluate.js";
3
+ export interface GithubSummaryInput {
4
+ title: string;
5
+ overallScore: number | null;
6
+ findings: Finding[];
7
+ violations: PolicyViolation[];
8
+ mode: "scan" | "verify";
9
+ verifySummary?: {
10
+ fixed: number;
11
+ remaining: number;
12
+ new: number;
13
+ unchanged: number;
14
+ };
15
+ }
16
+ /**
17
+ * Markdown for `$GITHUB_STEP_SUMMARY`.
18
+ */
19
+ export declare function renderGithubStepSummary(input: GithubSummaryInput): string;
20
+ export declare function renderGithubStepSummaryForScan(result: ScanResult, violations: PolicyViolation[]): string;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Markdown for `$GITHUB_STEP_SUMMARY`.
3
+ */
4
+ export function renderGithubStepSummary(input) {
5
+ const lines = [];
6
+ lines.push(`# AgentDoctor ${input.mode === "verify" ? "Verify" : "Scan"}`);
7
+ lines.push("");
8
+ if (input.overallScore !== null) {
9
+ lines.push(`**Readiness:** ${input.overallScore}/100`);
10
+ lines.push("");
11
+ }
12
+ if (input.violations.length > 0) {
13
+ lines.push("## Policy violations");
14
+ lines.push("");
15
+ for (const violation of input.violations) {
16
+ lines.push(`- ${violation.message}`);
17
+ }
18
+ lines.push("");
19
+ }
20
+ else {
21
+ lines.push("Policy gates: **passed**");
22
+ lines.push("");
23
+ }
24
+ if (input.verifySummary) {
25
+ lines.push("## Verify delta");
26
+ lines.push("");
27
+ lines.push(`| Fixed | Remaining | New | Unchanged |`);
28
+ lines.push(`| ---: | ---: | ---: | ---: |`);
29
+ lines.push(`| ${input.verifySummary.fixed} | ${input.verifySummary.remaining} | ${input.verifySummary.new} | ${input.verifySummary.unchanged} |`);
30
+ lines.push("");
31
+ }
32
+ const critical = input.findings.filter((f) => f.severity === "critical").length;
33
+ const warning = input.findings.filter((f) => f.severity === "warning").length;
34
+ const info = input.findings.filter((f) => f.severity === "info").length;
35
+ lines.push("## Findings");
36
+ lines.push("");
37
+ lines.push(`| Severity | Count |`);
38
+ lines.push(`| --- | ---: |`);
39
+ lines.push(`| Critical | ${critical} |`);
40
+ lines.push(`| Warning | ${warning} |`);
41
+ lines.push(`| Info | ${info} |`);
42
+ lines.push(`| **Total** | **${input.findings.length}** |`);
43
+ lines.push("");
44
+ if (input.findings.length > 0) {
45
+ lines.push("### Top findings");
46
+ lines.push("");
47
+ const top = [...input.findings]
48
+ .sort((a, b) => severityOrder(b.severity) - severityOrder(a.severity))
49
+ .slice(0, 20);
50
+ for (const finding of top) {
51
+ const path = finding.evidence?.path ? ` \`${finding.evidence.path}\`` : "";
52
+ lines.push(`- **${finding.severity}** \`${finding.ruleId}\`${path} — ${finding.title}`);
53
+ }
54
+ lines.push("");
55
+ }
56
+ if (input.violations.length > 0) {
57
+ lines.push(...renderGithubNextSteps(input));
58
+ }
59
+ return `${lines.join("\n")}\n`;
60
+ }
61
+ /**
62
+ * Shortest path back to green when the Action failed a policy gate.
63
+ * Shown only on failure — success summaries stay quiet.
64
+ */
65
+ function renderGithubNextSteps(input) {
66
+ const lines = [];
67
+ lines.push("## Next");
68
+ lines.push("");
69
+ lines.push("Policy gate failed. Shortest path back to green:");
70
+ lines.push("");
71
+ const hasSafe = input.findings.some((f) => f.fixability === "safe");
72
+ const topRule = [...input.findings].sort((a, b) => severityOrder(b.severity) - severityOrder(a.severity))[0]?.ruleId;
73
+ if (input.mode === "verify") {
74
+ lines.push("1. Reproduce locally: `npx @praneeth_54/agentdoctor verify`");
75
+ if (hasSafe) {
76
+ lines.push("2. Apply safe fixes: `npx @praneeth_54/agentdoctor fix -y`");
77
+ lines.push("3. Re-check: `npx @praneeth_54/agentdoctor verify`");
78
+ }
79
+ else {
80
+ lines.push(`2. Inspect a finding: \`npx @praneeth_54/agentdoctor explain ${topRule ?? "<rule-id>"}\``);
81
+ lines.push("3. Fix or baseline, then re-run this Action");
82
+ }
83
+ }
84
+ else {
85
+ lines.push("1. Reproduce locally: `npx @praneeth_54/agentdoctor scan --ci`");
86
+ if (hasSafe) {
87
+ lines.push("2. Apply safe fixes: `npx @praneeth_54/agentdoctor fix -y`");
88
+ lines.push("3. Confirm: `npx @praneeth_54/agentdoctor scan --ci`");
89
+ }
90
+ else {
91
+ lines.push(`2. Inspect a finding: \`npx @praneeth_54/agentdoctor explain ${topRule ?? "<rule-id>"}\``);
92
+ lines.push("3. Fix the gated findings, then re-run this Action");
93
+ }
94
+ }
95
+ lines.push("");
96
+ lines.push("Preview before applying: `npx @praneeth_54/agentdoctor fix --dry-run`");
97
+ lines.push("");
98
+ return lines;
99
+ }
100
+ export function renderGithubStepSummaryForScan(result, violations) {
101
+ return renderGithubStepSummary({
102
+ title: "AgentDoctor Scan",
103
+ overallScore: result.scores?.overall ?? null,
104
+ findings: result.findings,
105
+ violations,
106
+ mode: "scan",
107
+ });
108
+ }
109
+ function severityOrder(severity) {
110
+ return severity === "critical" ? 3 : severity === "warning" ? 2 : 1;
111
+ }
@@ -2,6 +2,11 @@ import type { ScanResult } from "../../types/index.js";
2
2
  export interface TerminalReportOptions {
3
3
  verbose?: boolean;
4
4
  }
5
+ /**
6
+ * Close the Scan → Fix → Verify loop on first run.
7
+ * Empty / agentless scans already print a Next line; findings used to dead-end.
8
+ */
9
+ export declare function renderNextSteps(result: ScanResult): string[];
5
10
  /**
6
11
  * Terminal report: agents and actionable findings.
7
12
  */
@@ -42,7 +42,12 @@ function renderFindingBlock(finding, verbose) {
42
42
  : finding.severity === "warning"
43
43
  ? symbolWarn()
44
44
  : symbolInfo();
45
- lines.push(` ${icon} ${sanitizeTerminalText(finding.title)}`);
45
+ const fixTag = finding.fixability === "safe"
46
+ ? colors.green("[safe]")
47
+ : finding.fixability === "review"
48
+ ? colors.yellow("[review]")
49
+ : colors.dim("[manual]");
50
+ lines.push(` ${icon} ${fixTag} ${sanitizeTerminalText(finding.title)}`);
46
51
  if (finding.evidence?.path) {
47
52
  lines.push(colors.dim(` ${sanitizeTerminalText(finding.evidence.path)}`));
48
53
  }
@@ -59,6 +64,50 @@ function renderFindingBlock(finding, verbose) {
59
64
  lines.push("");
60
65
  return lines;
61
66
  }
67
+ function countByFixability(findings) {
68
+ let safe = 0;
69
+ let review = 0;
70
+ let manual = 0;
71
+ for (const finding of findings) {
72
+ if (finding.fixability === "safe") {
73
+ safe += 1;
74
+ }
75
+ else if (finding.fixability === "review") {
76
+ review += 1;
77
+ }
78
+ else {
79
+ manual += 1;
80
+ }
81
+ }
82
+ return { safe, review, manual };
83
+ }
84
+ /**
85
+ * Close the Scan → Fix → Verify loop on first run.
86
+ * Empty / agentless scans already print a Next line; findings used to dead-end.
87
+ */
88
+ export function renderNextSteps(result) {
89
+ const lines = [];
90
+ const total = result.summary.total;
91
+ if (total === 0) {
92
+ return lines;
93
+ }
94
+ const { safe, review, manual } = countByFixability(result.findings);
95
+ const reviewLike = review + manual;
96
+ lines.push(colors.bold("Next"));
97
+ lines.push("");
98
+ if (safe > 0) {
99
+ lines.push(` ${safe} finding(s) are auto-fixable. Preview: ${colors.bold("agentdoctor fix --dry-run")}`);
100
+ lines.push(` Apply: ${colors.bold("agentdoctor fix -y")}`);
101
+ }
102
+ else if (reviewLike > 0) {
103
+ lines.push(` ${reviewLike} finding(s) need review or manual action — ${colors.bold("agentdoctor fix")} will not change them automatically.`);
104
+ lines.push(colors.dim(" Inspect a rule: agentdoctor explain <rule-id> (ids are in --json output)"));
105
+ }
106
+ lines.push(colors.dim(" Save a verify baseline: agentdoctor scan --json > agentdoctor-report.json"));
107
+ lines.push(colors.dim(" After changes: agentdoctor verify --baseline agentdoctor-report.json"));
108
+ lines.push("");
109
+ return lines;
110
+ }
62
111
  /**
63
112
  * Terminal report: agents and actionable findings.
64
113
  */
@@ -115,7 +164,10 @@ export function renderTerminalReport(result, options = {}) {
115
164
  }
116
165
  if (total === 0) {
117
166
  if (limited) {
118
- lines.push(` ${symbolOk()} No agent-configuration findings`);
167
+ lines.push(` ${symbolWarn()} Nothing to audit yet — no Cursor, Claude Code, or Codex config found`);
168
+ lines.push("");
169
+ lines.push(colors.dim(" Next: add project agent config (for example `.cursor/`, `CLAUDE.md` / `.claude/`, or `AGENTS.md`),"));
170
+ lines.push(colors.dim(" then re-run `agentdoctor` in this repository."));
119
171
  }
120
172
  else {
121
173
  lines.push(` ${symbolOk()} No findings`);
@@ -149,7 +201,7 @@ export function renderTerminalReport(result, options = {}) {
149
201
  }
150
202
  }
151
203
  if (total > shown) {
152
- lines.push(colors.dim(` … and ${total - shown} more. Re-run with --verbose to see additional detail.`));
204
+ lines.push(colors.dim(` … and ${total - shown} more. Re-run with --verbose for detail, or --json for the full list.`));
153
205
  lines.push("");
154
206
  }
155
207
  }
@@ -158,9 +210,26 @@ export function renderTerminalReport(result, options = {}) {
158
210
  lines.push(` ${result.summary.critical} critical`);
159
211
  lines.push(` ${result.summary.warning} warning`);
160
212
  lines.push(` ${result.summary.info} info`);
213
+ if (total > 0) {
214
+ const { safe, review, manual } = countByFixability(result.findings);
215
+ lines.push(colors.dim(` Fixability: ${safe} safe · ${review} review · ${manual} manual`));
216
+ }
161
217
  lines.push("");
162
- lines.push(colors.dim("Scoring: not included in this release"));
218
+ if (result.scoringAvailable && result.scores) {
219
+ if (limited) {
220
+ lines.push(` Readiness: n/a — configure Cursor, Claude Code, or Codex before treating scores as agent readiness`);
221
+ lines.push(colors.dim(` Numeric scores remain in JSON (overall ${result.scores.overall}/100) for repository-risk findings only`));
222
+ }
223
+ else {
224
+ lines.push(` Readiness: ${result.scores.overall}/100`);
225
+ lines.push(colors.dim(" Category and agent scores: agentdoctor scan --json"));
226
+ }
227
+ }
228
+ else {
229
+ lines.push(colors.dim(" Readiness scoring unavailable for this scan"));
230
+ }
163
231
  lines.push("");
232
+ lines.push(...renderNextSteps(result));
164
233
  if (verbose) {
165
234
  lines.push(colors.dim("Timing"));
166
235
  lines.push(` Discovery: ${result.timing.discoveryMs}ms`);
@@ -0,0 +1,5 @@
1
+ import type { VerifyResult } from "../../core/verify/verify.js";
2
+ /**
3
+ * Machine-readable verify report.
4
+ */
5
+ export declare function renderVerifyJsonReport(result: VerifyResult): string;
@@ -0,0 +1,51 @@
1
+ import { sanitizeForOutput } from "../../utils/path.js";
2
+ /**
3
+ * Machine-readable verify report.
4
+ */
5
+ export function renderVerifyJsonReport(result) {
6
+ const payload = {
7
+ version: result.version,
8
+ baselinePath: sanitizeForOutput(result.baselinePath),
9
+ repositoryRoot: sanitizeForOutput(result.repositoryRoot),
10
+ fixed: result.fixed.map((f) => ({
11
+ ...f,
12
+ message: sanitizeForOutput(f.message),
13
+ title: sanitizeForOutput(f.title),
14
+ ...(f.evidence?.path !== undefined
15
+ ? { evidence: { ...f.evidence, path: sanitizeForOutput(f.evidence.path) } }
16
+ : f.evidence !== undefined
17
+ ? { evidence: f.evidence }
18
+ : {}),
19
+ })),
20
+ remaining: result.remaining.map((f) => ({
21
+ ...f,
22
+ message: sanitizeForOutput(f.message),
23
+ title: sanitizeForOutput(f.title),
24
+ ...(f.evidence?.path !== undefined
25
+ ? { evidence: { ...f.evidence, path: sanitizeForOutput(f.evidence.path) } }
26
+ : f.evidence !== undefined
27
+ ? { evidence: f.evidence }
28
+ : {}),
29
+ })),
30
+ new: result.new.map((f) => ({
31
+ ...f,
32
+ message: sanitizeForOutput(f.message),
33
+ title: sanitizeForOutput(f.title),
34
+ ...(f.evidence?.path !== undefined
35
+ ? { evidence: { ...f.evidence, path: sanitizeForOutput(f.evidence.path) } }
36
+ : f.evidence !== undefined
37
+ ? { evidence: f.evidence }
38
+ : {}),
39
+ })),
40
+ unchanged: result.unchanged.map((f) => ({
41
+ id: f.id,
42
+ ruleId: f.ruleId,
43
+ severity: f.severity,
44
+ })),
45
+ summary: result.summary,
46
+ scoringAvailable: result.scoringAvailable,
47
+ scores: result.scores,
48
+ timing: result.timing,
49
+ };
50
+ return `${JSON.stringify(payload, null, 2)}\n`;
51
+ }
@@ -0,0 +1,6 @@
1
+ import type { VerifyResult } from "../../core/verify/verify.js";
2
+ export declare function renderVerifyTerminalReport(result: VerifyResult, verbose?: boolean): string;
3
+ /**
4
+ * Close the verify step: tell a first-time user what to do with Fixed / Remaining / New.
5
+ */
6
+ export declare function renderVerifyNextSteps(result: VerifyResult): string[];
@@ -0,0 +1,71 @@
1
+ import { PACKAGE_VERSION } from "../../constants.js";
2
+ import { colors, symbolFail, symbolOk, symbolWarn } from "../../utils/colors.js";
3
+ import { sanitizeForOutput } from "../../utils/path.js";
4
+ function renderBucket(title, symbol, color, items, limit) {
5
+ const lines = [];
6
+ lines.push(color(`${symbol} ${title} (${items.length})`));
7
+ if (items.length === 0) {
8
+ lines.push(colors.dim(" (none)"));
9
+ lines.push("");
10
+ return lines;
11
+ }
12
+ const shown = items.slice(0, limit);
13
+ for (const item of shown) {
14
+ const path = item.evidence?.path ? ` — ${sanitizeForOutput(item.evidence.path)}` : "";
15
+ lines.push(` ${sanitizeForOutput(item.ruleId)}${path}`);
16
+ lines.push(colors.dim(` ${sanitizeForOutput(item.title)}`));
17
+ }
18
+ if (items.length > limit) {
19
+ lines.push(colors.dim(` … and ${items.length - limit} more`));
20
+ }
21
+ lines.push("");
22
+ return lines;
23
+ }
24
+ export function renderVerifyTerminalReport(result, verbose = false) {
25
+ const lines = [];
26
+ const limit = verbose ? 50 : 12;
27
+ lines.push("");
28
+ lines.push(colors.bold(`AgentDoctor verify v${PACKAGE_VERSION}`));
29
+ lines.push(colors.dim(`Baseline: ${sanitizeForOutput(result.baselinePath)}`));
30
+ lines.push("");
31
+ lines.push(...renderBucket("Fixed", symbolOk(), colors.green, result.fixed, limit));
32
+ lines.push(...renderBucket("Remaining", symbolWarn(), colors.yellow, result.remaining, limit));
33
+ lines.push(...renderBucket("New", symbolFail(), colors.red, result.new, limit));
34
+ lines.push(colors.bold("Summary"));
35
+ lines.push(` Fixed ${result.summary.fixed} · Remaining ${result.summary.remaining} · New ${result.summary.new} · Unchanged ${result.summary.unchanged}`);
36
+ lines.push(colors.dim(` Before ${result.summary.before} → After ${result.summary.after} findings`));
37
+ if (result.scores) {
38
+ lines.push(colors.dim(` Score ${result.scores.overall}/100`));
39
+ }
40
+ if (verbose) {
41
+ lines.push(colors.dim(` Timing scan=${result.timing.scanMs}ms compare=${result.timing.compareMs}ms total=${result.timing.totalMs}ms`));
42
+ }
43
+ lines.push("");
44
+ lines.push(...renderVerifyNextSteps(result));
45
+ return `${lines.join("\n")}\n`;
46
+ }
47
+ /**
48
+ * Close the verify step: tell a first-time user what to do with Fixed / Remaining / New.
49
+ */
50
+ export function renderVerifyNextSteps(result) {
51
+ const lines = [];
52
+ const { fixed, remaining, new: newly } = result.summary;
53
+ lines.push(colors.bold("Next"));
54
+ if (remaining === 0 && newly === 0) {
55
+ lines.push(fixed > 0
56
+ ? " All tracked findings cleared. Optional: gate CI with agentdoctor scan --ci"
57
+ : " No regressions vs baseline. Optional: gate CI with agentdoctor scan --ci");
58
+ }
59
+ else {
60
+ if (newly > 0) {
61
+ lines.push(` ${newly} new finding(s) appeared since the baseline — inspect: agentdoctor scan`);
62
+ }
63
+ if (remaining > 0) {
64
+ lines.push(` ${remaining} finding(s) still open — auto-fixable ones: agentdoctor fix --dry-run`);
65
+ lines.push(colors.dim(" Review/manual leftovers: agentdoctor explain <rule-id> (ids in scan --json)"));
66
+ }
67
+ lines.push(colors.dim(" After more fixes, re-check: agentdoctor verify --baseline agentdoctor-report.json"));
68
+ }
69
+ lines.push("");
70
+ return lines;
71
+ }
@@ -15,3 +15,8 @@ export declare function listDirectorySafe(dirPath: string): Promise<{
15
15
  ok: false;
16
16
  error: string;
17
17
  }>;
18
+ /**
19
+ * Write text via a same-directory temp file, then replace the target.
20
+ * On Windows, `rename` cannot overwrite an existing destination — remove first.
21
+ */
22
+ export declare function atomicWriteTextFile(filePath: string, content: string): Promise<void>;
package/dist/utils/fs.js CHANGED
@@ -61,3 +61,37 @@ export async function listDirectorySafe(dirPath) {
61
61
  return { ok: false, error: message };
62
62
  }
63
63
  }
64
+ /**
65
+ * Write text via a same-directory temp file, then replace the target.
66
+ * On Windows, `rename` cannot overwrite an existing destination — remove first.
67
+ */
68
+ export async function atomicWriteTextFile(filePath, content) {
69
+ const dir = path.dirname(filePath);
70
+ await fs.mkdir(dir, { recursive: true });
71
+ const tmp = path.join(dir, `.${path.basename(filePath)}.agentdoctor.${process.pid}.${Date.now()}.tmp`);
72
+ await fs.writeFile(tmp, content, "utf8");
73
+ try {
74
+ await replaceFile(tmp, filePath);
75
+ }
76
+ catch (error) {
77
+ await fs.rm(tmp, { force: true }).catch(() => undefined);
78
+ throw error;
79
+ }
80
+ }
81
+ async function replaceFile(tmpPath, targetPath) {
82
+ try {
83
+ await fs.rename(tmpPath, targetPath);
84
+ }
85
+ catch (error) {
86
+ const code = error && typeof error === "object" && "code" in error
87
+ ? String(error.code)
88
+ : "";
89
+ // Windows (and some locked-file cases) cannot rename over an existing path.
90
+ if (process.platform === "win32" || code === "EEXIST" || code === "EPERM") {
91
+ await fs.rm(targetPath, { force: true });
92
+ await fs.rename(tmpPath, targetPath);
93
+ return;
94
+ }
95
+ throw error;
96
+ }
97
+ }
@@ -7,6 +7,11 @@ export declare function resolveRepoRoot(inputPath: string): string;
7
7
  * Produce a POSIX-style relative path for stable cross-platform reporting.
8
8
  */
9
9
  export declare function toPosixRelative(from: string, to: string): string;
10
+ /**
11
+ * Normalize a repository-relative path to POSIX form without regex backtracking.
12
+ * Converts `\` to `/` and strips leading/trailing `/`.
13
+ */
14
+ export declare function toRepoRelativePosix(relativePath: string): string;
10
15
  /**
11
16
  * Ensure a candidate path stays within a root (defense against traversal).
12
17
  */
@@ -16,6 +16,22 @@ export function resolveRepoRoot(inputPath) {
16
16
  export function toPosixRelative(from, to) {
17
17
  return path.relative(from, to).split(path.sep).join("/");
18
18
  }
19
+ /**
20
+ * Normalize a repository-relative path to POSIX form without regex backtracking.
21
+ * Converts `\` to `/` and strips leading/trailing `/`.
22
+ */
23
+ export function toRepoRelativePosix(relativePath) {
24
+ const value = relativePath.includes("\\") ? relativePath.split("\\").join("/") : relativePath;
25
+ let start = 0;
26
+ let end = value.length;
27
+ while (start < end && value.charCodeAt(start) === 47) {
28
+ start += 1;
29
+ }
30
+ while (end > start && value.charCodeAt(end - 1) === 47) {
31
+ end -= 1;
32
+ }
33
+ return start === 0 && end === value.length ? value : value.slice(start, end);
34
+ }
19
35
  /**
20
36
  * Ensure a candidate path stays within a root (defense against traversal).
21
37
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praneeth_54/agentdoctor",
3
- "version": "0.2.0-beta",
3
+ "version": "1.0.0",
4
4
  "description": "Audit AI coding agent configuration in a repository — local, deterministic, no API key.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,14 @@
33
33
  "format:check": "prettier --check .",
34
34
  "test": "vitest run",
35
35
  "test:watch": "vitest",
36
+ "test:understanding": "vitest run --config vitest.understanding.config.ts",
37
+ "validate:understanding": "tsx validation/software-understanding/runner/run.ts",
38
+ "validate:project-brain": "tsx validation/project-brain/run.ts",
39
+ "validate:project-brain:real-world": "tsx validation/project-brain/real-world-run.ts",
40
+ "benchmark:project-brain": "tsx validation/project-brain/benchmark.ts",
36
41
  "verify": "npm run typecheck && npm run lint && npm run format:check && npm test && npm run build",
42
+ "verify:understanding": "npm run typecheck && npm run test:understanding",
43
+ "verify:project-brain": "npm run typecheck && npm run test:understanding && npm run validate:project-brain && npm run benchmark:project-brain",
37
44
  "prepare": "node scripts/ensure-cli-bin.mjs",
38
45
  "prepublishOnly": "npm run verify"
39
46
  },
@@ -77,5 +84,8 @@
77
84
  "typescript": "^5.9.3",
78
85
  "typescript-eslint": "^8.50.0",
79
86
  "vitest": "^3.2.4"
87
+ },
88
+ "overrides": {
89
+ "js-yaml": "4.3.1"
80
90
  }
81
91
  }