@praneeth_54/agentdoctor 2.1.0 → 3.0.1

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 (167) hide show
  1. package/CHANGELOG.md +86 -3
  2. package/README.md +490 -290
  3. package/dist/agent/chat/deterministic.d.ts +6 -0
  4. package/dist/agent/chat/deterministic.js +180 -0
  5. package/dist/agent/chat/service.js +8 -14
  6. package/dist/agent/context/retrieve.d.ts +2 -1
  7. package/dist/agent/context/retrieve.js +14 -1
  8. package/dist/agent/index.d.ts +2 -0
  9. package/dist/agent/index.js +1 -0
  10. package/dist/agent/loop.d.ts +2 -0
  11. package/dist/agent/loop.js +31 -0
  12. package/dist/agent/roles.d.ts +12 -0
  13. package/dist/agent/roles.js +138 -0
  14. package/dist/agent/runtime.d.ts +21 -5
  15. package/dist/agent/runtime.js +141 -28
  16. package/dist/agent/tools/execute.d.ts +2 -0
  17. package/dist/agent/tools/execute.js +49 -43
  18. package/dist/agent/tools/write.d.ts +1 -1
  19. package/dist/agent/tools/write.js +18 -4
  20. package/dist/ai/config.js +1 -0
  21. package/dist/ai/providers/adversarial-local.d.ts +7 -0
  22. package/dist/ai/providers/adversarial-local.js +146 -0
  23. package/dist/ai/redact.d.ts +1 -0
  24. package/dist/ai/redact.js +47 -7
  25. package/dist/ai/types.d.ts +1 -1
  26. package/dist/cli/commands/agent.d.ts +2 -0
  27. package/dist/cli/commands/agent.js +32 -11
  28. package/dist/cli/commands/architecture.js +7 -2
  29. package/dist/cli/commands/brain.js +7 -2
  30. package/dist/cli/commands/chat.js +15 -7
  31. package/dist/cli/commands/complete.js +13 -3
  32. package/dist/cli/commands/fix.js +6 -0
  33. package/dist/cli/commands/learn.js +7 -2
  34. package/dist/cli/commands/mcp.js +7 -3
  35. package/dist/cli/commands/platform.js +7 -2
  36. package/dist/cli/commands/policy-graph-run.js +7 -0
  37. package/dist/cli/commands/product.d.ts +19 -0
  38. package/dist/cli/commands/product.js +157 -0
  39. package/dist/cli/commands/scan.js +6 -0
  40. package/dist/cli/commands/start.d.ts +15 -0
  41. package/dist/cli/commands/start.js +86 -0
  42. package/dist/cli/commands/v2.js +61 -11
  43. package/dist/cli/commands/verify.js +6 -0
  44. package/dist/cli/program.js +141 -0
  45. package/dist/cli/safe-root.d.ts +17 -0
  46. package/dist/cli/safe-root.js +32 -0
  47. package/dist/constants.d.ts +1 -1
  48. package/dist/constants.js +1 -1
  49. package/dist/core/brain-cli/service.js +5 -2
  50. package/dist/core/monorepo/detect.js +10 -0
  51. package/dist/core/secrets/scan.js +9 -0
  52. package/dist/core/understanding/brain/storage/store.d.ts +4 -0
  53. package/dist/core/understanding/brain/storage/store.js +22 -2
  54. package/dist/dashboard/page.d.ts +2 -0
  55. package/dist/dashboard/page.js +53 -0
  56. package/dist/dashboard/server.js +117 -140
  57. package/dist/dashboard/ui/client.d.ts +2 -0
  58. package/dist/dashboard/ui/client.js +2 -0
  59. package/dist/dashboard/ui/styles.d.ts +2 -0
  60. package/dist/dashboard/ui/styles.js +138 -0
  61. package/dist/discovery/files.js +13 -0
  62. package/dist/index.d.ts +4 -0
  63. package/dist/index.js +2 -0
  64. package/dist/intelligence/graph/build.js +46 -38
  65. package/dist/intelligence/graph/incremental.d.ts +2 -0
  66. package/dist/intelligence/graph/incremental.js +9 -0
  67. package/dist/intelligence/resolve/imports.js +1 -1
  68. package/dist/languages/dart.d.ts +10 -0
  69. package/dist/languages/dart.js +99 -0
  70. package/dist/languages/go.d.ts +13 -7
  71. package/dist/languages/go.js +84 -24
  72. package/dist/languages/index.d.ts +5 -1
  73. package/dist/languages/index.js +13 -36
  74. package/dist/languages/java.d.ts +10 -0
  75. package/dist/languages/java.js +80 -0
  76. package/dist/languages/kotlin.d.ts +10 -0
  77. package/dist/languages/kotlin.js +85 -0
  78. package/dist/languages/rust.d.ts +10 -0
  79. package/dist/languages/rust.js +93 -0
  80. package/dist/languages/types.d.ts +4 -3
  81. package/dist/mcp/agent/registry.d.ts +1 -1
  82. package/dist/mcp/agent/registry.js +109 -23
  83. package/dist/mcp/intelligence/handlers.d.ts +3 -0
  84. package/dist/mcp/intelligence/handlers.js +28 -0
  85. package/dist/mcp/intelligence/path-safety.d.ts +6 -1
  86. package/dist/mcp/intelligence/path-safety.js +55 -2
  87. package/dist/mcp/intelligence/registry.d.ts +1 -1
  88. package/dist/mcp/intelligence/registry.js +30 -1
  89. package/dist/platform/graph/build.js +9 -0
  90. package/dist/product/api/doctor.d.ts +14 -0
  91. package/dist/product/api/doctor.js +185 -0
  92. package/dist/product/api/openapi.d.ts +7 -0
  93. package/dist/product/api/openapi.js +122 -0
  94. package/dist/product/approval/model.d.ts +30 -0
  95. package/dist/product/approval/model.js +64 -0
  96. package/dist/product/approval/session.d.ts +49 -0
  97. package/dist/product/approval/session.js +134 -0
  98. package/dist/product/database/doctor.d.ts +30 -0
  99. package/dist/product/database/doctor.js +184 -0
  100. package/dist/product/decisions/ledger.d.ts +36 -0
  101. package/dist/product/decisions/ledger.js +141 -0
  102. package/dist/product/deps/analyze.d.ts +46 -0
  103. package/dist/product/deps/analyze.js +137 -0
  104. package/dist/product/deps/lockfiles.d.ts +25 -0
  105. package/dist/product/deps/lockfiles.js +200 -0
  106. package/dist/product/discovery/roots.d.ts +52 -0
  107. package/dist/product/discovery/roots.js +231 -0
  108. package/dist/product/dna/build.d.ts +50 -0
  109. package/dist/product/dna/build.js +255 -0
  110. package/dist/product/eval/lab.d.ts +17 -0
  111. package/dist/product/eval/lab.js +218 -0
  112. package/dist/product/events/doctor.d.ts +21 -0
  113. package/dist/product/events/doctor.js +147 -0
  114. package/dist/product/evidence-scan.d.ts +12 -0
  115. package/dist/product/evidence-scan.js +46 -0
  116. package/dist/product/evolution/timeline.d.ts +29 -0
  117. package/dist/product/evolution/timeline.js +123 -0
  118. package/dist/product/features/intelligence.d.ts +23 -0
  119. package/dist/product/features/intelligence.js +158 -0
  120. package/dist/product/forensic/mode.d.ts +25 -0
  121. package/dist/product/forensic/mode.js +70 -0
  122. package/dist/product/graph/enrich-languages.d.ts +20 -0
  123. package/dist/product/graph/enrich-languages.js +168 -0
  124. package/dist/product/health/code-health.d.ts +20 -0
  125. package/dist/product/health/code-health.js +149 -0
  126. package/dist/product/index.d.ts +72 -0
  127. package/dist/product/index.js +36 -0
  128. package/dist/product/ledger/change-ledger.d.ts +23 -0
  129. package/dist/product/ledger/change-ledger.js +64 -0
  130. package/dist/product/map/software-map.d.ts +17 -0
  131. package/dist/product/map/software-map.js +96 -0
  132. package/dist/product/memory/institutional.d.ts +16 -0
  133. package/dist/product/memory/institutional.js +92 -0
  134. package/dist/product/ops/incident.d.ts +20 -0
  135. package/dist/product/ops/incident.js +61 -0
  136. package/dist/product/ops/infra.d.ts +15 -0
  137. package/dist/product/ops/infra.js +112 -0
  138. package/dist/product/org/model.d.ts +34 -0
  139. package/dist/product/org/model.js +195 -0
  140. package/dist/product/privacy/doctor.d.ts +13 -0
  141. package/dist/product/privacy/doctor.js +90 -0
  142. package/dist/product/requirements/trace.d.ts +21 -0
  143. package/dist/product/requirements/trace.js +166 -0
  144. package/dist/product/search/index.d.ts +29 -0
  145. package/dist/product/search/index.js +116 -0
  146. package/dist/product/search/software-search.d.ts +19 -0
  147. package/dist/product/search/software-search.js +105 -0
  148. package/dist/product/security/doctor.d.ts +23 -0
  149. package/dist/product/security/doctor.js +124 -0
  150. package/dist/product/self/diagnose.d.ts +15 -0
  151. package/dist/product/self/diagnose.js +82 -0
  152. package/dist/product/techdebt/roadmap.d.ts +20 -0
  153. package/dist/product/techdebt/roadmap.js +118 -0
  154. package/dist/product/testbrain/analyze.d.ts +29 -0
  155. package/dist/product/testbrain/analyze.js +129 -0
  156. package/dist/product/truth.d.ts +12 -0
  157. package/dist/product/truth.js +30 -0
  158. package/dist/product/twin/digital-twin.d.ts +23 -0
  159. package/dist/product/twin/digital-twin.js +52 -0
  160. package/dist/product/twin/store.d.ts +21 -0
  161. package/dist/product/twin/store.js +78 -0
  162. package/dist/product/whatif/engine.d.ts +28 -0
  163. package/dist/product/whatif/engine.js +91 -0
  164. package/dist/project/ownership.d.ts +65 -0
  165. package/dist/project/ownership.js +169 -0
  166. package/dist/utils/fs.js +10 -2
  167. package/package.json +1 -1
