@praneeth_54/agentdoctor 3.0.2 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.0.3] — 2026-09-27
11
+
12
+ Patch release on top of public **3.0.2**: Brain → Project Chat evidence bridge and
13
+ real operation-based progress UX for `agentdoctor ask`.
14
+
15
+ Public relationship:
16
+
17
+ - **3.0.2** remains the immutable public discovery patch (`v3.0.2` → `8d41936`).
18
+ - **3.0.3** packages the finalize work from `a586cdf` (Brain→Chat + ask progress).
19
+ - Does **not** rewrite **3.0.0**, **3.0.1**, or **3.0.2** history.
20
+
21
+ ### Fixed
22
+
23
+ - Deterministic `agentdoctor ask` / Project Chat now consumes searchable Project Brain claims/components as first-class **INFERRED** evidence (aligned with `brain search`).
24
+ - Brain-derived source paths are ownership-gated before excerpts; Brain claims are never auto-upgraded to **VERIFIED**.
25
+ - `.agentdoctor/**` control-plane paths cannot become **VERIFIED** application source evidence.
26
+ - Architecture/module answers prioritize Brain + owned sources over generic minified graph noise.
27
+ - Ownership, containment, HOME/broad-root, `.private`, nested-repo, symlink, and stale-brain protections remain enforced.
28
+
29
+ ### Added
30
+
31
+ - `agentdoctor ask` progress UX driven by real pipeline stages (understand → search → retrieve → verify → prepare).
32
+ - Progress writes to stderr (TTY spinner / non-TTY stable lines); answers and `--json` stay on stdout without contamination.
33
+ - No artificial delays — spinner frames only animate while real work is in flight.
34
+
35
+ ### Tests
36
+
37
+ - Added `tests/unit/agent/chat-brain-context.test.ts` for Brain → ask evidence, control-plane hostility, stale brain, and UNKNOWN.
38
+ - Added `tests/unit/cli/ask-progress.test.ts` for TTY/non-TTY/JSON/error progress lifecycle.
39
+
10
40
  ## [3.0.2] — 2026-09-27
11
41
 
12
42
  Patch release: fix current-directory project discovery for `agentdoctor start`.
@@ -14,7 +44,7 @@ Patch release: fix current-directory project discovery for `agentdoctor start`.
14
44
  Public relationship:
15
45
 
16
46
  - **3.0.0** and **3.0.1** remain immutable public releases.
17
- - **3.0.2** does not rewrite prior npm/GitHub history.
47
+ - **3.0.2** is the public discovery patch (`v3.0.2` → `8d41936`). Brain→Chat and ask progress ship in **3.0.3**.
18
48
 
19
49
  ### Fixed
20
50
 
@@ -622,7 +652,8 @@ First public beta.
622
652
  - Not a complete secret scanner
623
653
  - Git “tracked secret” detection deferred
624
654
 
625
- [Unreleased]: https://github.com/pranee54/AgentDoctor/compare/v3.0.2...HEAD
655
+ [Unreleased]: https://github.com/pranee54/AgentDoctor/compare/v3.0.3...HEAD
656
+ [3.0.3]: https://github.com/pranee54/AgentDoctor/compare/v3.0.2...v3.0.3
626
657
  [3.0.2]: https://github.com/pranee54/AgentDoctor/compare/v3.0.1...v3.0.2
627
658
  [3.0.1]: https://github.com/pranee54/AgentDoctor/compare/v3.0.0...v3.0.1
628
659
  [3.0.0]: https://github.com/pranee54/AgentDoctor/compare/v2.1.0...v3.0.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.2
285
- agentdoctor --version # 3.0.2
284
+ npm install -g @praneeth_54/agentdoctor@3.0.3
285
+ agentdoctor --version # 3.0.3
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.2 --help
292
+ npx @praneeth_54/agentdoctor@3.0.3 --help
293
293
  ```
294
294
 
295
295
  ---
@@ -298,7 +298,7 @@ npx @praneeth_54/agentdoctor@3.0.2 --help
298
298
 
299
299
  ```bash
300
300
  # 1. Install
