@praneeth_54/agentdoctor 3.0.1 → 3.0.3

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.
@@ -0,0 +1,85 @@
1
+ import { colors, symbolFail, symbolOk } from "../utils/colors.js";
2
+ const STAGE_LABELS = {
3
+ understand: "Understanding question",
4
+ search: "Searching project knowledge",
5
+ retrieve: "Retrieving relevant evidence",
6
+ verify: "Verifying evidence",
7
+ prepare: "Preparing answer",
8
+ };
9
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
10
+ /**
11
+ * Human-readable ask progress. Stages must be driven by real operations.
12
+ * Animation uses setInterval only for frame updates while work is in flight —
13
+ * never to invent delay.
14
+ */
15
+ export function createAskProgress(options = {}) {
16
+ const enabled = options.enabled !== false;
17
+ const stream = options.stream ?? process.stderr;
18
+ const tty = options.isTTY ??
19
+ Boolean(stream && typeof stream === "object" && "isTTY" in stream && stream.isTTY);
20
+ let frame = 0;
21
+ let timer;
22
+ let activeLabel;
23
+ let stopped = false;
24
+ const clearLine = () => {
25
+ if (!tty || !enabled)
26
+ return;
27
+ stream.write("\r\x1b[K");
28
+ };
29
+ const stopSpin = () => {
30
+ if (timer) {
31
+ clearInterval(timer);
32
+ timer = undefined;
33
+ }
34
+ clearLine();
35
+ };
36
+ const renderSpin = () => {
37
+ if (!tty || !enabled || !activeLabel || stopped)
38
+ return;
39
+ const glyph = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
40
+ frame += 1;
41
+ stream.write(`\r${colors.cyan(glyph)} ${activeLabel}...`);
42
+ };
43
+ const writeDone = (ok, label, detail) => {
44
+ if (!enabled || stopped)
45
+ return;
46
+ const mark = ok ? symbolOk() : symbolFail();
47
+ const suffix = detail ? colors.dim(` — ${detail}`) : "";
48
+ stream.write(`${mark} ${label}${suffix}\n`);
49
+ };
50
+ return {
51
+ stage(id, state, detail) {
52
+ if (!enabled || stopped)
53
+ return;
54
+ const label = STAGE_LABELS[id];
55
+ if (state === "start") {
56
+ stopSpin();
57
+ activeLabel = label;
58
+ if (tty) {
59
+ frame = 0;
60
+ renderSpin();
61
+ timer = setInterval(renderSpin, 80);
62
+ timer.unref?.();
63
+ }
64
+ else {
65
+ stream.write(`${label}...\n`);
66
+ }
67
+ return;
68
+ }
69
+ stopSpin();
70
+ activeLabel = undefined;
71
+ writeDone(state === "ok", state === "ok" && id === "prepare" ? "Answer ready" : label, detail);
72
+ },
73
+ stop() {
74
+ stopSpin();
75
+ activeLabel = undefined;
76
+ stopped = true;
77
+ },
78
+ };
79
+ }
80
+ export function silentAskProgress() {
81
+ return {
82
+ stage() { },
83
+ stop() { },
84
+ };
85
+ }
@@ -5,6 +5,12 @@ export declare function runAskCommand(options: {
5
5
  json?: boolean;
6
6
  /** Test-only: force mock provider */
7
7
  useMock?: boolean;
8
+ /** Test override: force progress enabled/disabled */
9
+ progressEnabled?: boolean;
10
+ /** Test override: progress stream */
11
+ progressStream?: NodeJS.WritableStream;
12
+ /** Test override: treat progress stream as TTY */
13
+ progressIsTTY?: boolean;
8
14
  }): Promise<ExitCode>;
