@praneeth_54/agentdoctor 3.0.1 → 3.0.2

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.0.2] — 2026-09-27
11
+
12
+ Patch release: fix current-directory project discovery for `agentdoctor start`.
13
+
14
+ Public relationship:
15
+
16
+ - **3.0.0** and **3.0.1** remain immutable public releases.
17
+ - **3.0.2** does not rewrite prior npm/GitHub history.
18
+
19
+ ### Fixed
20
+
21
+ - `agentdoctor start` no longer reports `(none found)` for valid software trees that lack classic manifests (e.g. language-only roots without `package.json` / `.git`).
22
+ - Discovery fallback now reuses the same deterministic `detectProject` engine as `dna`, so `start` and `dna` agree on the project root.
23
+ - Clearer discovery UX (`✓ Project detected`) and empty-state guidance.
24
+ - Reject empty trees that only report `unknown` language; do not silently absorb multi-project parents with 2+ child marker projects.
25
+ - Ownership, containment, HOME/broad-root, `.private`, nested-repo, and symlink protections remain enforced.
26
+
27
+ ### Tests
28
+
29
+ - Added `tests/unit/project/start-discovery.test.ts` regression coverage for markerless cwd, nested roots, multi-project parents, `.private`, and home refusal.
30
+
10
31
  ## [3.0.1] — 2026-09-27
11
32
 
12
33
  Production-hardening follow-up on the reviewed freeze remediation commit
@@ -601,7 +622,8 @@ First public beta.
601
622
  - Not a complete secret scanner
602
623
  - Git “tracked secret” detection deferred
603
624
 
604
- [Unreleased]: https://github.com/pranee54/AgentDoctor/compare/v3.0.1...HEAD
625
+ [Unreleased]: https://github.com/pranee54/AgentDoctor/compare/v3.0.2...HEAD
626
+ [3.0.2]: https://github.com/pranee54/AgentDoctor/compare/v3.0.1...v3.0.2
605
627
  [3.0.1]: https://github.com/pranee54/AgentDoctor/compare/v3.0.0...v3.0.1
606
628
  [3.0.0]: https://github.com/pranee54/AgentDoctor/compare/v2.1.0...v3.0.0
607
629
  [2.1.0]: https://github.com/pranee54/AgentDoctor/compare/v2.0.1...v2.1.0
