@praneeth_54/agentdoctor 3.0.0 → 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.
Files changed (63) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/README.md +10 -8
  3. package/dist/agent/chat/deterministic.js +22 -4
  4. package/dist/agent/context/retrieve.d.ts +2 -1
  5. package/dist/agent/context/retrieve.js +14 -1
  6. package/dist/agent/tools/execute.js +39 -43
  7. package/dist/agent/tools/write.d.ts +1 -1
  8. package/dist/agent/tools/write.js +18 -4
  9. package/dist/ai/providers/adversarial-local.d.ts +7 -0
  10. package/dist/ai/providers/adversarial-local.js +146 -0
  11. package/dist/cli/commands/agent.js +13 -3
  12. package/dist/cli/commands/architecture.js +7 -2
  13. package/dist/cli/commands/brain.js +7 -2
  14. package/dist/cli/commands/chat.js +13 -3
  15. package/dist/cli/commands/complete.js +13 -3
  16. package/dist/cli/commands/fix.js +6 -0
  17. package/dist/cli/commands/learn.js +7 -2
  18. package/dist/cli/commands/mcp.js +7 -3
  19. package/dist/cli/commands/platform.js +7 -2
  20. package/dist/cli/commands/policy-graph-run.js +7 -0
  21. package/dist/cli/commands/product.js +13 -3
  22. package/dist/cli/commands/scan.js +6 -0
  23. package/dist/cli/commands/start.js +22 -7
  24. package/dist/cli/commands/v2.js +61 -11
  25. package/dist/cli/commands/verify.js +6 -0
  26. package/dist/cli/safe-root.d.ts +17 -0
  27. package/dist/cli/safe-root.js +32 -0
  28. package/dist/constants.d.ts +1 -1
  29. package/dist/constants.js +1 -1
  30. package/dist/core/brain-cli/service.js +5 -2
  31. package/dist/core/monorepo/detect.js +10 -0
  32. package/dist/core/secrets/scan.js +9 -0
  33. package/dist/core/understanding/brain/storage/store.d.ts +4 -0
  34. package/dist/core/understanding/brain/storage/store.js +22 -2
  35. package/dist/dashboard/page.d.ts +2 -0
  36. package/dist/dashboard/page.js +53 -0
  37. package/dist/dashboard/server.js +20 -244
  38. package/dist/dashboard/ui/client.d.ts +2 -0
  39. package/dist/dashboard/ui/client.js +2 -0
  40. package/dist/dashboard/ui/styles.d.ts +2 -0
  41. package/dist/dashboard/ui/styles.js +138 -0
  42. package/dist/discovery/files.js +13 -0
  43. package/dist/intelligence/graph/build.js +10 -30
  44. package/dist/intelligence/graph/incremental.d.ts +2 -0
  45. package/dist/intelligence/graph/incremental.js +9 -0
  46. package/dist/mcp/intelligence/path-safety.d.ts +6 -1
  47. package/dist/mcp/intelligence/path-safety.js +55 -2
  48. package/dist/platform/graph/build.js +9 -0
  49. package/dist/product/decisions/ledger.d.ts +13 -1
  50. package/dist/product/decisions/ledger.js +41 -10
  51. package/dist/product/discovery/roots.d.ts +10 -0
  52. package/dist/product/discovery/roots.js +137 -15
  53. package/dist/product/graph/enrich-languages.js +7 -32
  54. package/dist/product/index.d.ts +4 -1
  55. package/dist/product/index.js +2 -1
  56. package/dist/product/map/software-map.js +2 -1
  57. package/dist/product/memory/institutional.js +5 -0
  58. package/dist/product/search/software-search.js +5 -0
  59. package/dist/product/twin/store.js +9 -1
  60. package/dist/product/whatif/engine.js +22 -1
  61. package/dist/project/ownership.d.ts +65 -0
  62. package/dist/project/ownership.js +169 -0
  63. package/package.json +1 -1
@@ -3,7 +3,7 @@ import { stdin as input, stdout as output } from "node:process";
3
3
  import { ChatService, formatChatResponseForCli } from "../../agent/chat/index.js";
4
4
  import { MockModelProvider, createModelProvider, loadAiConfig } from "../../ai/index.js";