@@ -0,0 +1,86 @@
1
+ import { EXIT_CODES } from "../../types/index.js";
2
+ import { initBrainStore, rebuildBrain, getBrainStatus } from "../../core/brain-cli/service.js";
3
+ import { discoverProjectRoots } from "../../product/discovery/roots.js";
4
+ import { buildProjectDna, persistProjectDna } from "../../product/dna/build.js";
5
+ import { resolveRepoRoot } from "../../utils/path.js";
6
+ import { resolveCliProjectRoot } from "../safe-root.js";
7
+ function printJson(value) {
8
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
9
+ }
10
+ export async function runStartCommand(options) {
11
+ const prefer = options.select ?? options.root;
12
+ const discovery = await discoverProjectRoots({
13
+ cwd: prefer ? resolveRepoRoot(prefer) : process.cwd(),
14
+ ...(prefer ? { prefer: resolveRepoRoot(prefer) } : {}),
15
+ ...(options.maxEntries !== undefined ? { maxEntries: options.maxEntries } : {}),
16
+ autoSelect: !options.listOnly,
17
+ });
18
+ if (options.listOnly || discovery.blocked || !discovery.selected) {
19
+ if (options.json) {
20
+ printJson({ discovery, dna: null, brain: null });
21
+ return discovery.blocked ? EXIT_CODES.USAGE_ERROR : EXIT_CODES.SUCCESS;
22
+ }
23
+ if (discovery.blocked) {
24
+ process.stderr.write(`Blocked: ${discovery.blockReason}\n`);
25
+ return EXIT_CODES.USAGE_ERROR;
26
+ }
27
+ process.stdout.write("Project candidates:\n");
28
+ if (discovery.candidates.length === 0) {
29
+ process.stdout.write(" (none found — navigate into a project or pass a path)\n");
30
+ return EXIT_CODES.USAGE_ERROR;
31
+ }
32
+ for (const [i, c] of discovery.candidates.entries()) {
33
+ process.stdout.write(` [${i + 1}] ${c.root}\n score=${c.score} markers=${c.markers.join(", ")} (${c.reason})\n`);
34
+ }
35
+ process.stdout.write("\nRe-run with --select <path> to initialize that project.\n");
36
+ return EXIT_CODES.SUCCESS;
37
+ }
38
+ const root = discovery.selected.root;
39
+ const dna = await persistProjectDna(root);
40
+ let brainStatus = null;
41
+ if (options.initBrain !== false) {
42
+ brainStatus = await initBrainStore(root);
43
+ if (options.rebuildBrain) {
44
+ await rebuildBrain(root);
45
+ brainStatus = await getBrainStatus(root);
46
+ }
47
+ }
48
+ const payload = { discovery, dna, brain: brainStatus };
49
+ if (options.json) {
50
+ printJson(payload);
51
+ }
52
+ else {
53
+ process.stdout.write(`AgentDoctor start\n`);
54
+ process.stdout.write(` root: ${root}\n`);
55
+ process.stdout.write(` fingerprint: ${dna.fingerprint}\n`);
56
+ process.stdout.write(` type: ${dna.projectType}\n`);
57
+ process.stdout.write(` languages: ${dna.languages.join(", ") || "(none)"}\n`);
58
+ process.stdout.write(` frameworks: ${dna.frameworks.join(", ") || "(none)"}\n`);
59
+ process.stdout.write(` files: ${dna.counts.files} (source ${dna.counts.sourceFiles}, tests ${dna.counts.testFiles})\n`);
60
+ if (brainStatus) {
61
+ process.stdout.write(` brain: ${brainStatus.hasSnapshot ? `snapshot ${brainStatus.latestSnapshotId}` : "initialized (no snapshot yet — run with --rebuild-brain or brain rebuild)"}\n`);
62
+ }
63
+ process.stdout.write(` DNA written to .agentdoctor/dna/project-dna.json\n`);
64
+ }
65
+ return EXIT_CODES.SUCCESS;
66
+ }
67
+ export async function runDnaCommand(options) {
68
+ const gated = await resolveCliProjectRoot(options.root);
69
+ if (!gated.ok) {
70
+ console.error(`Error: ${gated.message}`);
71
+ return gated.code;
72
+ }
73
+ const root = gated.root;
74
+ const dna = options.persist ? await persistProjectDna(root) : await buildProjectDna(root);
75
+ if (options.json) {
76
+ printJson(dna);
77
+ }
78
+ else {
79
+ process.stdout.write(`Project DNA: ${dna.name} (${dna.fingerprint})\n`);
80
+ process.stdout.write(` type: ${dna.projectType}\n`);
81
+ process.stdout.write(` languages: ${dna.languages.join(", ") || "(none)"}\n`);
82
+ process.stdout.write(` frameworks: ${dna.frameworks.join(", ") || "(none)"}\n`);
83
+ process.stdout.write(` monorepo: ${dna.monorepo.isMonorepo ? dna.monorepo.tool : "no"}\n`);
84
+ }
85
+ return EXIT_CODES.SUCCESS;
86
+ }
@@ -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,
@@ -20,6 +20,8 @@ import { runWorkspaceCommand } from "./commands/workspace.js";
20
20
  import { runAskCommand, runChatCommand } from "./commands/chat.js";