9
15
  export declare function runChatCommand(options: {
10
16
  root?: string;
@@ -5,6 +5,7 @@ import { MockModelProvider, createModelProvider, loadAiConfig } from "../../ai/i
5
5
  import { EXIT_CODES } from "../../types/index.js";
6
6
  import { resolveCliProjectRoot } from "../safe-root.js";
7
7
  import { colors } from "../../utils/colors.js";
8
+ import { createAskProgress } from "../ask-progress.js";
8
9
  function printBanner(options) {
9
10
  const lines = [
10
11
  "",
@@ -58,13 +59,21 @@ export async function runAskCommand(options) {
58
59
  }
59
60
  const root = gated.root;
60
61
  const provider = options.useMock ? new MockModelProvider() : createModelProvider(loadAiConfig());
62
+ const progressEnabled = options.progressEnabled ?? !options.json;
63
+ const progress = createAskProgress({
64
+ enabled: progressEnabled,
65
+ ...(options.progressStream ? { stream: options.progressStream } : {}),
66
+ ...(options.progressIsTTY !== undefined ? { isTTY: options.progressIsTTY } : {}),
67
+ });
61
68
  const chat = new ChatService({
62
69
  root,
63
70
  provider,
64
71
  persistAudit: !options.useMock,
72
+ progress,
65
73
  });
66
74
  try {
67
75
  const response = await chat.ask(options.question);
76
+ progress.stop();
68
77
  if (options.json) {
69
78
  process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
70
79
  }
@@ -78,7 +87,14 @@ export async function runAskCommand(options) {
78
87
  }
79
88
  return EXIT_CODES.SUCCESS;
80
89
  }
90
+ catch (error) {
91
+ progress.stop();
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ console.error(`AgentDoctor error:\n${message}`);
94
+ return EXIT_CODES.INTERNAL_ERROR;
95
+ }
81
96
  finally {
97
+ progress.stop();
82
98
  await chat.end();
83
99
  }
84
100
  }
@@ -2,16 +2,21 @@ import { EXIT_CODES } from "../../types/index.js";
2
2
  import { initBrainStore, rebuildBrain, getBrainStatus } from "../../core/brain-cli/service.js";
3
3
  import { discoverProjectRoots } from "../../product/discovery/roots.js";
4
4
  import { buildProjectDna, persistProjectDna } from "../../product/dna/build.js";
5
- import { resolveRepoRoot } from "../../utils/path.js";
6
5
  import { resolveCliProjectRoot } from "../safe-root.js";
7
6
  function printJson(value) {
8
7
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
9
8
  }
10
9
  export async function runStartCommand(options) {
11
10
  const prefer = options.select ?? options.root;
11
+ // Fail closed before discovery walks anything (home/Desktop refusal must be instant).
12
+ const gated = await resolveCliProjectRoot(prefer);
13
+ if (!gated.ok) {
14
+ console.error(`Error: ${gated.message}`);
15
+ return gated.code;
16
+ }
12
17
  const discovery = await discoverProjectRoots({
13
- cwd: prefer ? resolveRepoRoot(prefer) : process.cwd(),
14
- ...(prefer ? { prefer: resolveRepoRoot(prefer) } : {}),
18
+ cwd: gated.root,
19
+ prefer: gated.root,
15
20
  ...(options.maxEntries !== undefined ? { maxEntries: options.maxEntries } : {}),
16
21
  autoSelect: !options.listOnly,
17
22
  });
@@ -26,13 +31,16 @@ export async function runStartCommand(options) {
26
31
  }
27
32
  process.stdout.write("Project candidates:\n");
28
33
  if (discovery.candidates.length === 0) {
29
- process.stdout.write(" (none found — navigate into a project or pass a path)\n");
34
+ process.stdout.write(` (none found under ${gated.root})\n`);
35
+ process.stdout.write(" Tip: open a project directory that contains source code or a manifest (package.json, composer.json, pyproject.toml, .git, …),\n");
36
+ process.stdout.write(` or run: agentdoctor start ${gated.root}\n`);
37
+ process.stdout.write(" Related: agentdoctor dna . # inspect what DNA sees in this folder\n");
30
38
  return EXIT_CODES.USAGE_ERROR;
31
39
  }
32
40
  for (const [i, c] of discovery.candidates.entries()) {
33
41
  process.stdout.write(` [${i + 1}] ${c.root}\n score=${c.score} markers=${c.markers.join(", ")} (${c.reason})\n`);
34
42
  }
35
- process.stdout.write("\nRe-run with --select <path> to initialize that project.\n");
43
+ process.stdout.write("\nMultiple projects detected — re-run with --select <path> (or pass an explicit path).\n");
36
44
  return EXIT_CODES.SUCCESS;
37
45
  }
38
46
  const root = discovery.selected.root;
@@ -50,8 +58,9 @@ export async function runStartCommand(options) {
50
58
  printJson(payload);
51
59
  }
52
60
  else {
61
+ process.stdout.write(`✓ Project detected: ${dna.name}\n`);
62
+ process.stdout.write(` Root: ${root}\n`);
53
63
  process.stdout.write(`AgentDoctor start\n`);
54
- process.stdout.write(` root: ${root}\n`);
55
64
  process.stdout.write(` fingerprint: ${dna.fingerprint}\n`);
56
65
  process.stdout.write(` type: ${dna.projectType}\n`);
57
66
  process.stdout.write(` languages: ${dna.languages.join(", ") || "(none)"}\n`);
@@ -1255,7 +1255,8 @@ export function createProgram() {
1255
1255
  .argument("<question>", "Question about the project")
1256
1256
  .argument("[path]", "Repository path (default: current directory)")
1257
1257
  .option("--json", "Emit JSON response", false)
1258
- .action(async (question, pathArg, options) => {
1258
+ .action(async (question, pathArg, _options, command) => {
1259
+ const options = command.optsWithGlobals();
1259
1260
  process.exitCode = await runAskCommand({
1260
1261
  question,
1261
1262
  root: resolveTargetArgument(pathArg),
@@ -1,5 +1,5 @@
1
1
  import type { AgentId } from "./types/index.js";
2
- export declare const PACKAGE_VERSION = "3.0.1";
2
+ export declare const PACKAGE_VERSION = "3.0.3";
3
3
  export declare const DEFAULT_MAX_FILE_SIZE_BYTES: number;
4
4
  /** Directories skipped during normal discovery (unless a rule needs them later). */
5
5
  export declare const DEFAULT_IGNORE_DIRECTORIES: Set<string>;
package/dist/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- export const PACKAGE_VERSION = "3.0.1";
1
+ export const PACKAGE_VERSION = "3.0.3";
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([
@@ -128,7 +128,6 @@ export class LocalBrainStore {
128
128
  await fs.mkdir(path.join(this.root, "deltas"), { recursive: true });
129
129
  }
130
130
  async readMeta() {
131
- await this.ensureRoot();
132
131
  try {
133
132
  const raw = await fs.readFile(this.metaPath(), "utf8");
134
133
  const parsed = JSON.parse(raw);
@@ -20,7 +20,6 @@ import { listKnowledge } from "../knowledge/store.js";
20
20
  import { CONTRACTS_VERSION } from "../contracts/index.js";
21
21
  import { collectOpsHealth } from "../ops/health.js";
22
22
  import { listPolicyPacks } from "../policy/packs.js";
23
- import { sanitizeForOutput } from "../utils/path.js";
24
23
  import { createModelProvider, loadAiConfig } from "../ai/index.js";
25
24
  import { ChatService } from "../agent/chat/service.js";
26
25
  import { formatChatResponseForCli } from "../agent/chat/response.js";
@@ -43,11 +42,15 @@ import { analyzeFeatureIntelligence } from "../product/features/intelligence.js"
43
42
  import { buildSoftwareEvolutionTimeline } from "../product/evolution/timeline.js";
44
43
  import { queryMemory } from "../product/memory/institutional.js";
45
44
  import { loadDecisionLedger } from "../product/decisions/ledger.js";
45
+ import { ProjectOwnershipError } from "../project/ownership.js";
46
+ import { PathEscapeError } from "../security/paths.js";
46
47
  import { htmlPage } from "./page.js";
47
- function safeJsonError(error) {
48
- const raw = error instanceof Error ? error.message : String(error);
49
- const { text } = redactSecrets(sanitizeForOutput(raw));
50
- return text.slice(0, 240);
48
+ /** HTTP-safe client error — never returns Error.message/stack (CodeQL js/stack-trace-exposure). */
49
+ function clientErrorMessage(error) {
50
+ if (error instanceof ProjectOwnershipError || error instanceof PathEscapeError) {
51
+ return "path outside project ownership";
52
+ }
53
+ return "internal error";
51
54
  }
52
55
  function pathnameLooksHostile(pathname) {
53
56
  const lower = pathname.toLowerCase();
@@ -386,11 +389,9 @@ export async function startDashboardServer(options) {
386
389
  sendJson(res, 200, await analyzeWhatIf(root, target));
387
390
  }
388
391
  catch (error) {
389
- const message = error instanceof Error ? error.message : String(error);
390
- const ownershipDenied = /ownership|escapes|path escape|PROJECT_OWNERSHIP/i.test(message) ||
391
- (error instanceof Error && error.name === "ProjectOwnershipError");
392
+ const ownershipDenied = error instanceof ProjectOwnershipError || error instanceof PathEscapeError;
392
393
  sendJson(res, ownershipDenied ? 400 : 500, {
393
- error: ownershipDenied ? "path outside project ownership" : message,
394
+ error: ownershipDenied ? "path outside project ownership" : "what-if analysis failed",
394
395
  });
395
396
  }
396
397
  return;
@@ -439,7 +440,7 @@ export async function startDashboardServer(options) {
439
440
  sendJson(res, 404, { error: "not found" });
440
441
  }
441
442
  catch (error) {
442
- sendJson(res, 500, { error: safeJsonError(error) });
443
+ sendJson(res, 500, { error: clientErrorMessage(error) });
443
444
  }
444
445
  });
445
446
  await new Promise((resolve, reject) => {
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { DEFAULT_IGNORE_DIRECTORIES } from "../../constants.js";
5
+ import { detectProject } from "../../detectors/project.js";
5
6
  import { isDirectory, pathExists } from "../../utils/fs.js";
6
7
  import { resolveRepoRoot } from "../../utils/path.js";
7
8
  /** Markers that strongly suggest a software project root. */
@@ -19,6 +20,8 @@ const PROJECT_MARKERS = [
19
20
  "pubspec.yaml",
20
21
  ".git",
21
22
  ];
23
+ /** Directory basenames that must never be treated as a project root candidate. */
24
+ const FORBIDDEN_ROOT_BASENAMES = new Set([".private", "agentdoctoros"]);
22
25
  const SUSPICIOUS_DIR_NAMES = new Set([
23
26
  "desktop",
24
27
  "downloads",
@@ -32,9 +35,19 @@ const SUSPICIOUS_DIR_NAMES = new Set([
32
35
  function normalizeHome(home) {
33
36
  return path.resolve(home);
34
37
  }
38
+ function pathsEqual(a, b) {
39
+ const left = path.resolve(a);
40
+ const right = path.resolve(b);
41
+ return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
42
+ }
35
43
  function isUnderHomeSubtree(abs, home, leaf) {
36
44
  const target = path.resolve(home, leaf);
37
45
  const resolved = path.resolve(abs);
46
+ if (process.platform === "win32") {
47
+ const t = target.toLowerCase();
48
+ const r = resolved.toLowerCase();
49
+ return r === t || r.startsWith(t + "\\");
50
+ }
38
51
  return resolved === target || resolved.startsWith(target + path.sep);
39
52
  }
40
53
  async function countEntriesShallow(dir, budget) {
@@ -92,7 +105,7 @@ export function classifyBroadUserScanRoot(cwdInput, homeInput) {
92
105
  const cwd = path.resolve(cwdInput);
93
106
  const home = normalizeHome(homeInput ?? os.homedir());
94
107
  const baselower = path.basename(cwd).toLowerCase();
95
- if (cwd === home ||
108
+ if (pathsEqual(cwd, home) ||
96
109
  SUSPICIOUS_DIR_NAMES.has(baselower) ||
97
110
  isUnderHomeSubtree(cwd, home, "Desktop") ||
98
111
  isUnderHomeSubtree(cwd, home, "Downloads") ||
@@ -113,12 +126,11 @@ export async function discoverProjectRoots(options = {}) {
113
126
  const home = normalizeHome(os.homedir());
114
127
  const maxEntries = options.maxEntries ?? 25_000;
115
128
  const limitations = [
116
- "Discovery uses shallow marker probes and bounded entry counts — not a full repository index.",
117
- "Candidates are VERIFIED only when marker files/directories exist on disk.",
129
+ "Discovery uses shallow marker probes, bounded entry counts, and detectProject for cwd/prefer when classic markers are absent.",
130
+ "Candidates are VERIFIED only when marker files exist or detectProject finds languages/source under that root.",
118
131
  ];
119
132
  const broad = classifyBroadUserScanRoot(cwd, home);
120
133
  if (broad.blocked) {
121
- const estimatedEntries = await countEntriesShallow(cwd, Math.min(maxEntries, 5_000));
122
134
  return {
123
135
  cwd,
124
136
  home,
@@ -126,7 +138,8 @@ export async function discoverProjectRoots(options = {}) {
126
138
  selected: null,
127
139
  blocked: true,
128
140
  blockReason: broad.reason,
129
- estimatedEntries,
141
+ // Do not walk the tree — counting home/Desktop can hang CI (esp. Windows).
142
+ estimatedEntries: 0,
130
143
  limitations,
131
144
  };
132
145
  }
@@ -145,13 +158,19 @@ export async function discoverProjectRoots(options = {}) {
145
158
  }
146
159
  const candidates = [];
147
160
  const seen = new Set();
161
+ function rejectForbiddenRoot(resolved) {
162
+ const base = path.basename(resolved).toLowerCase();
163
+ return FORBIDDEN_ROOT_BASENAMES.has(base);
164
+ }
148
165
  async function consider(dir, reason) {
149
166
  const resolved = path.resolve(dir);
150
167
  if (seen.has(resolved))
151
168
  return;
152
169
  if (!(await isDirectory(resolved)))
153
170
  return;
154
- if (resolved === home)
171
+ if (pathsEqual(resolved, home))
172
+ return;
173
+ if (rejectForbiddenRoot(resolved))
155
174
  return;
156
175
  const markers = await markersAt(resolved);
157
176
  if (markers.length === 0)
@@ -165,6 +184,46 @@ export async function discoverProjectRoots(options = {}) {
165
184
  reason,
166
185
  });
167
186
  }
187
+ /**
188
+ * When classic manifests/.git are absent, reuse the same detectProject path as DNA
189
+ * so `start` agrees with `dna` for valid software trees (e.g. language-only roots).
190
+ * Only applied to prefer/cwd — never to unbounded parent/child walks.
191
+ */
192
+ async function considerDetectProject(dir, reason) {
193
+ const resolved = path.resolve(dir);
194
+ if (seen.has(resolved))
195
+ return;
196
+ if (!(await isDirectory(resolved)))
197
+ return;
198
+ if (pathsEqual(resolved, home))
199
+ return;
200
+ if (rejectForbiddenRoot(resolved))
201
+ return;
202
+ if (classifyBroadUserScanRoot(resolved, home).blocked)
203
+ return;
204
+ let detection;
205
+ try {
206
+ detection = await detectProject(resolved);
207
+ }
208
+ catch {
209
+ return;
210
+ }
211
+ // detectProject returns ["unknown"] for empty trees — that is not a project signal.
212
+ const languages = detection.repository.languages.filter((l) => l !== "unknown");
213
+ const sourceFiles = detection.discovery.files.filter((f) => /\.(ts|tsx|js|jsx|mjs|cjs|py|php|go|rs|java|kt|kts|dart|rb|cs)$/i.test(f.relativePath)).length;
214
+ if (languages.length === 0 && sourceFiles === 0)
215
+ return;
216
+ seen.add(resolved);
217
+ const markers = languages.length > 0 ? languages.map((l) => `lang:${l}`) : [`source-files:${sourceFiles}`];
218
+ candidates.push({
219
+ root: resolved,
220
+ // Match strong marker score so a lone detectProject hit auto-selects like package.json.
221
+ score: languages.length > 0 ? 5 : 3,
222
+ markers,
223
+ truth: "VERIFIED",
224
+ reason,
225
+ });
226
+ }
168
227
  if (options.prefer) {
169
228
  await consider(options.prefer, "explicit prefer path");
170
229
  }
@@ -172,7 +231,7 @@ export async function discoverProjectRoots(options = {}) {
172
231
  // Walk up a few parents looking for markers (bounded).
173
232
  let parent = path.dirname(cwd);
174
233
  for (let i = 0; i < 4; i++) {
175
- if (parent === home || parent === path.dirname(parent))
234
+ if (pathsEqual(parent, home) || parent === path.dirname(parent))
176
235
  break;
177
236
  await consider(parent, "parent directory with project markers");
178
237
  parent = path.dirname(parent);
@@ -185,16 +244,63 @@ export async function discoverProjectRoots(options = {}) {
185
244
  continue;
186
245
  if (DEFAULT_IGNORE_DIRECTORIES.has(entry.name) || entry.name.startsWith("."))
187
246
  continue;
247
+ if (FORBIDDEN_ROOT_BASENAMES.has(entry.name.toLowerCase()))
248
+ continue;
188
249
  await consider(path.join(cwd, entry.name), "child directory with project markers");
189
250
  }
190
251
  }
191
252
  catch {
192
253
  limitations.push("Could not list child directories for candidate discovery.");
193
254
  }
255
+ // detectProject fallback (same engine as DNA) for prefer/cwd when no classic markers
256
+ // apply to that path AND no ancestor marker candidate already covers it (nested Case B).
257
+ // Also skip when child classic-marker projects already exist — otherwise detectProject
258
+ // would absorb sibling packages into a multi-project parent (start always passes prefer=cwd).
259
+ const preferAbs = options.prefer ? path.resolve(options.prefer) : null;
260
+ function ancestorMarkerCovers(target) {
261
+ return candidates.find((c) => {
262
+ if (pathsEqual(c.root, target))
263
+ return true;
264
+ const rootWithSep = c.root.endsWith(path.sep) ? c.root : c.root + path.sep;
265
+ if (process.platform === "win32") {
266
+ return target.toLowerCase().startsWith(rootWithSep.toLowerCase());
267
+ }
268
+ return target.startsWith(rootWithSep);
269
+ });
270
+ }
271
+ function descendantMarkerCandidates(target) {
272
+ const targetWithSep = target.endsWith(path.sep) ? target : target + path.sep;
273
+ return candidates.filter((c) => {
274
+ if (pathsEqual(c.root, target))
275
+ return false;
276
+ if (process.platform === "win32") {
277
+ return c.root.toLowerCase().startsWith(targetWithSep.toLowerCase());
278
+ }
279
+ return c.root.startsWith(targetWithSep);
280
+ });
281
+ }
282
+ if (preferAbs) {
283
+ // Skip detectProject absorption when 2+ classic child projects exist (multi-project
284
+ // parent). A single nested foreign repo must not block prefer's own language tree.
285
+ if (!ancestorMarkerCovers(preferAbs) && descendantMarkerCandidates(preferAbs).length < 2) {
286
+ await considerDetectProject(preferAbs, "explicit prefer path (detectProject)");
287
+ }
288
+ }
289
+ else if (candidates.length === 0) {
290
+ await considerDetectProject(cwd, "current working directory (detectProject)");
291
+ }
194
292
  candidates.sort((a, b) => b.score - a.score || a.root.localeCompare(b.root));
195
293
  let selected = null;
196
- if (options.prefer) {
197
- selected = candidates.find((c) => c.root === path.resolve(options.prefer)) ?? null;
294
+ if (preferAbs) {
295
+ const exact = candidates.find((c) => pathsEqual(c.root, preferAbs));
296
+ const covered = ancestorMarkerCovers(preferAbs);
297
+ // Nested dir under a marker project → select the marker root, not the nested leaf.
298
+ if (covered && !pathsEqual(covered.root, preferAbs)) {
299
+ selected = covered;
300
+ }
301
+ else {
302
+ selected = exact ?? covered ?? null;
303
+ }
198
304
  }
199
305
  else if (options.autoSelect !== false) {
200
306
  const strong = candidates.filter((c) => c.score >= 5);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praneeth_54/agentdoctor",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "Engineering assurance for AI coding agents — repository intelligence, change evidence, safety controls, and MCP tools.",
5
5
  "type": "module",
6
6
  "bin": {