@praneeth_54/agentdoctor 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,57 @@ 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
+
40
+ ## [3.0.2] — 2026-09-27
41
+
42
+ Patch release: fix current-directory project discovery for `agentdoctor start`.
43
+
44
+ Public relationship:
45
+
46
+ - **3.0.0** and **3.0.1** remain immutable public releases.
47
+ - **3.0.2** is the public discovery patch (`v3.0.2` → `8d41936`). Brain→Chat and ask progress ship in **3.0.3**.
48
+
49
+ ### Fixed
50
+
51
+ - `agentdoctor start` no longer reports `(none found)` for valid software trees that lack classic manifests (e.g. language-only roots without `package.json` / `.git`).
52
+ - Discovery fallback now reuses the same deterministic `detectProject` engine as `dna`, so `start` and `dna` agree on the project root.
53
+ - Clearer discovery UX (`✓ Project detected`) and empty-state guidance.
54
+ - Reject empty trees that only report `unknown` language; do not silently absorb multi-project parents with 2+ child marker projects.
55
+ - Ownership, containment, HOME/broad-root, `.private`, nested-repo, and symlink protections remain enforced.
56
+
57
+ ### Tests
58
+
59
+ - Added `tests/unit/project/start-discovery.test.ts` regression coverage for markerless cwd, nested roots, multi-project parents, `.private`, and home refusal.
60
+
10
61
  ## [3.0.1] — 2026-09-27
11
62
 
12
63
  Production-hardening follow-up on the reviewed freeze remediation commit
@@ -601,7 +652,9 @@ First public beta.
601
652
  - Not a complete secret scanner
602
653
  - Git “tracked secret” detection deferred
603
654
 
604
- [Unreleased]: https://github.com/pranee54/AgentDoctor/compare/v3.0.1...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
657
+ [3.0.2]: https://github.com/pranee54/AgentDoctor/compare/v3.0.1...v3.0.2
605
658
  [3.0.1]: https://github.com/pranee54/AgentDoctor/compare/v3.0.0...v3.0.1
606
659
  [3.0.0]: https://github.com/pranee54/AgentDoctor/compare/v2.1.0...v3.0.0
607
660
  [2.1.0]: https://github.com/pranee54/AgentDoctor/compare/v2.0.1...v2.1.0