21
21
  import { runAgentCommand, runPlanCommand } from "./commands/agent.js";
22
22
  import { runLearnCommand } from "./commands/learn.js";
23
+ import { runProductCommand } from "./commands/product.js";
24
+ import { runStartCommand, runDnaCommand } from "./commands/start.js";
23
25
  import { collectOpsHealth } from "../ops/health.js";
24
26
  import { listSessions, loadSession } from "../platform/sessions/store.js";
25
27
  import { exportReports } from "../platform/reports/export.js";
@@ -1282,6 +1284,7 @@ export function createProgram() {
1282
1284
  .option("--list-tools", "List available agent tools", false)
1283
1285
  .option("--tool <name>", "Run a single tool by name")
1284
1286
  .option("--goal <text>", "Build a change plan for this goal")
1287
+ .option("--role <role>", "Specialized role for plan/apply (planner, coder, tester, reviewer, security, …)")
1285
1288
  .option("--approve", "Record explicit human approval", false)
1286
1289
  .option("--apply", "Apply approved changes (requires --approve)", false)
1287
1290
  .option("--apply-ops <json>", "JSON array of {name, arguments} tool ops for --apply")
@@ -1294,6 +1297,7 @@ export function createProgram() {
1294
1297
  listTools: Boolean(options.listTools),
1295
1298
  ...(options.tool !== undefined ? { tool: options.tool } : {}),
1296
1299
  ...(options.goal !== undefined ? { goal: options.goal } : {}),
1300
+ ...(options.role !== undefined ? { role: options.role } : {}),
1297
1301
  approve: Boolean(options.approve),
1298
1302
  apply: Boolean(options.apply),
1299
1303
  ...(options.applyOps !== undefined ? { applyOpsJson: options.applyOps } : {}),
@@ -1543,6 +1547,143 @@ export function createProgram() {
1543
1547
  const code = await runExplainCommand(rule);
1544
1548
  process.exitCode = code;
1545
1549
  });
1550
+ function bindProductCommand(name, description, action) {
1551
+ program
1552
+ .command(name)
1553
+ .description(description)
1554
+ .argument("[path]", "Repository path")
1555
+ .option("--json", "Emit JSON report", false)
1556
+ .action(async (pathArg, _options, command) => {
1557
+ const options = command.optsWithGlobals();
1558
+ process.exitCode = await runProductCommand({
1559
+ action,
1560
+ root: resolveTargetArgument(pathArg),
1561
+ json: Boolean(options.json),
1562
+ });
1563
+ });
1564
+ }
1565
+ program
1566
+ .command("start")
1567
+ .description("Safe project discovery: detect project root, build Project DNA, initialize Project Brain")
1568
+ .argument("[path]", "Optional project path (default: cwd)")
1569
+ .option("--list", "List candidates without initializing", false)
1570
+ .option("--select <path>", "Explicitly select a project root")
1571
+ .option("--max-entries <n>", "Refuse scans larger than this entry budget", "25000")
1572
+ .option("--no-init-brain", "Skip Project Brain store initialization")
1573
+ .option("--rebuild-brain", "Compile Project Brain after init", false)
1574
+ .option("--json", "Emit JSON", false)
1575
+ .action(async (pathArg, options) => {
1576
+ process.exitCode = await runStartCommand({
1577
+ ...(pathArg !== undefined ? { root: resolveTargetArgument(pathArg) } : {}),
1578
+ listOnly: Boolean(options.list),
1579
+ ...(options.select !== undefined ? { select: options.select } : {}),
1580
+ maxEntries: Number(options.maxEntries ?? 25000),
1581
+ initBrain: options.initBrain !== false,
1582
+ rebuildBrain: Boolean(options.rebuildBrain),
1583
+ json: Boolean(options.json),
1584
+ });
1585
+ });
1586
+ program
1587
+ .command("dna")
1588
+ .description("Build deterministic Project DNA fingerprint")
1589
+ .argument("[path]", "Repository path")
1590
+ .option("--persist", "Write .agentdoctor/dna/project-dna.json", false)
1591
+ .option("--json", "Emit JSON", false)
1592
+ .action(async (pathArg, options) => {
1593
+ process.exitCode = await runDnaCommand({
1594
+ root: resolveTargetArgument(pathArg),
1595
+ persist: Boolean(options.persist),
1596
+ json: Boolean(options.json),
1597
+ });
1598
+ });
1599
+ bindProductCommand("requirements", "Trace requirements from markdown docs (evidence-backed)", "requirements");
1600
+ bindProductCommand("api", "Discover HTTP route patterns (regex evidence)", "api");
1601
+ bindProductCommand("database", "Discover schema objects from migrations/SQL/Prisma", "database");
1602
+ bindProductCommand("events", "Detect queues, workers, cron, and event markers", "events");
1603
+ bindProductCommand("dependency", "Analyze direct dependencies and workspace duplicates", "dependency");
1604
+ bindProductCommand("deps", "Alias for dependency", "dependency");
1605
+ bindProductCommand("health-code", "Aggregate code health indicators with evidence", "health-code");
1606
+ bindProductCommand("code-health", "Alias for health-code", "health-code");
1607
+ bindProductCommand("map", "Build navigable software map from project layout", "map");
1608
+ bindProductCommand("decisions", "Load ADR files and decision ledger", "decisions");
1609
+ bindProductCommand("forensic", "Read-only forensic analysis (git + ledgers + brain)", "forensic");
1610
+ bindProductCommand("twin", "Software digital twin snapshot", "twin");
1611
+ bindProductCommand("eval-lab", "Run evaluation lab fixture checks", "eval-lab");
1612
+ bindProductCommand("self-check", "Self-diagnosis of AgentDoctor installation", "self-check");
1613
+ bindProductCommand("infra", "Detect Docker/K8s/Terraform/CI artifacts", "infra");
1614
+ bindProductCommand("incident", "Build incident timeline hypotheses", "incident");
1615
+ bindProductCommand("security-doctor", "Security heuristics: secrets, auth markers, dangerous patterns", "security-doctor");
1616
+ bindProductCommand("test-brain", "Test Brain: test-impact plus graph file↔test mapping", "test-brain");
1617
+ bindProductCommand("privacy-doctor", "PII-ish pattern scan (not legal compliance)", "privacy-doctor");
1618
+ bindProductCommand("tech-debt", "Technical debt roadmap (TODO/FIXME, architecture, tests)", "tech-debt");
1619
+ bindProductCommand("features", "Feature intelligence (flows + API links)", "features");
1620
+ bindProductCommand("evolution", "Software evolution timeline from git + ledger", "evolution");
1621
+ bindProductCommand("org", "Load organization model (.agentdoctor/org)", "org");
1622
+ program
1623
+ .command("memory")
1624
+ .description("Query institutional memory (brain + ledgers)")
1625
+ .argument("<query>", "Memory query")
1626
+ .argument("[path]", "Repository path")
1627
+ .option("--json", "Emit JSON report", false)
1628
+ .action(async (query, pathArg, _options, command) => {
1629
+ const options = command.optsWithGlobals();
1630
+ process.exitCode = await runProductCommand({
1631
+ action: "memory",
1632
+ query,
1633
+ root: resolveTargetArgument(pathArg),
1634
+ json: Boolean(options.json),
1635
+ });
1636
+ });
1637
+ program
1638
+ .command("search")
1639
+ .description("Search symbols and concepts (brain + bounded file scan)")
1640
+ .argument("<query>", "Search query")
1641
+ .argument("[path]", "Repository path")
1642
+ .option("--json", "Emit JSON report", false)
1643
+ .action(async (query, pathArg, _options, command) => {
1644
+ const options = command.optsWithGlobals();
1645
+ process.exitCode = await runProductCommand({
1646
+ action: "search",
1647
+ query,
1648
+ root: resolveTargetArgument(pathArg),
1649
+ json: Boolean(options.json),
1650
+ });
1651
+ });
1652
+ program
1653
+ .command("role-agent")
1654
+ .description("Run the coding loop with a specialized agent role")
1655
+ .argument("[path]", "Repository path")
1656
+ .requiredOption("--role <role>", "Agent role (planner, coder, tester, reviewer, security, …)")
1657
+ .requiredOption("--goal <text>", "Goal for the role agent")
1658
+ .option("--approve", "Record human approval for writes", false)
1659
+ .option("--apply", "Apply changes (requires --approve)", false)
1660
+ .option("--json", "Emit JSON", false)
1661
+ .action(async (pathArg, options) => {
1662
+ process.exitCode = await runProductCommand({
1663
+ action: "role-agent",
1664
+ root: resolveTargetArgument(pathArg),
1665
+ role: options.role,
1666
+ goal: options.goal,
1667
+ approve: Boolean(options.approve),
1668
+ apply: Boolean(options.apply),
1669
+ json: Boolean(options.json),
1670
+ });
1671
+ });
1672
+ program
1673
+ .command("what-if")
1674
+ .description("Graph-backed change impact for a file or symbol")
1675
+ .argument("<target>", "File path or symbol")
1676
+ .argument("[path]", "Repository path")
1677
+ .option("--json", "Emit JSON report", false)
1678
+ .action(async (target, pathArg, _options, command) => {
1679
+ const options = command.optsWithGlobals();
1680
+ process.exitCode = await runProductCommand({
1681
+ action: "what-if",
1682
+ root: resolveTargetArgument(pathArg),
1683
+ target,
1684
+ json: Boolean(options.json),
1685
+ });
1686
+ });
1546
1687
  program
1547
1688
  .command("doctor")
1548
1689
  .description("Check AgentDoctor installation health")
@@ -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 = "2.1.0";
2
+ export declare const PACKAGE_VERSION = "3.0.1";
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 = "2.1.0";
1
+ export const PACKAGE_VERSION = "3.0.1";
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;
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { atomicWriteTextFile } from "../../../../utils/fs.js";
5
+ import { OWNERSHIP_BOUNDARY_VERSION } from "../../../../project/ownership.js";
5
6
  import { parseBrainDelta, serializeBrainDelta } from "../delta.js";
6
7
  import { checkBrainCompatibility, migrateBrain } from "../migrate.js";
7
8
  import { assertBrainContract } from "../contract.js";
@@ -198,6 +199,7 @@ export class LocalBrainStore {
198
199
  projectName: brain.metadata.projectName,
199
200
  brainId: brain.metadata.brainId,
200
201
  checksum: nextChecksum,
202
+ ownershipBoundaryVersion: OWNERSHIP_BOUNDARY_VERSION,
201
203
  };
202
204
  const meta = await this.readMeta();
203
205
  const snapshots = meta.snapshots.filter((s) => s.id !== snapshotId);
@@ -209,10 +211,15 @@ export class LocalBrainStore {
209
211
  projectName: brain.metadata.projectName,
210
212
  latestSnapshotId: snapshotId,
211
213
  snapshots,
214
+ ownershipBoundaryVersion: OWNERSHIP_BOUNDARY_VERSION,
212
215
  });