301
- npm install -g @praneeth_54/agentdoctor@3.0.2
301
+ npm install -g @praneeth_54/agentdoctor@3.0.3
302
302
 
303
303
  # 2. Enter YOUR project (not your home folder)
304
304
  cd /path/to/my-project
@@ -586,6 +586,7 @@ Post-acceptance ideas for later releases: maintainers see [docs/internal/POST_3_
586
586
  | **3.0.0 (released)** | Original public release (`v3.0.0` → `c21faf1`) |
587
587
  | **3.0.1 (released)** | Freeze remediation / ownership & security hardening follow-up |
588
588
  | **3.0.2 (released)** | `start` discovery aligned with deterministic `detectProject` |
589
+ | **3.0.3 (released)** | Brain → Project Chat evidence + `ask` progress UX |
589
590
  | **Coming soon** | Maturity, language depth, runtime adapters, DX — not promised ship dates |
590
591
 
591
592
  ---
@@ -1,6 +1,8 @@
1
1
  import type { ChatTurnResponse } from "./types.js";
2
+ import { type AskProgressReporter } from "../progress.js";
2
3
  export declare function answerDeterministicProjectQuestion(options: {
3
4
  root: string;
4
5
  question: string;
5
6
  sessionId: string;
7
+ progress?: AskProgressReporter;
6
8
  }): Promise<ChatTurnResponse>;
@@ -4,11 +4,14 @@ import { analyzeDependencies } from "../../product/deps/analyze.js";
4
4
  import { buildIntelligenceGraph } from "../../intelligence/graph/build.js";
5
5
  import { resolveRepoRoot } from "../../utils/path.js";
6
6
  import { retrieveProjectContext } from "../context/retrieve.js";
7
+ import { extractQueryTerms } from "../context/brain-evidence.js";
7
8
  import { buildChatTurnResponse } from "./response.js";
9
+ import { noopAskProgress } from "../progress.js";
8
10
  const ARCH_RE = /architecture|structure|module|layout|what (is|does) (this|my) project|project overview|understand (my |this )?project/i;
9
11
  const AUTH_RE = /auth|login|password|session|jwt|oauth/i;
10
12
  const DEPS_RE = /depend|package|lockfile|npm|yarn|pnpm/i;
11
13
  const DB_RE = /database|db\.|sql|prisma|query\(/i;
14
+ const MODULE_RE = /\bmodule\b|\bcomponent\b|\bprovides?\b|\bbelong/i;
12
15
  /** ReDoS-safe: avoid `where.*…` / `how.*…` backtracking on uncontrolled chat input. */
13
16
  function isStartQuestion(q) {
14
17
  if (/application start|entry\s*point/i.test(q))
@@ -28,14 +31,42 @@ function isArchitectureQuestion(q) {
28
31
  return true;
29
32
  return false;
30
33
  }
34
+ function isModuleQuestion(q) {
35
+ return MODULE_RE.test(q) || extractQueryTerms(q).length > 0;
36
+ }
31
37
  export async function answerDeterministicProjectQuestion(options) {
32
38
  const root = resolveRepoRoot(options.root);
33
39
  const q = options.question.trim();
40
+ const progress = options.progress ?? noopAskProgress;
41
+ progress.stage("understand", "start");
42
+ const terms = extractQueryTerms(q);
43
+ progress.stage("understand", "ok", terms.length ? `focus: ${terms.slice(0, 4).join(", ")}` : undefined);
34
44
  const context = await retrieveProjectContext({
35
45
  root,
36
46
  query: q,
37
47
  budgetTokens: 4_000,
48
+ maxSourceFiles: 8,
49
+ progress,
38
50
  });
51
+ progress.stage("prepare", "start");
52
+ try {
53
+ const response = await buildDeterministicAnswer({
54
+ root,
55
+ q,
56
+ terms,
57
+ context,
58
+ sessionId: options.sessionId,
59
+ });
60
+ progress.stage("prepare", "ok");
61
+ return response;
62
+ }
63
+ catch (error) {
64
+ progress.stage("prepare", "fail", error instanceof Error ? error.message : String(error));
65
+ throw error;
66
+ }
67
+ }
68
+ async function buildDeterministicAnswer(options) {
69
+ const { root, q, terms, context, sessionId } = options;
39
70
  const sections = [
40
71
  "Deterministic project answer (no LLM). Evidence from DNA, graph, brain, and dependency analyzers.",
41
72
  "",
@@ -46,6 +77,27 @@ export async function answerDeterministicProjectQuestion(options) {
46
77
  sections.push(`Frameworks: ${dna.frameworks.join(", ") || "UNKNOWN"}`);
47
78
  sections.push(`Monorepo: ${dna.monorepo.isMonorepo ? dna.monorepo.tool : "no"}`);
48
79
  sections.push("");
80
+ const brainCitations = context.citations.filter((c) => c.source === "brain" || c.evidenceType === "project-brain");
81
+ const sourceCitations = context.citations.filter((c) => c.confidence === "VERIFIED" && c.evidenceType === "source-code" && c.path);
82
+ if (brainCitations.length > 0 && (isModuleQuestion(q) || isArchitectureQuestion(q))) {
83
+ const focus = terms.length ? terms.join(", ") : "project";
84
+ sections.push(`[INFERRED] Project Brain evidence for: ${focus}`);
85
+ for (const c of brainCitations.slice(0, 14)) {
86
+ sections.push(` - ${c.excerpt ?? c.note ?? "brain claim"}`);
87
+ }
88
+ sections.push("");
89
+ if (sourceCitations.length) {
90
+ sections.push("[VERIFIED] Owned source paths linked from Brain / retrieval:");
91
+ for (const c of sourceCitations.slice(0, 10)) {
92
+ sections.push(` - ${c.path}`);
93
+ }
94
+ sections.push("");
95
+ }
96
+ else {
97
+ sections.push("[INFERRED] No owned source excerpts resolved for these Brain claims; structural evidence only.");
98
+ sections.push("");
99
+ }
100
+ }
49
101
  if (AUTH_RE.test(q)) {
50
102
  const authFiles = context.citations
51
103
  .filter((c) => c.path && /auth|login|session|password/i.test(c.path))
@@ -157,11 +209,28 @@ export async function answerDeterministicProjectQuestion(options) {
157
209
  sections.push(`Lockfiles: ${deps.lockfilesPresent.join(", ") || "none"}`);
158
210
  sections.push("");
159
211
  }
160
- if (isArchitectureQuestion(q)) {
212
+ if (isArchitectureQuestion(q) && brainCitations.length === 0) {
161
213
  try {
162
214
  const graph = await buildIntelligenceGraph({ root, mode: "auto" });
163
215
  sections.push(`[INFERRED] Graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges (${graph.builder})`);
164
- const sample = graph.nodes.filter((n) => n.path).slice(0, 6);
216
+ const termLower = terms.map((t) => t.toLowerCase());
217
+ const filtered = graph.nodes
218
+ .filter((n) => n.path && n.label)
219
+ .filter((n) => {
220
+ if (/\.min\.js$/i.test(n.path) || /chart\.umd|node_modules/i.test(n.path))
221
+ return false;
222
+ if (termLower.length === 0)
223
+ return n.kind === "file" || n.kind === "module";
224
+ const blob = `${n.label} ${n.path}`.toLowerCase();
225
+ return termLower.some((t) => blob.includes(t));
226
+ })
227
+ .slice(0, 8);
228
+ const sample = filtered.length
229
+ ? filtered
230
+ : graph.nodes
231
+ .filter((n) => n.path && !/\.min\.js$/i.test(n.path))
232
+ .filter((n) => n.kind === "file")
233
+ .slice(0, 6);
165
234
  for (const n of sample) {
166
235
  sections.push(` - ${n.kind}: ${n.label}${n.path ? ` @ ${n.path}` : ""}`);
167
236
  }
@@ -171,6 +240,17 @@ export async function answerDeterministicProjectQuestion(options) {
171
240
  }
172
241
  sections.push("");
173
242
  }
243
+ else if (isArchitectureQuestion(q)) {
244
+ // Brain already rendered; add a short graph filter if present in context.
245
+ const graphCite = context.citations.find((c) => c.source === "graph");
246
+ if (graphCite?.excerpt) {
247
+ sections.push("[INFERRED] Query-filtered graph nodes:");
248
+ for (const line of graphCite.excerpt.split("\n").slice(0, 6)) {
249
+ sections.push(` - ${line}`);
250
+ }
251
+ sections.push("");
252
+ }
253
+ }
174
254
  const brain = await getBrainStatus(root);
175
255
  if (brain.hasSnapshot) {
176
256
  sections.push(`[INFERRED] Project brain snapshot present (${brain.snapshotCount} stored; latest ${brain.latestSnapshotId ?? "unknown"}).`);
@@ -178,6 +258,14 @@ export async function answerDeterministicProjectQuestion(options) {
178
258
  else {
179
259
  sections.push("[UNKNOWN] No project brain snapshot — run brain compile for richer answers.");
180
260
  }
261
+ if (brainCitations.length === 0 &&
262
+ sourceCitations.length === 0 &&
263
+ !AUTH_RE.test(q) &&
264
+ !DEPS_RE.test(q) &&
265
+ !isArchitectureQuestion(q)) {
266
+ sections.push("");
267
+ sections.push("[UNKNOWN] Insufficient Brain/source evidence for this question — try a more specific module or path name.");
268
+ }
181
269
  sections.push("");
182
270
  sections.push("Limitations: deterministic answers cannot invent behavior not present in evidence.");
183
271
  const bundle = {
@@ -188,7 +276,7 @@ export async function answerDeterministicProjectQuestion(options) {
188
276
  ],
189
277
  };
190
278
  return buildChatTurnResponse({
191
- sessionId: options.sessionId,
279
+ sessionId,
192
280
  modelText: sections.join("\n"),
193
281
  context: bundle,
194
282
  provider: "deterministic",
@@ -4,8 +4,12 @@ import { truthLabelHelp } from "../context/truth.js";
4
4
  * Never invents line ranges. Citations come only from the context bundle.
5
5
  */
6
6
  export function buildChatTurnResponse(options) {
7
- const citations = options.context.citations.filter((c) => c.path && c.confidence === "VERIFIED");
8
- const contextPaths = citations.map((c) => c.path).filter(Boolean);
7
+ const verifiedSource = options.context.citations.filter((c) => c.path && c.confidence === "VERIFIED" && c.evidenceType === "source-code");
8
+ const brainCitations = options.context.citations.filter((c) => c.source === "brain" || c.evidenceType === "project-brain");
9
+ const graphCitations = options.context.citations.filter((c) => c.source === "graph" || c.evidenceType === "graph");
10
+ // Surface verified source + brain/graph evidence (brain is never upgraded to VERIFIED).
11
+ const citations = [...verifiedSource, ...brainCitations, ...graphCitations];
12
+ const contextPaths = verifiedSource.map((c) => c.path).filter(Boolean);
9
13
  const mentioned = extractMentionedPaths(options.modelText, contextPaths);
10
14
  const truthClaims = [];
11
15
  for (const path of mentioned) {
@@ -15,13 +19,21 @@ export function buildChatTurnResponse(options) {
15
19
  citationPaths: [path],
16
20
  });
17
21
  }
18
- if (citations.length === 0) {
22
+ const hasUsableEvidence = verifiedSource.length > 0 || brainCitations.length > 0;
23
+ if (!hasUsableEvidence) {
19
24
  truthClaims.push({
20
25
  text: "Insufficient repository evidence was retrieved for this question.",
21
26
  label: "UNKNOWN",
22
27
  citationPaths: [],
23
28
  });
24
29
  }
30
+ else if (brainCitations.length > 0 && verifiedSource.length === 0) {
31
+ truthClaims.push({
32
+ text: "Answer grounded in Project Brain structural evidence (INFERRED; no owned source excerpts).",
33
+ label: "INFERRED",
34
+ citationPaths: [],
35
+ });
36
+ }
25
37
  else if (options.modelText.trim()) {
26
38
  truthClaims.push({
27
39
  text: "Interpretive explanation based on retrieved project evidence.",
@@ -83,7 +95,16 @@ export function formatChatResponseForCli(response) {
83
95
  }
84
96
  if (response.citations.length > 0) {
85
97
  lines.push("Evidence:");
86
- for (const c of response.citations.slice(0, 12)) {
98
+ for (const c of response.citations.slice(0, 16)) {
99
+ if (c.source === "brain" || c.evidenceType === "project-brain") {
100
+ lines.push(` - ${c.excerpt ?? c.note ?? "brain claim"} (${c.confidence})`);
101
+ continue;
102
+ }
103
+ if (c.source === "graph" || c.evidenceType === "graph") {
104
+ const first = (c.excerpt ?? c.note ?? "graph").split("\n")[0] ?? "graph";
105
+ lines.push(` - ${first} (${c.confidence})`);
106
+ continue;
107
+ }
87
108
  const path = c.path ?? "(unknown)";
88
109
  lines.push(` - ${path}${c.range ? `:${c.range}` : ""} (${c.confidence})`);
89
110
  }
@@ -15,6 +15,7 @@ export declare class ChatService {
15
15
  private readonly budgetTokens;
16
16
  private readonly persistAudit;
17
17
  private readonly systemPrompt;
18
+ private readonly progress;
18
19
  private auditSession;
19
20
  private graphCache;
20
21
  private lastContext;
@@ -9,6 +9,7 @@ import { PROJECT_CHAT_SYSTEM_PROMPT, wrapProjectData } from "./prompts.js";
9
9
  import { formatProjectSummary, summarizeProjectForChat, } from "./project-summary.js";
10
10
  import { buildChatTurnResponse, formatChatResponseForCli } from "./response.js";
11
11
  import { answerDeterministicProjectQuestion } from "./deterministic.js";
12
+ import { noopAskProgress } from "../progress.js";
12
13
  export const CHAT_PROVIDER_NONE_MESSAGE = `AI chat is not configured.
13
14
 
14
15
  Configure an AI provider to use AgentDoctor Project Chat.
@@ -28,6 +29,7 @@ export class ChatService {
28
29
  budgetTokens;
29
30
  persistAudit;
30
31
  systemPrompt;
32
+ progress;
31
33
  auditSession;
32
34
  graphCache;
33
35
  lastContext;
@@ -37,6 +39,7 @@ export class ChatService {
37
39
  this.provider = options.provider ?? createModelProvider(loadAiConfig());
38
40
  this.budgetTokens = options.budgetTokens ?? 6_000;
39
41
  this.persistAudit = options.persistAudit !== false;
42
+ this.progress = options.progress ?? noopAskProgress;
40
43
  this.systemPrompt = options.systemPromptAddon
41
44
  ? `${PROJECT_CHAT_SYSTEM_PROMPT}\n\n${options.systemPromptAddon}`
42
45
  : PROJECT_CHAT_SYSTEM_PROMPT;
@@ -104,27 +107,40 @@ export class ChatService {
104
107
  root: this.root,
105
108
  question: safeUser,
106
109
  sessionId: this.sessionId,
110
+ progress: this.progress,
107
111
  });
112
+ this.lastContext = undefined;
108
113
  this.memory.addAssistant(response.message, response.contextPaths);
109
114
  await this.audit("prompt", "CHAT_DETERMINISTIC", { status: response.status });
110
115
  return response;
111
116
  }
117
+ this.progress.stage("understand", "start");
112
118
  const query = this.memory.resolveQuery(safeUser);
113
- const context = await retrieveProjectContext({
114
- root: this.root,
115
- query,
116
- budgetTokens: this.budgetTokens,
117
- includePaths: [
118
- ...this.memory.getReferencedPaths().slice(-8),
119
- ...this.memory.getLastContextPaths().slice(-8),
120
- ],
121
- ...(this.graphCache ? { graph: this.graphCache } : {}),
122
- });
119
+ this.progress.stage("understand", "ok");
120
+ let context;
121
+ try {
122
+ context = await retrieveProjectContext({
123
+ root: this.root,
124
+ query,
125
+ budgetTokens: this.budgetTokens,
126
+ includePaths: [
127
+ ...this.memory.getReferencedPaths().slice(-8),
128
+ ...this.memory.getLastContextPaths().slice(-8),
129
+ ],
130
+ ...(this.graphCache ? { graph: this.graphCache } : {}),
131
+ progress: this.progress,
132
+ });
133
+ }
134
+ catch (error) {
135
+ this.progress.stop();
136
+ throw error;
137
+ }
123
138
  this.lastContext = context;
124
139
  await this.audit("file-read", "CHAT_CONTEXT_RETRIEVED", {
125
140
  citations: String(context.citations.length),
126
141
  tokens: String(context.estimatedTokens),
127
142
  });
143
+ this.progress.stage("prepare", "start");
128
144
  const history = this.memory.historyForModel().slice(0, -1); // exclude current user (added separately)
129
145
  const messages = [
130
146
  { role: "system", content: this.systemPrompt },
@@ -142,6 +158,7 @@ export class ChatService {
142
158
  maxTokens: 2048,
143
159
  });
144
160
  if (modelResult.error) {
161
+ this.progress.stage("prepare", "fail", sanitizeProviderError(modelResult.error));
145
162
  const response = buildChatTurnResponse({
146
163
  sessionId: this.sessionId,
147
164
  modelText: `AgentDoctor could not reach the configured AI provider.\n\nProvider: ${modelResult.provider}\nModel: ${modelResult.model}\nError: ${sanitizeProviderError(modelResult.error)}`,
@@ -158,6 +175,7 @@ export class ChatService {
158
175
  });
159
176
  return response;
160
177
  }
178
+ this.progress.stage("prepare", "ok");
161
179
  const text = redactForModel(modelResult.message.content || "");
162
180
  const response = buildChatTurnResponse({
163
181
  sessionId: this.sessionId,
@@ -1,5 +1,6 @@
1
1
  import type { TruthLabel, ContextCitation } from "../context/types.js";
2
2
  import type { AiProviderId, ModelProvider, TokenUsage } from "../../ai/types.js";
3
+ import type { AskProgressReporter } from "../progress.js";
3
4
  export type ChatRole = "system" | "user" | "assistant";
4
5
  export interface ChatMessage {
5
6
  role: ChatRole;
@@ -45,4 +46,6 @@ export interface ChatServiceOptions {
45
46
  persistAudit?: boolean;
46
47
  /** Appended to PROJECT_CHAT_SYSTEM_PROMPT (modes) */
47
48
  systemPromptAddon?: string;
49
+ /** Optional ask progress reporter (CLI TTY UX). */
50
+ progress?: AskProgressReporter;
48
51
  }
@@ -0,0 +1,26 @@
1
+ import { type BrainSearchHit } from "../../core/brain-cli/service.js";
2
+ import type { ProjectBrain } from "../../core/understanding/brain/index.js";
3
+ import type { ContextCitation } from "./types.js";
4
+ /**
5
+ * AgentDoctor control-plane under `.agentdoctor/**` — usable internally,
6
+ * never as ordinary VERIFIED application source evidence.
7
+ */
8
+ export declare function isControlPlaneRelativePath(relativePath: string): boolean;
9
+ /** Significant tokens from a natural-language project question. */
10
+ export declare function extractQueryTerms(query: string): string[];
11
+ export interface BrainEvidencePack {
12
+ brain: ProjectBrain | null;
13
+ hits: BrainSearchHit[];
14
+ citations: ContextCitation[];
15
+ /** Owned-looking source paths derived from claims/components/evidence (not yet ownership-gated). */
16
+ candidateSourcePaths: string[];
17
+ limitations: string[];
18
+ }
19
+ /**
20
+ * Search the latest usable Project Brain for query-relevant claims/components.
21
+ * Brain hits are INFERRED structural evidence — never auto-upgraded to VERIFIED.
22
+ * Stale ownership-boundary brains load as null (same as CLI brain status).
23
+ */
24
+ export declare function collectBrainEvidenceForQuery(root: string, query: string, options?: {
25
+ maxHits?: number;
26
+ }): Promise<BrainEvidencePack>;