package/README.md CHANGED
@@ -281,15 +281,15 @@ Student path: `learn` → ask → viva → docs → Build With Me (with `--appro
281
281
  Requires **Node.js 20+**.
282
282
 
283
283
  ```bash
284
- npm install -g @praneeth_54/agentdoctor@3.0.1
285
- agentdoctor --version # 3.0.1
284
+ npm install -g @praneeth_54/agentdoctor@3.0.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.1 --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.1 --help
298
298
 
299
299
  ```bash
300
300
  # 1. Install
301
- npm install -g @praneeth_54/agentdoctor@3.0.1
301
+ npm install -g @praneeth_54/agentdoctor@3.0.3
302
302
 
303
303
  # 2. Enter YOUR project (not your home folder)
304
304
  cd /path/to/my-project
@@ -581,11 +581,13 @@ Post-acceptance ideas for later releases: maintainers see [docs/internal/POST_3_
581
581
 
582
582
  ## Roadmap
583
583
 
584
- | Track | Meaning |
585
- | --------------------- | ------------------------------------------------------------------------- |
586
- | **3.0.0 (released)** | Original public release (`v3.0.0` → `c21faf1`) |
587
- | **3.0.1 (candidate)** | Freeze remediation / ownership & security hardening follow-up (`319810a`) |
588
- | **Coming soon** | Maturity, language depth, runtime adapters, DX — not promised ship dates |
584
+ | Track | Meaning |
585
+ | -------------------- | ------------------------------------------------------------------------ |
586
+ | **3.0.0 (released)** | Original public release (`v3.0.0` → `c21faf1`) |
587
+ | **3.0.1 (released)** | Freeze remediation / ownership & security hardening follow-up |
588
+ | **3.0.2 (released)** | `start` discovery aligned with deterministic `detectProject` |
589
+ | **3.0.3 (released)** | Brain → Project Chat evidence + `ask` progress UX |
590
+ | **Coming soon** | Maturity, language depth, runtime adapters, DX — not promised ship dates |
589
591
 
590
592
  ---
591
593
 
@@ -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,20 +4,69 @@ 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";
8
- const ARCH_RE = /architecture|structure|module|layout|how.*(project|repo|codebase)|explain.*(project|repo|codebase|this)|what (is|does) (this|my) project|project overview|understand (my |this )?project/i;
9
+ import { noopAskProgress } from "../progress.js";
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
- const START_RE = /where.*(start|entry|boot|main)|application start|entry\s*point/i;
12
13
  const DB_RE = /database|db\.|sql|prisma|query\(/i;
14
+ const MODULE_RE = /\bmodule\b|\bcomponent\b|\bprovides?\b|\bbelong/i;
15
+ /** ReDoS-safe: avoid `where.*…` / `how.*…` backtracking on uncontrolled chat input. */
16
+ function isStartQuestion(q) {
17
+ if (/application start|entry\s*point/i.test(q))
18
+ return true;
19
+ const lower = q.toLowerCase();
20
+ if (!lower.includes("where"))
21
+ return false;
22
+ return /\b(start|entry|boot|main)\b/i.test(q);
23
+ }
24
+ function isArchitectureQuestion(q) {
25
+ if (ARCH_RE.test(q))
26
+ return true;
27
+ const lower = q.toLowerCase();
28
+ if (lower.includes("how") && /(?:project|repo|codebase)/i.test(q))
29
+ return true;
30
+ if (lower.includes("explain") && /(?:project|repo|codebase|\bthis\b)/i.test(q))
31
+ return true;
32
+ return false;
33
+ }
34
+ function isModuleQuestion(q) {
35
+ return MODULE_RE.test(q) || extractQueryTerms(q).length > 0;
36
+ }
13
37
  export async function answerDeterministicProjectQuestion(options) {
14
38
  const root = resolveRepoRoot(options.root);
15
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);
16
44
  const context = await retrieveProjectContext({
17
45
  root,
18
46
  query: q,
19
47
  budgetTokens: 4_000,
48
+ maxSourceFiles: 8,
49
+ progress,
20
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;
21
70
  const sections = [
22
71
  "Deterministic project answer (no LLM). Evidence from DNA, graph, brain, and dependency analyzers.",
23
72
  "",
@@ -28,6 +77,27 @@ export async function answerDeterministicProjectQuestion(options) {
28
77
  sections.push(`Frameworks: ${dna.frameworks.join(", ") || "UNKNOWN"}`);
29
78
  sections.push(`Monorepo: ${dna.monorepo.isMonorepo ? dna.monorepo.tool : "no"}`);
30
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
+ }
31
101
  if (AUTH_RE.test(q)) {
32
102
  const authFiles = context.citations
33
103
  .filter((c) => c.path && /auth|login|session|password/i.test(c.path))
@@ -61,7 +131,7 @@ export async function answerDeterministicProjectQuestion(options) {
61
131
  }
62
132
  sections.push("");
63
133
  }
64
- if (START_RE.test(q)) {
134
+ if (isStartQuestion(q)) {
65
135
  const startHits = context.citations
66
136
  .filter((c) => c.path && /(server|index|main|app)\.[jt]sx?$/i.test(c.path))
67
137
  .slice(0, 6);
@@ -139,11 +209,28 @@ export async function answerDeterministicProjectQuestion(options) {
139
209
  sections.push(`Lockfiles: ${deps.lockfilesPresent.join(", ") || "none"}`);
140
210
  sections.push("");
141
211
  }
142
- if (ARCH_RE.test(q)) {
212
+ if (isArchitectureQuestion(q) && brainCitations.length === 0) {
143
213
  try {
144
214
  const graph = await buildIntelligenceGraph({ root, mode: "auto" });
145
215
  sections.push(`[INFERRED] Graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges (${graph.builder})`);
146
- 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);
147
234
  for (const n of sample) {
148
235
  sections.push(` - ${n.kind}: ${n.label}${n.path ? ` @ ${n.path}` : ""}`);
149
236
  }
@@ -153,6 +240,17 @@ export async function answerDeterministicProjectQuestion(options) {
153
240
  }
154
241
  sections.push("");
155
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
+ }
156
254
  const brain = await getBrainStatus(root);
157
255
  if (brain.hasSnapshot) {
158
256
  sections.push(`[INFERRED] Project brain snapshot present (${brain.snapshotCount} stored; latest ${brain.latestSnapshotId ?? "unknown"}).`);
@@ -160,6 +258,14 @@ export async function answerDeterministicProjectQuestion(options) {
160
258
  else {
161
259
  sections.push("[UNKNOWN] No project brain snapshot — run brain compile for richer answers.");
162
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
+ }
163
269
  sections.push("");
164
270
  sections.push("Limitations: deterministic answers cannot invent behavior not present in evidence.");
165
271
  const bundle = {
@@ -170,7 +276,7 @@ export async function answerDeterministicProjectQuestion(options) {
170
276
  ],
171
277
  };
172
278
  return buildChatTurnResponse({
173
- sessionId: options.sessionId,
279
+ sessionId,
174
280
  modelText: sections.join("\n"),
175
281
  context: bundle,
176
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>;