213
216
  return snapMeta;
214
217
  }
215
218
  async loadSnapshot(snapshotId) {
219
+ const meta = await this.readMeta();
220
+ if (meta.ownershipBoundaryVersion !== OWNERSHIP_BOUNDARY_VERSION) {
221
+ throw new BrainStorageError(`brain store ownership boundary is stale (have ${String(meta.ownershipBoundaryVersion ?? "missing")}, need ${OWNERSHIP_BOUNDARY_VERSION}); rebuild required`);
222
+ }
216
223
  const target = await assertInsideRootResolved(this.root, this.brainPath(snapshotId));
217
224
  let raw;
218
225
  try {
@@ -224,9 +231,17 @@ export class LocalBrainStore {
224
231
  }
225
232
  throw error;
226
233
  }
227
- const meta = await this.readMeta();
228
234
  const snap = meta.snapshots.find((s) => s.id === snapshotId);
229
235
  if (snap) {
236
+ if (snap.ownershipBoundaryVersion !== undefined &&
237
+ snap.ownershipBoundaryVersion !== OWNERSHIP_BOUNDARY_VERSION) {
238
+ throw new BrainStorageError(`snapshot ${snapshotId} ownership boundary is stale (have ${snap.ownershipBoundaryVersion}, need ${OWNERSHIP_BOUNDARY_VERSION}); rebuild required`);
239
+ }
240
+ // Pre-stamp snapshots created before ownershipBoundaryVersion was recorded are
241
+ // unsafe to serve once the store claims a current boundary — require rebuild.
242
+ if (snap.ownershipBoundaryVersion === undefined) {
243
+ throw new BrainStorageError(`snapshot ${snapshotId} missing ownershipBoundaryVersion; rebuild required`);
244
+ }
230
245
  const actual = checksumPayload(raw);
231
246
  if (actual !== snap.checksum) {
232
247
  throw new BrainStorageError(`checksum mismatch for snapshot ${snapshotId}`);
@@ -261,7 +276,12 @@ export class LocalBrainStore {
261
276
  return meta.latestSnapshotId;
262
277
  }
263
278
  async loadLatest() {
264
- const id = await this.latestSnapshotId();
279
+ const meta = await this.readMeta();
280
+ if (meta.ownershipBoundaryVersion !== OWNERSHIP_BOUNDARY_VERSION) {
281
+ // Pre-boundary or outdated snapshots must not be served as current project truth.
282
+ return null;
283
+ }
284
+ const id = meta.latestSnapshotId;
265
285
  if (!id) {
266
286
  return null;
267
287
  }
@@ -0,0 +1,2 @@
1
+ /** Embedded dashboard HTML. Version injected only from PACKAGE_VERSION. */
2
+ export declare function htmlPage(): string;