package/README.md CHANGED
@@ -281,15 +281,15 @@ Student path: `learn` → ask → viva → docs → Build With Me (with `--appro
281
281
  Requires **Node.js 20+**.
282
282
 
283
283
  ```bash
284
- npm install -g @praneeth_54/agentdoctor@3.0.1
285
- agentdoctor --version # 3.0.1
284
+ npm install -g @praneeth_54/agentdoctor@3.0.2
285
+ agentdoctor --version # 3.0.2
286
286
  agentdoctor --help
287
287
  ```
288
288
 
289
289
  Or without global install:
290
290
 
291
291
  ```bash
292
- npx @praneeth_54/agentdoctor@3.0.1 --help
292
+ npx @praneeth_54/agentdoctor@3.0.2 --help
293
293
  ```
294
294
 
295
295
  ---
@@ -298,7 +298,7 @@ npx @praneeth_54/agentdoctor@3.0.1 --help
298
298
 
299
299
  ```bash
300
300
  # 1. Install
301
- npm install -g @praneeth_54/agentdoctor@3.0.1
301
+ npm install -g @praneeth_54/agentdoctor@3.0.2
302
302
 
303
303
  # 2. Enter YOUR project (not your home folder)
304
304
  cd /path/to/my-project
@@ -581,11 +581,12 @@ Post-acceptance ideas for later releases: maintainers see [docs/internal/POST_3_
581
581
 
582
582
  ## Roadmap
583
583
 
584
- | Track | Meaning |
585
- | --------------------- | ------------------------------------------------------------------------- |
586
- | **3.0.0 (released)** | Original public release (`v3.0.0` → `c21faf1`) |
587
- | **3.0.1 (candidate)** | Freeze remediation / ownership & security hardening follow-up (`319810a`) |
588
- | **Coming soon** | Maturity, language depth, runtime adapters, DX — not promised ship dates |
584
+ | Track | Meaning |
585
+ | -------------------- | ------------------------------------------------------------------------ |
586
+ | **3.0.0 (released)** | Original public release (`v3.0.0` → `c21faf1`) |
587
+ | **3.0.1 (released)** | Freeze remediation / ownership & security hardening follow-up |
588
+ | **3.0.2 (released)** | `start` discovery aligned with deterministic `detectProject` |
589
+ | **Coming soon** | Maturity, language depth, runtime adapters, DX — not promised ship dates |
589
590
 
590
591
  ---
591
592
 
@@ -5,11 +5,29 @@ import { buildIntelligenceGraph } from "../../intelligence/graph/build.js";
5
5
  import { resolveRepoRoot } from "../../utils/path.js";
6
6
  import { retrieveProjectContext } from "../context/retrieve.js";
7
7
  import { buildChatTurnResponse } from "./response.js";
8
- const ARCH_RE = /architecture|structure|module|layout|how.*(project|repo|codebase)|explain.*(project|repo|codebase|this)|what (is|does) (this|my) project|project overview|understand (my |this )?project/i;
8
+ const ARCH_RE = /architecture|structure|module|layout|what (is|does) (this|my) project|project overview|understand (my |this )?project/i;
9
9
  const AUTH_RE = /auth|login|password|session|jwt|oauth/i;
10
10
  const DEPS_RE = /depend|package|lockfile|npm|yarn|pnpm/i;
11
- const START_RE = /where.*(start|entry|boot|main)|application start|entry\s*point/i;
12
11
  const DB_RE = /database|db\.|sql|prisma|query\(/i;
12
+ /** ReDoS-safe: avoid `where.*…` / `how.*…` backtracking on uncontrolled chat input. */
13
+ function isStartQuestion(q) {
14
+ if (/application start|entry\s*point/i.test(q))
15
+ return true;
16
+ const lower = q.toLowerCase();
17
+ if (!lower.includes("where"))
18
+ return false;
19
+ return /\b(start|entry|boot|main)\b/i.test(q);
20
+ }
21
+ function isArchitectureQuestion(q) {
22
+ if (ARCH_RE.test(q))
23
+ return true;
24
+ const lower = q.toLowerCase();
25
+ if (lower.includes("how") && /(?:project|repo|codebase)/i.test(q))
26
+ return true;
27
+ if (lower.includes("explain") && /(?:project|repo|codebase|\bthis\b)/i.test(q))
28
+ return true;
29
+ return false;
30
+ }
13
31
  export async function answerDeterministicProjectQuestion(options) {
14
32
  const root = resolveRepoRoot(options.root);
15
33
  const q = options.question.trim();
@@ -61,7 +79,7 @@ export async function answerDeterministicProjectQuestion(options) {
61
79
  }
62
80
  sections.push("");
63
81
  }
64
- if (START_RE.test(q)) {
82
+ if (isStartQuestion(q)) {
65
83
  const startHits = context.citations
66
84
  .filter((c) => c.path && /(server|index|main|app)\.[jt]sx?$/i.test(c.path))
67
85
  .slice(0, 6);
@@ -139,7 +157,7 @@ export async function answerDeterministicProjectQuestion(options) {
139
157
  sections.push(`Lockfiles: ${deps.lockfilesPresent.join(", ") || "none"}`);
140
158
  sections.push("");
141
159
  }
142
- if (ARCH_RE.test(q)) {
160
+ if (isArchitectureQuestion(q)) {
143
161
  try {
144
162
  const graph = await buildIntelligenceGraph({ root, mode: "auto" });
145
163
  sections.push(`[INFERRED] Graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges (${graph.builder})`);
@@ -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`);
@@ -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.2";
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.2";
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([
@@ -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.2",
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": {