5
5
  import { EXIT_CODES } from "../../types/index.js";
6
- import { resolveRepoRoot } from "../../utils/path.js";
6
+ import { resolveCliProjectRoot } from "../safe-root.js";
7
7
  import { colors } from "../../utils/colors.js";
8
8
  function printBanner(options) {
9
9
  const lines = [
@@ -51,7 +51,12 @@ function printHelp() {
51
51
  ].join("\n"));
52
52
  }
53
53
  export async function runAskCommand(options) {
54
- const root = resolveRepoRoot(options.root ?? process.cwd());
54
+ const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
55
+ if (!gated.ok) {
56
+ console.error(`Error: ${gated.message}`);
57
+ return gated.code;
58
+ }
59
+ const root = gated.root;
55
60
  const provider = options.useMock ? new MockModelProvider() : createModelProvider(loadAiConfig());
56
61
  const chat = new ChatService({
57
62
  root,
@@ -78,7 +83,12 @@ export async function runAskCommand(options) {
78
83
  }
79
84
  }
80
85
  export async function runChatCommand(options) {
81
- const root = resolveRepoRoot(options.root ?? process.cwd());
86
+ const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
87
+ if (!gated.ok) {
88
+ console.error(`Error: ${gated.message}`);
89
+ return gated.code;
90
+ }
91
+ const root = gated.root;
82
92
  const provider = options.useMock ? new MockModelProvider() : createModelProvider(loadAiConfig());
83
93
  const chat = new ChatService({
84
94
  root,
@@ -1,5 +1,5 @@
1
1
  import { EXIT_CODES } from "../../types/index.js";
2
- import { resolveRepoRoot } from "../../utils/path.js";
2
+ import { resolveCliProjectRoot } from "../safe-root.js";
3
3
  import { runProjectInit } from "../../core/brain-product/init.js";
4
4
  import { buildIntelligenceGraph } from "../../intelligence/graph/build.js";
5
5
  import { analyzeGitIntelligence, deadCodeCategory } from "../../intelligence/git/analyze.js";
@@ -14,7 +14,12 @@ function printJson(value) {
14
14
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
15
15
  }
16
16
  export async function runInitCommand(options) {
17
- const root = resolveRepoRoot(options.root ?? process.cwd());
17
+ const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
18
+ if (!gated.ok) {
19
+ console.error(`Error: ${gated.message}`);
20
+ return gated.code;
21
+ }
22
+ const root = gated.root;
18
23
  const result = await runProjectInit(root, {
19
24
  ...(options.name ? { projectName: options.name } : {}),
20
25
  ...(options.domain ? { businessDomain: options.domain } : {}),
@@ -27,7 +32,12 @@ export async function runInitCommand(options) {
27
32
  return EXIT_CODES.SUCCESS;
28
33
  }
29
34
  export async function runV2SurfaceCommand(options) {
30
- const root = resolveRepoRoot(options.root ?? process.cwd());
35
+ const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
36
+ if (!gated.ok) {
37
+ console.error(`Error: ${gated.message}`);
38
+ return gated.code;
39
+ }
40
+ const root = gated.root;
31
41
  try {
32
42
  switch (options.action) {
33
43
  case "graph-ast": {
@@ -6,6 +6,7 @@ import { buildFixPlan } from "../../core/fix/plan.js";
6
6
  import { renderFixPlanTerminal } from "../../core/fix/render.js";
7
7
  import { runFix } from "../../core/fix/run.js";
8
8
  import { scan } from "../../core/scanner/scan.js";
9
+ import { classifyBroadUserScanRoot } from "../../product/discovery/roots.js";
9
10
  import { colors } from "../../utils/colors.js";
10
11
  /**
11
12
  * Safe Repository Mutation — apply allowlisted agent-config exclusions.
@@ -16,6 +17,11 @@ export async function runFixCommand(options) {
16
17
  console.error(`Error: not a directory: ${target}`);
17
18
  return EXIT_CODES.USAGE_ERROR;
18
19
  }
20
+ const broad = classifyBroadUserScanRoot(target);
21
+ if (broad.blocked) {
22
+ console.error(`Error: ${broad.reason}`);
23
+ return EXIT_CODES.USAGE_ERROR;
24
+ }
19
25
  const dryRun = options.dryRun === true;
20
26
  const yes = options.yes === true;
21
27
  try {
@@ -2,9 +2,14 @@ import { MockModelProvider, createModelProvider, loadAiConfig } from "../../ai/i
2
2
  import { StudentService } from "../../agent/student.js";
3
3
  import { parseAgentMode, defaultStudentMode } from "../../agent/modes.js";
4
4
  import { EXIT_CODES } from "../../types/index.js";
5
- import { resolveRepoRoot } from "../../utils/path.js";
5
+ import { resolveCliProjectRoot } from "../safe-root.js";
6
6
  export async function runLearnCommand(options) {
7
- const root = resolveRepoRoot(options.root ?? process.cwd());
7
+ const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
8
+ if (!gated.ok) {
9
+ console.error(`Error: ${gated.message}`);
10
+ return gated.code;
11
+ }
12
+ const root = gated.root;
8
13
  const mode = parseAgentMode(options.mode) ?? defaultStudentMode();
9
14
  const provider = options.useMock ? new MockModelProvider() : createModelProvider(loadAiConfig());
10
15
  const resolvedMode = options.build
@@ -1,13 +1,17 @@
1
1
  import { EXIT_CODES } from "../../types/index.js";
2
- import { resolveRepoRoot } from "../../utils/path.js";
2
+ import { resolveCliProjectRoot } from "../safe-root.js";
3
3
  import { runAgentDoctorMcpStdio } from "../../mcp/agentdoctor/server.js";
4
4
  /**
5
5
  * Start combined AgentDoctor MCP (Brain tools + intelligence tools) over STDIO.
6
6
  */
7
7
  export async function runMcpCommand(options) {
8
- const root = resolveRepoRoot(options.root);
8
+ const gated = await resolveCliProjectRoot(options.root);
9
+ if (!gated.ok) {
10
+ console.error(`Error: ${gated.message}`);
11
+ return gated.code;
12
+ }
9
13
  await runAgentDoctorMcpStdio({
10
- root,
14
+ root: gated.root,
11
15
  buildIfMissing: options.buildIfMissing !== false,
12
16
  });
13
17
  return EXIT_CODES.SUCCESS;
@@ -1,13 +1,18 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { EXIT_CODES } from "../../types/index.js";
3
- import { resolveRepoRoot } from "../../utils/path.js";
3
+ import { resolveCliProjectRoot } from "../safe-root.js";
4
4
  import { analyzeRenameImpact, analyzeTestImpact, appendSessionEvent, buildProvenance, compareCommits, createSession, endSession, evaluateAgentAction, exportSessionMarkdown, formatTestImpactHuman, listSessions, loadSession, persistTestImpactReport, planContext, runPlatformScan, saveProvenance, EVALUATE_ONLY, } from "../../platform/index.js";
5
5
  import { buildRepositoryGraph } from "../../platform/graph/build.js";
6
6
  function printJson(value) {
7
7
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
8
8
  }
9
9
  export async function runPlatformCommand(options) {
10
- const root = resolveRepoRoot(options.root ?? process.cwd());
10
+ const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
11
+ if (!gated.ok) {
12
+ console.error(`Error: ${gated.message}`);
13
+ return gated.code;
14
+ }
15
+ const root = gated.root;
11
16
  try {
12
17
  switch (options.action) {
13
18
  case "scan": {
@@ -3,6 +3,7 @@ import { runControlledCommand } from "../../enforcement/runner.js";
3
3
  import { graphBuild, graphRebuild, graphStatus, graphUpdate, } from "../../intelligence/graph/incremental.js";
4
4
  import { buildIntelligenceGraph } from "../../intelligence/graph/build.js";
5
5
  import { EXIT_CODES } from "../../types/index.js";
6
+ import { resolveCliProjectRoot } from "../safe-root.js";
6
7
  function emit(json, value, human) {
7
8
  if (json) {
8
9
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
@@ -13,6 +14,12 @@ function emit(json, value, human) {
13
14
  }
14
15
  export async function runGraphSurfaceCommand(options) {
15
16
  try {
17
+ const gated = await resolveCliProjectRoot(options.root);
18
+ if (!gated.ok) {
19
+ console.error(`Error: ${gated.message}`);
20
+ return gated.code;
21
+ }
22
+ options = { ...options, root: gated.root };
16
23
  const mode = options.mode ?? "auto";
17
24
  if (options.action === "status") {
18
25
  const status = await graphStatus({ root: options.root });
@@ -1,5 +1,5 @@
1
1
  import { EXIT_CODES } from "../../types/index.js";
2
- import { resolveRepoRoot } from "../../utils/path.js";
2
+ import { resolveCliProjectRoot } from "../safe-root.js";
3
3
  import { resolveTargetArgument } from "./scan.js";
4
4
  import { traceRequirements, analyzeApiSurface, analyzeDatabaseSchema, analyzeEvents, analyzeDependencies, analyzeCodeHealth, buildSoftwareMap, analyzeWhatIf, loadDecisionLedger, runForensicAnalysis, buildSoftwareDigitalTwin, runEvalLab, diagnoseAgentDoctorSelf, analyzeInfra, buildIncidentHypotheses, readChangeLedger, analyzeSecuritySurface, analyzePrivacySurface, searchSymbolsAndConcepts, buildTechnicalDebtRoadmap, analyzeFeatureIntelligence, buildSoftwareEvolutionTimeline, queryMemory, loadOrganizationModel, analyzeTestBrain, } from "../../product/index.js";
5
5
  import { runRoleAgent } from "../../agent/roles.js";
@@ -7,7 +7,12 @@ function printJson(value) {
7
7
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
8
8
  }
9
9
  export async function runProductCommand(options) {
10
- const root = resolveRepoRoot(options.root ?? resolveTargetArgument(undefined));
10
+ const gated = await resolveCliProjectRoot(options.root ?? resolveTargetArgument(undefined));
11
+ if (!gated.ok) {
12
+ console.error(`Error: ${gated.message}`);
13
+ return gated.code;
14
+ }
15
+ const root = gated.root;
11
16
  try {
12
17
  let payload;
13
18
  switch (options.action) {
@@ -137,7 +142,12 @@ export async function runProductCommand(options) {
137
142
  }
138
143
  }
139
144
  export async function runChangeLedgerListCommand(options) {
140
- const root = resolveRepoRoot(options.root ?? process.cwd());
145
+ const gated = await resolveCliProjectRoot(options.root);
146
+ if (!gated.ok) {
147
+ console.error(`Error: ${gated.message}`);
148
+ return gated.code;
149
+ }
150
+ const root = gated.root;
141
151
  const payload = await readChangeLedger(root);
142
152
  if (options.json)
143
153
  printJson(payload);
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { evaluateScanPolicy } from "../../core/policy/evaluate.js";
3
3
  import { scan } from "../../core/scanner/scan.js";
4
+ import { classifyBroadUserScanRoot } from "../../product/discovery/roots.js";
4
5
  import { emitGithubReports } from "../../reporters/github/emit.js";
5
6
  import { renderJsonReport } from "../../reporters/json/report.js";
6
7
  import { renderTerminalReport } from "../../reporters/terminal/report.js";
@@ -13,6 +14,11 @@ export async function runScanCommand(options) {
13
14
  console.error(`Error: not a directory: ${target}`);
14
15
  return EXIT_CODES.USAGE_ERROR;
15
16
  }
17
+ const broad = classifyBroadUserScanRoot(target);
18
+ if (broad.blocked) {
19
+ console.error(`Error: ${broad.reason}`);
20
+ return EXIT_CODES.USAGE_ERROR;
21
+ }
16
22
  try {
17
23
  const result = await scan({
18
24
  cwd: target,
@@ -2,15 +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";
5
+ import { resolveCliProjectRoot } from "../safe-root.js";
6
6
  function printJson(value) {
7
7
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
8
8
  }
9
9
  export async function runStartCommand(options) {
10
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
+ }
11
17
  const discovery = await discoverProjectRoots({
12
- cwd: prefer ? resolveRepoRoot(prefer) : process.cwd(),
13
- ...(prefer ? { prefer: resolveRepoRoot(prefer) } : {}),
18
+ cwd: gated.root,
19
+ prefer: gated.root,
14
20
  ...(options.maxEntries !== undefined ? { maxEntries: options.maxEntries } : {}),
15
21
  autoSelect: !options.listOnly,
16
22
  });
@@ -25,13 +31,16 @@ export async function runStartCommand(options) {
25
31
  }
26
32
  process.stdout.write("Project candidates:\n");
27
33
  if (discovery.candidates.length === 0) {
28
- 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");
29
38
  return EXIT_CODES.USAGE_ERROR;
30
39
  }
31
40
  for (const [i, c] of discovery.candidates.entries()) {
32
41
  process.stdout.write(` [${i + 1}] ${c.root}\n score=${c.score} markers=${c.markers.join(", ")} (${c.reason})\n`);
33
42
  }
34
- 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");
35
44
  return EXIT_CODES.SUCCESS;
36
45
  }
37
46
  const root = discovery.selected.root;
@@ -49,8 +58,9 @@ export async function runStartCommand(options) {
49
58
  printJson(payload);
50
59
  }
51
60
  else {
61
+ process.stdout.write(`✓ Project detected: ${dna.name}\n`);
62
+ process.stdout.write(` Root: ${root}\n`);
52
63
  process.stdout.write(`AgentDoctor start\n`);
53
- process.stdout.write(` root: ${root}\n`);
54
64
  process.stdout.write(` fingerprint: ${dna.fingerprint}\n`);
55
65
  process.stdout.write(` type: ${dna.projectType}\n`);
56
66
  process.stdout.write(` languages: ${dna.languages.join(", ") || "(none)"}\n`);
@@ -64,7 +74,12 @@ export async function runStartCommand(options) {
64
74
  return EXIT_CODES.SUCCESS;
65
75
  }
66
76
  export async function runDnaCommand(options) {
67
- const root = resolveRepoRoot(options.root ?? process.cwd());
77
+ const gated = await resolveCliProjectRoot(options.root);
78
+ if (!gated.ok) {
79
+ console.error(`Error: ${gated.message}`);
80
+ return gated.code;
81
+ }
82
+ const root = gated.root;
68
83
  const dna = options.persist ? await persistProjectDna(root) : await buildProjectDna(root);
69
84
  if (options.json) {
70
85
  printJson(dna);
@@ -1,5 +1,5 @@
1
1
  import { EXIT_CODES } from "../../types/index.js";
2
- import { resolveRepoRoot } from "../../utils/path.js";
2
+ import { resolveCliProjectRoot } from "../safe-root.js";
3
3
  import { analyzeChanges } from "../../core/changes/analyze.js";
4
4
  import { analyzeContextHealth } from "../../core/context-health/analyze.js";
5
5
  import { scanSecrets } from "../../core/secrets/scan.js";
@@ -16,7 +16,12 @@ function printJson(value) {
16
16
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
17
17
  }
18
18
  export async function runChangesCommand(options) {
19
- const root = resolveRepoRoot(options.root ?? process.cwd());
19
+ const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
20
+ if (!gated.ok) {
21
+ console.error(`Error: ${gated.message}`);
22
+ return gated.code;
23
+ }
24
+ const root = gated.root;
20
25
  const report = await analyzeChanges({
21
26
  root,
22
27
  ...(options.since ? { since: options.since } : {}),
@@ -38,7 +43,12 @@ export async function runChangesCommand(options) {
38
43
  return EXIT_CODES.SUCCESS;
39
44
  }
40
45
  export async function runContextHealthCommand(options) {
41
- const root = resolveRepoRoot(options.root ?? process.cwd());
46
+ const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
47
+ if (!gated.ok) {
48
+ console.error(`Error: ${gated.message}`);
49
+ return gated.code;
50
+ }
51
+ const root = gated.root;
42
52
  const report = await analyzeContextHealth(root);
43
53
  if (options.json)
44
54
  printJson(report);
@@ -51,7 +61,12 @@ export async function runContextHealthCommand(options) {
51
61
  return EXIT_CODES.SUCCESS;
52
62
  }
53
63
  export async function runSecretsCommand(options) {
54
- const root = resolveRepoRoot(options.root ?? process.cwd());
64
+ const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
65
+ if (!gated.ok) {
66
+ console.error(`Error: ${gated.message}`);
67
+ return gated.code;
68
+ }
69
+ const root = gated.root;
55
70
  const report = await scanSecrets({ root, enabled: true });
56
71
  if (options.json)
57
72
  printJson(report);
@@ -67,8 +82,13 @@ export async function runSecretsCommand(options) {
67
82
  }
68
83
  export async function runFixUndoCommand(options) {
69
84
  try {
85
+ const gated = await resolveCliProjectRoot(options.root);
86
+ if (!gated.ok) {
87
+ console.error(`Error: ${gated.message}`);
88
+ return gated.code;
89
+ }
70
90
  const record = await undoFix({
71
- root: resolveRepoRoot(options.root ?? process.cwd()),
91
+ root: gated.root,
72
92
  auditId: options.auditId,
73
93
  });
74
94
  process.stdout.write(`Undid Safe Fix audit ${options.auditId} → undo audit ${record.id}\n`);
@@ -80,7 +100,12 @@ export async function runFixUndoCommand(options) {
80
100
  }
81
101
  }
82
102
  export async function runFixHistoryCommand(options) {
83
- const audits = await listFixAudits(resolveRepoRoot(options.root ?? process.cwd()));
103
+ const gated = await resolveCliProjectRoot(options.root);
104
+ if (!gated.ok) {
105
+ console.error(`Error: ${gated.message}`);
106
+ return gated.code;
107
+ }
108
+ const audits = await listFixAudits(gated.root);
84
109
  if (options.json)
85
110
  printJson({ audits });
86
111
  else if (audits.length === 0)
@@ -93,7 +118,12 @@ export async function runFixHistoryCommand(options) {
93
118
  return EXIT_CODES.SUCCESS;
94
119
  }
95
120
  export async function runBaselineCommand(options) {
96
- const root = resolveRepoRoot(options.root ?? process.cwd());
121
+ const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
122
+ if (!gated.ok) {
123
+ console.error(`Error: ${gated.message}`);
124
+ return gated.code;
125
+ }
126
+ const root = gated.root;
97
127
  try {
98
128
  if (options.action === "list") {
99
129
  const names = await listNamedBaselines(root);
@@ -167,7 +197,12 @@ export async function runBaselineCommand(options) {
167
197
  }
168
198
  }
169
199
  export async function runPackagesCommand(options) {
170
- const root = resolveRepoRoot(options.root ?? process.cwd());
200
+ const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
201
+ if (!gated.ok) {
202
+ console.error(`Error: ${gated.message}`);
203
+ return gated.code;
204
+ }
205
+ const root = gated.root;
171
206
  const mono = await detectMonorepo(root);
172
207
  if (!options.scan) {
173
208
  if (options.json)
@@ -200,8 +235,13 @@ export async function runPackagesCommand(options) {
200
235
  return EXIT_CODES.SUCCESS;
201
236
  }
202
237
  export async function runPrReviewCommand(options) {
238
+ const gated = await resolveCliProjectRoot(options.root);
239
+ if (!gated.ok) {
240
+ console.error(`Error: ${gated.message}`);
241
+ return gated.code;
242
+ }
203
243
  const report = await analyzePullRequest({
204
- root: resolveRepoRoot(options.root ?? process.cwd()),
244
+ root: gated.root,
205
245
  dryRun: true,
206
246
  ...(options.base ? { baseRef: options.base } : {}),
207
247
  });
@@ -215,8 +255,13 @@ export async function runPrReviewCommand(options) {
215
255
  }
216
256
  export async function runDashboardCommand(options) {
217
257
  try {
258
+ const gated = await resolveCliProjectRoot(options.root);
259
+ if (!gated.ok) {
260
+ console.error(`Error: ${gated.message}`);
261
+ return gated.code;
262
+ }
218
263
  const server = await startDashboardServer({
219
- root: resolveRepoRoot(options.root ?? process.cwd()),
264
+ root: gated.root,
220
265
  host: options.host ?? "127.0.0.1",
221
266
  port: options.port ?? 8787,
222
267
  readOnly: true,
@@ -237,7 +282,12 @@ export async function runDashboardCommand(options) {
237
282
  }
238
283
  }
239
284
  export async function runPluginsCommand(options) {
240
- const root = resolveRepoRoot(options.root ?? process.cwd());
285
+ const gated = await resolveCliProjectRoot(options.root);
286
+ if (!gated.ok) {
287
+ console.error(`Error: ${gated.message}`);
288
+ return gated.code;
289
+ }
290
+ const root = gated.root;
241
291
  const plugins = await discoverPlugins(root);
242
292
  const analyzerResults = options.run ? await runPluginAnalyzers(root) : undefined;
243
293
  if (options.json) {
@@ -1,5 +1,6 @@
1
1
  import { evaluateVerifyPolicy } from "../../core/policy/evaluate.js";
2
2
  import { verify } from "../../core/verify/verify.js";
3
+ import { classifyBroadUserScanRoot } from "../../product/discovery/roots.js";
3
4
  import { emitGithubReports } from "../../reporters/github/emit.js";
4
5
  import { renderVerifyJsonReport } from "../../reporters/verify/json.js";
5
6
  import { renderVerifyTerminalReport } from "../../reporters/verify/terminal.js";
@@ -15,6 +16,11 @@ export async function runVerifyCommand(options) {
15
16
  console.error(`Error: not a directory: ${target}`);
16
17
  return EXIT_CODES.USAGE_ERROR;
17
18
  }
19
+ const broad = classifyBroadUserScanRoot(target);
20
+ if (broad.blocked) {
21
+ console.error(`Error: ${broad.reason}`);
22
+ return EXIT_CODES.USAGE_ERROR;
23
+ }
18
24
  try {
19
25
  const result = await verify({
20
26
  cwd: target,
@@ -0,0 +1,17 @@
1
+ import { type ExitCode } from "../types/index.js";
2
+ export type CliRootOk = {
3
+ ok: true;
4
+ root: string;
5
+ };
6
+ export type CliRootErr = {
7
+ ok: false;
8
+ message: string;
9
+ code: ExitCode;
10
+ };
11
+ /**
12
+ * Resolve a CLI project root and refuse home / Desktop / Downloads / Documents
13
+ * (and similarly broad user folders) before any recursive intelligence work.
14
+ */
15
+ export declare function resolveCliProjectRoot(pathArg?: string): Promise<CliRootOk | CliRootErr>;
16
+ /** Sync variant when the path is already known to exist (tests / internal). */
17
+ export declare function assertCliProjectRootSync(pathArg?: string): CliRootOk | CliRootErr;
@@ -0,0 +1,32 @@
1
+ import { classifyBroadUserScanRoot } from "../product/discovery/roots.js";
2
+ import { EXIT_CODES } from "../types/index.js";
3
+ import { isDirectory } from "../utils/fs.js";
4
+ import { resolveRepoRoot } from "../utils/path.js";
5
+ /**
6
+ * Resolve a CLI project root and refuse home / Desktop / Downloads / Documents
7
+ * (and similarly broad user folders) before any recursive intelligence work.
8
+ */
9
+ export async function resolveCliProjectRoot(pathArg) {
10
+ const root = resolveRepoRoot(pathArg ?? process.cwd());
11
+ if (!(await isDirectory(root))) {
12
+ return {
13
+ ok: false,
14
+ message: `not a directory: ${root}`,
15
+ code: EXIT_CODES.USAGE_ERROR,
16
+ };
17
+ }
18
+ const broad = classifyBroadUserScanRoot(root);
19
+ if (broad.blocked) {
20
+ return { ok: false, message: broad.reason, code: EXIT_CODES.USAGE_ERROR };
21
+ }
22
+ return { ok: true, root };
23
+ }
24
+ /** Sync variant when the path is already known to exist (tests / internal). */
25
+ export function assertCliProjectRootSync(pathArg) {
26
+ const root = resolveRepoRoot(pathArg ?? process.cwd());
27
+ const broad = classifyBroadUserScanRoot(root);
28
+ if (broad.blocked) {
29
+ return { ok: false, message: broad.reason, code: EXIT_CODES.USAGE_ERROR };
30
+ }
31
+ return { ok: true, root };
32
+ }
@@ -1,5 +1,5 @@
1
1
  import type { AgentId } from "./types/index.js";
2
- export declare const PACKAGE_VERSION = "3.0.0";
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.0";
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([
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { LocalBrainStore, createBrainQueryEngine, redactBrainForStorage, } from "../understanding/brain/index.js";
4
4
  import { compileProjectBrain } from "../../mcp/brain/compile.js";
5
+ import { OWNERSHIP_BOUNDARY_VERSION } from "../../project/ownership.js";
5
6
  import { resolveRepoRoot } from "../../utils/path.js";
6
7
  import { isDirectory } from "../../utils/fs.js";
7
8
  function storeFor(root) {
@@ -16,11 +17,13 @@ export async function getBrainStatus(root) {
16
17
  const storeRoot = path.join(resolved, ".agentdoctor", "project-brain");
17
18
  try {
18
19
  const meta = await store.readMeta();
20
+ const boundaryOk = meta.ownershipBoundaryVersion === OWNERSHIP_BOUNDARY_VERSION;
21
+ const hasUsableLatest = boundaryOk && meta.latestSnapshotId !== null;
19
22
  return {
20
23
  root: resolved,
21
24
  storeRoot,
22
- hasSnapshot: meta.latestSnapshotId !== null,
23
- latestSnapshotId: meta.latestSnapshotId,
25
+ hasSnapshot: hasUsableLatest,
26
+ latestSnapshotId: hasUsableLatest ? meta.latestSnapshotId : null,
24
27
  snapshotCount: meta.snapshots.length,
25
28
  projectName: meta.projectName || null,
26
29
  schemaVersion: meta.schemaVersion || null,
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { classifyRelativePathOwnership, isUnderNestedRepository } from "../../project/ownership.js";
3
4
  import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
4
5
  async function readJson(file) {
5
6
  try {
@@ -99,6 +100,15 @@ export async function detectMonorepo(rootInput) {
99
100
  continue;
100
101
  if (seen.has(pkg.absolutePath))
101
102
  continue;
103
+ const ownership = classifyRelativePathOwnership(pkg.relativePath);
104
+ if (ownership !== "project_owned") {
105
+ limitations.push(`Skipped non-owned workspace package: ${pkg.relativePath} (${ownership})`);
106
+ continue;
107
+ }
108
+ if (await isUnderNestedRepository(root, pkg.absolutePath)) {
109
+ limitations.push(`Skipped nested-repository workspace package: ${pkg.relativePath}`);
110
+ continue;
111
+ }
102
112
  seen.add(pkg.absolutePath);
103
113
  packages.push(pkg);
104
114
  }
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { decideDirectoryTraversal } from "../../project/ownership.js";
3
4
  import { readTextFile } from "../../utils/fs.js";
4
5
  import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
5
6
  const SECRET_PATTERNS = [
@@ -94,6 +95,14 @@ async function walkFiles(root, dir, out, limit) {
94
95
  if (entry.isSymbolicLink())
95
96
  continue;
96
97
  if (entry.isDirectory()) {
98
+ const relativeDir = toPosixRelative(root, absolute);
99
+ const traversal = await decideDirectoryTraversal({
100
+ projectRoot: root,
101
+ absoluteDir: absolute,
102
+ relativeDir,
103
+ });
104
+ if (!traversal.traverse)
105
+ continue;
97
106
  await walkFiles(root, absolute, out, limit);
98
107
  continue;
99
108
  }
@@ -9,6 +9,8 @@ export interface SnapshotMeta {
9
9
  projectName: string;
10
10
  brainId: string;
11
11
  checksum: string;
12
+ /** Snapshots without this stamp (or with a mismatch) must not be served as current truth. */
13
+ ownershipBoundaryVersion?: number;
12
14
  }
13
15
  export interface BrainStoreMeta {
14
16
  storageFormatVersion: BrainStorageFormatVersion;
@@ -16,6 +18,8 @@ export interface BrainStoreMeta {
16
18
  projectName: string;
17
19
  latestSnapshotId: string | null;
18
20
  snapshots: SnapshotMeta[];
21
+ /** When missing or mismatched, latest brain is treated as stale vs current ownership rules. */
22
+ ownershipBoundaryVersion?: number;
19
23
  }
20
24
  export interface SnapshotComparison {
21
25
  leftId: string;