Browse Source

fix(mcp): bold labels instead of ATX headings in tool results (#778) (#951)

MCP tool results used Markdown ATX headings (##/###/####) for section
headers — the status summary, each search hit, every file section in an
exploration — which Markdown-rendering clients (e.g. the Claude Code
VSCode extension) blow up to H1–H4 font size, filling the transcript with
oversized lines (worst on search/explore, where the noise scales with
result count). Swap them all for bold labels, which render at body size
while keeping the same structure. CLI/TTY output (ContextBuilder) is
unchanged — the issue notes it's fine.

The format is parse-coupled, so kept in sync:
- The explore truncation boundary and the offload chunker
  (reasoning/reasoner.ts) both key off the per-file header, now a unique
  `**`-prefixed marker emitted via a shared fileSectionHeader() helper.
- Updated the offload strip regexes and switched the opt-in report-style
  prompt off ATX headings (same client, same rendering issue).
- Updated test helpers (sectionFor, sourcedFiles, the callers
  section-boundary scan) that scanned the old markers.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby Mchenry 18 hours ago
parent
commit
3e1547bbe1

+ 1 - 0
CHANGELOG.md

@@ -31,6 +31,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 
 ### Fixes
 ### Fixes
 
 
+- MCP tool results no longer show up as oversized headings in Markdown-rendering clients (such as the Claude Code VSCode extension). Results used Markdown headings (`##`/`###`) for things like the status summary, each search hit, and every file section in an exploration, so a normal query filled the transcript with large-font lines — worst with `codegraph_search` and `codegraph_explore`, where the noise grew with the number of results. Section headers are now bold labels, which render at normal text size while keeping the same structure. Terminal/CLI output is unchanged. (#778)
 - An MCP server pointed at a very large repository (tens of thousands of files) no longer hangs on the first tool call after a fresh start. On startup CodeGraph reconciles its index against the current files on disk, and on a huge repo that reconcile could run for minutes while blocking the very first request — long enough that the background server was sometimes force-restarted mid-scan, so the first query never came back at all. The reconcile now yields as it runs (keeping the server responsive instead of pinning it), and the first tool call waits only briefly for it before answering and letting the rest finish in the background — so you get a fast first response and the index still catches up. Set `CODEGRAPH_CATCHUP_GATE_TIMEOUT_MS` to tune how long that first call waits (default 3000ms), or `=0` to always wait for the full reconcile. (#905)
 - An MCP server pointed at a very large repository (tens of thousands of files) no longer hangs on the first tool call after a fresh start. On startup CodeGraph reconciles its index against the current files on disk, and on a huge repo that reconcile could run for minutes while blocking the very first request — long enough that the background server was sometimes force-restarted mid-scan, so the first query never came back at all. The reconcile now yields as it runs (keeping the server responsive instead of pinning it), and the first tool call waits only briefly for it before answering and letting the rest finish in the background — so you get a fast first response and the index still catches up. Set `CODEGRAPH_CATCHUP_GATE_TIMEOUT_MS` to tune how long that first call waits (default 3000ms), or `=0` to always wait for the full reconcile. (#905)
 - `codegraph install` now wires up your agents and stops there — it no longer indexes the current directory. Building a project's graph is always the explicit `codegraph init` (or `codegraph index`), so you decide what gets indexed and when, and the steps are the same whether you installed globally or just for one project. This clears up the confusion where a project-local install silently indexed but a global one didn't, and where the docs and the tool disagreed about whether you still had to run `init`. (#826)
 - `codegraph install` now wires up your agents and stops there — it no longer indexes the current directory. Building a project's graph is always the explicit `codegraph init` (or `codegraph index`), so you decide what gets indexed and when, and the steps are the same whether you installed globally or just for one project. This clears up the confusion where a project-local install silently indexed but a global one didn't, and where the docs and the tool disagreed about whether you still had to run `init`. (#826)
 - React components declared with `forwardRef`, `memo`, or styled-components / emotion (`const Button = forwardRef(...)`, `const Card = memo(...)`, `const Box = styled.button\`…\``) are now recognized as components, so finding where they're used works. Before, they were indexed as plain constants, so `codegraph callers` and impact analysis reported "no callers found" even when the component was rendered across dozens of files — a dangerous false "safe to change" right before refactoring a shared component. Now every `<Button/>` usage links back to the component, so callers and blast radius are complete. This is the standard shadcn/ui declaration style, so for typical React design systems the whole UI layer is no longer invisible to impact analysis. Thanks @Arlandaren for the report and @maxmilian for the root-cause. (#841)
 - React components declared with `forwardRef`, `memo`, or styled-components / emotion (`const Button = forwardRef(...)`, `const Card = memo(...)`, `const Box = styled.button\`…\``) are now recognized as components, so finding where they're used works. Before, they were indexed as plain constants, so `codegraph callers` and impact analysis reported "no callers found" even when the component was rendered across dozens of files — a dangerous false "safe to change" right before refactoring a shared component. Now every `<Button/>` usage links back to the component, so callers and blast radius are complete. This is the standard shadcn/ui declaration style, so for typical React design systems the whole UI layer is no longer invisible to impact analysis. Thanks @Arlandaren for the report and @maxmilian for the root-cause. (#841)

+ 7 - 6
__tests__/adaptive-explore-sizing.test.ts

@@ -35,15 +35,16 @@ import CodeGraph from '../src/index';
 // (the steer-to-explore phrasing changed when the Read invitation was removed).
 // (the steer-to-explore phrasing changed when the Read invitation was removed).
 const SKELETON_MARK = '· skeleton (signatures only';
 const SKELETON_MARK = '· skeleton (signatures only';
 
 
-/** Return the `#### <path> ...` section for a file basename, header through the
- *  line before the next `###`/`####` header (or end of output). */
+/** Return the ``**`<path>`** ...`` section for a file basename, header through the
+ *  line before the next bold header (or end of output). Headers are bold labels,
+ *  not ATX headings (issue #778); file sections start with ``**` ``. */
 function sectionFor(text: string, basename: string): string {
 function sectionFor(text: string, basename: string): string {
   const lines = text.split('\n');
   const lines = text.split('\n');
-  const start = lines.findIndex((l) => l.startsWith('#### ') && l.includes(basename));
+  const start = lines.findIndex((l) => l.startsWith('**`') && l.includes(basename));
   if (start < 0) return '';
   if (start < 0) return '';
   let end = lines.length;
   let end = lines.length;
   for (let i = start + 1; i < lines.length; i++) {
   for (let i = start + 1; i < lines.length; i++) {
-    if (lines[i].startsWith('### ') || lines[i].startsWith('#### ')) {
+    if (lines[i].startsWith('**')) {
       end = i;
       end = i;
       break;
       break;
     }
     }
@@ -284,7 +285,7 @@ export class YamlCodec extends Codec {
     const text = result.content?.[0]?.text ?? '';
     const text = result.content?.[0]?.text ?? '';
 
 
     // Precondition: the spine must have formed, or nothing skeletonizes.
     // Precondition: the spine must have formed, or nothing skeletonizes.
-    expect(text).toContain('## Flow (call path among the symbols you queried)');
+    expect(text).toContain('**Flow (call path among the symbols you queried)');
 
 
     for (const [file, marker] of [
     for (const [file, marker] of [
       ['bridge-interceptor.ts', 'BRIDGE_BODY_MARKER'],
       ['bridge-interceptor.ts', 'BRIDGE_BODY_MARKER'],
@@ -345,7 +346,7 @@ export class YamlCodec extends Codec {
   it('spares an off-spine sibling when the agent NAMED a callable in it (RealCall fix)', async () => {
   it('spares an off-spine sibling when the agent NAMED a callable in it (RealCall fix)', async () => {
     const result = await handler.execute('codegraph_explore', { query: SPARE_QUERY, maxFiles: 15 });
     const result = await handler.execute('codegraph_explore', { query: SPARE_QUERY, maxFiles: 15 });
     const text = result.content?.[0]?.text ?? '';
     const text = result.content?.[0]?.text ?? '';
-    expect(text).toContain('## Flow (call path among the symbols you queried)');
+    expect(text).toContain('**Flow (call path among the symbols you queried)');
 
 
     // auth-interceptor.ts is an off-spine Interceptor sibling — would skeletonize —
     // auth-interceptor.ts is an off-spine Interceptor sibling — would skeletonize —
     // but the agent named its method `authenticate`, so it stays FULL.
     // but the agent named its method `authenticate`, so it stays FULL.

+ 12 - 12
__tests__/dynamic-boundaries.test.ts

@@ -184,7 +184,7 @@ describe('codegraph_explore — dynamic boundaries', () => {
     const res = await handler.execute('codegraph_explore', { query: 'routeSave onSave' });
     const res = await handler.execute('codegraph_explore', { query: 'routeSave onSave' });
     const text = res.content[0].text as string;
     const text = res.content[0].text as string;
 
 
-    expect(text).toContain('## Dynamic boundaries');
+    expect(text).toContain('**Dynamic boundaries');
     expect(text).toContain('computed member call');
     expect(text).toContain('computed member call');
     expect(text).toMatch(/router\.ts:6/); // the exact dispatch site
     expect(text).toMatch(/router\.ts:6/); // the exact dispatch site
     expect(text).toContain('candidates for key `save`');
     expect(text).toContain('candidates for key `save`');
@@ -212,7 +212,7 @@ describe('codegraph_explore — dynamic boundaries', () => {
     const res = await handler.execute('codegraph_explore', { query: 'route onSave' });
     const res = await handler.execute('codegraph_explore', { query: 'route onSave' });
     const text = res.content[0].text as string;
     const text = res.content[0].text as string;
 
 
-    expect(text).toContain('## Dynamic boundaries');
+    expect(text).toContain('**Dynamic boundaries');
     expect(text).toContain('computed member call');
     expect(text).toContain('computed member call');
     expect(text).not.toContain('candidates for key'); // runtime key → no shortlist to claim
     expect(text).not.toContain('candidates for key'); // runtime key → no shortlist to claim
   });
   });
@@ -234,7 +234,7 @@ describe('codegraph_explore — dynamic boundaries', () => {
     // `processPayment` does not exist anywhere — only `route` resolves.
     // `processPayment` does not exist anywhere — only `route` resolves.
     const res = await handler.execute('codegraph_explore', { query: 'route processPayment' });
     const res = await handler.execute('codegraph_explore', { query: 'route processPayment' });
     const text = res.content[0].text as string;
     const text = res.content[0].text as string;
-    expect(text).toContain('## Dynamic boundaries');
+    expect(text).toContain('**Dynamic boundaries');
   });
   });
 
 
   it('renders a direct synthesized emit→handler hop as a dynamic-dispatch link (#687 criterion 1)', async () => {
   it('renders a direct synthesized emit→handler hop as a dynamic-dispatch link (#687 criterion 1)', async () => {
@@ -267,11 +267,11 @@ describe('codegraph_explore — dynamic boundaries', () => {
     const res = await handler.execute('codegraph_explore', { query: 'completeCheckout settleInvoice' });
     const res = await handler.execute('codegraph_explore', { query: 'completeCheckout settleInvoice' });
     const text = res.content[0].text as string;
     const text = res.content[0].text as string;
 
 
-    expect(text).toContain('## Dynamic-dispatch links among your symbols');
+    expect(text).toContain('**Dynamic-dispatch links among your symbols');
     expect(text).toMatch(/completeCheckout → settleInvoice/);
     expect(text).toMatch(/completeCheckout → settleInvoice/);
     expect(text).toContain('invoice.settled');
     expect(text).toContain('invoice.settled');
     // Connected via the synthesized edge — no boundary to announce.
     // Connected via the synthesized edge — no boundary to announce.
-    expect(text).not.toContain('## Dynamic boundaries');
+    expect(text).not.toContain('**Dynamic boundaries');
   });
   });
 
 
   it('never adds the section to a fully connected flow', async () => {
   it('never adds the section to a fully connected flow', async () => {
@@ -285,8 +285,8 @@ describe('codegraph_explore — dynamic boundaries', () => {
 
 
     const res = await handler.execute('codegraph_explore', { query: 'stepOne stepThree' });
     const res = await handler.execute('codegraph_explore', { query: 'stepOne stepThree' });
     const text = res.content[0].text as string;
     const text = res.content[0].text as string;
-    expect(text).toContain('## Flow');
-    expect(text).not.toContain('## Dynamic boundaries');
+    expect(text).toContain('**Flow');
+    expect(text).not.toContain('**Dynamic boundaries');
   });
   });
 
 
   it('python getattr dispatch surfaces with a prefix-key candidate', async () => {
   it('python getattr dispatch surfaces with a prefix-key candidate', async () => {
@@ -305,7 +305,7 @@ describe('codegraph_explore — dynamic boundaries', () => {
     const res = await handler.execute('codegraph_explore', { query: 'process handle_save' });
     const res = await handler.execute('codegraph_explore', { query: 'process handle_save' });
     const text = res.content[0].text as string;
     const text = res.content[0].text as string;
 
 
-    expect(text).toContain('## Dynamic boundaries');
+    expect(text).toContain('**Dynamic boundaries');
     expect(text).toContain('getattr');
     expect(text).toContain('getattr');
     expect(text).toContain('handle_save');
     expect(text).toContain('handle_save');
   });
   });
@@ -373,7 +373,7 @@ describe('codegraph_explore — interface dispatch', () => {
     const res = await handler.execute('codegraph_explore', { query: 'processRunExecutionData executeNode execute' });
     const res = await handler.execute('codegraph_explore', { query: 'processRunExecutionData executeNode execute' });
     const text = res.content[0].text as string;
     const text = res.content[0].text as string;
 
 
-    expect(text).toContain('## Interface dispatch (a named method has many implementations)');
+    expect(text).toContain('**Interface dispatch (a named method has many implementations)');
     expect(text).toMatch(/`execute` → runtime dispatch to \*\*9\*\* types implementing `INodeType`/);
     expect(text).toMatch(/`execute` → runtime dispatch to \*\*9\*\* types implementing `INodeType`/);
     // a couple of concrete targets, with file:line
     // a couple of concrete targets, with file:line
     expect(text).toMatch(/\b\w+Node\.execute` \(/);
     expect(text).toMatch(/\b\w+Node\.execute` \(/);
@@ -392,8 +392,8 @@ describe('codegraph_explore — interface dispatch', () => {
 
 
     const res = await handler.execute('codegraph_explore', { query: 'stepOne stepThree' });
     const res = await handler.execute('codegraph_explore', { query: 'stepOne stepThree' });
     const text = res.content[0].text as string;
     const text = res.content[0].text as string;
-    expect(text).toContain('## Flow');
-    expect(text).not.toContain('## Interface dispatch');
+    expect(text).toContain('**Flow');
+    expect(text).not.toContain('**Interface dispatch');
   });
   });
 
 
   it('stays SILENT when the interface family is below the polymorphism threshold (3 impls)', async () => {
   it('stays SILENT when the interface family is below the polymorphism threshold (3 impls)', async () => {
@@ -401,6 +401,6 @@ describe('codegraph_explore — interface dispatch', () => {
 
 
     const res = await handler.execute('codegraph_explore', { query: 'processRunExecutionData executeNode execute' });
     const res = await handler.execute('codegraph_explore', { query: 'processRunExecutionData executeNode execute' });
     const text = res.content[0].text as string;
     const text = res.content[0].text as string;
-    expect(text).not.toContain('## Interface dispatch');
+    expect(text).not.toContain('**Interface dispatch');
   });
   });
 });
 });

+ 1 - 1
__tests__/explore-blast-radius.test.ts

@@ -55,7 +55,7 @@ describe('codegraph_explore — blast radius', () => {
     const res = await handler.execute('codegraph_explore', { query: 'target' });
     const res = await handler.execute('codegraph_explore', { query: 'target' });
     const text = res.content[0].text;
     const text = res.content[0].text;
 
 
-    expect(text).toContain('### Blast radius');
+    expect(text).toContain('**Blast radius');
     expect(text).toContain('`target`');
     expect(text).toContain('`target`');
     expect(text).toMatch(/caller/); // a caller count is reported
     expect(text).toMatch(/caller/); // a caller count is reported
     // It names WHERE (the caller file) — not the caller's source body.
     // It names WHERE (the caller file) — not the caller's source body.

+ 3 - 2
__tests__/explore-corroboration-ranking.test.ts

@@ -28,11 +28,12 @@ import * as os from 'os';
 import CodeGraph from '../src/index';
 import CodeGraph from '../src/index';
 import { ToolHandler } from '../src/mcp/tools';
 import { ToolHandler } from '../src/mcp/tools';
 
 
-/** Paths that explore rendered as full-body `#### <path> —` source sections. */
+/** Paths that explore rendered as full-body ``**`<path>`** —`` source sections.
+ *  Headers are bold labels, not ATX headings (issue #778). */
 function sourcedFiles(text: string): string[] {
 function sourcedFiles(text: string): string[] {
   const out: string[] = [];
   const out: string[] = [];
   for (const line of text.split('\n')) {
   for (const line of text.split('\n')) {
-    const m = line.match(/^#### (.+?) —/);
+    const m = line.match(/^\*\*`(.+?)`\*\* —/);
     if (m) out.push(m[1].trim());
     if (m) out.push(m[1].trim());
   }
   }
   return out;
   return out;

+ 2 - 2
__tests__/explore-output-budget.test.ts

@@ -206,8 +206,8 @@ describe('codegraph_explore output respects the adaptive budget', () => {
     const text = result.content?.[0]?.text ?? '';
     const text = result.content?.[0]?.text ?? '';
     // Either there are relationships, or no edges were significant — both are fine.
     // Either there are relationships, or no edges were significant — both are fine.
     // We just want to confirm we did not accidentally gate it off.
     // We just want to confirm we did not accidentally gate it off.
-    const hasRelationships = text.includes('### Relationships');
-    const sourceFollowsHeader = text.indexOf('### Source Code') > 0;
+    const hasRelationships = text.includes('**Relationships');
+    const sourceFollowsHeader = text.indexOf('**Source Code') > 0;
     expect(hasRelationships || sourceFollowsHeader).toBe(true);
     expect(hasRelationships || sourceFollowsHeader).toBe(true);
   });
   });
 
 

+ 2 - 2
__tests__/explore-synth-constant-endpoints.test.ts

@@ -9,7 +9,7 @@
  * "### Relationships" section would have caught it, but that is disabled below 500 files.
  * "### Relationships" section would have caught it, but that is disabled below 500 files.
  * Net: on a small RTK app the synthesized edge existed in the graph yet was invisible to
  * Net: on a small RTK app the synthesized edge existed in the graph yet was invisible to
  * the agent. The fix feeds a `dynNamed` set (named non-callable endpoints that participate
  * the agent. The fix feeds a `dynNamed` set (named non-callable endpoints that participate
- * in a heuristic edge) to the tier-independent "## Dynamic-dispatch links" scan. This test
+ * in a heuristic edge) to the tier-independent "**Dynamic-dispatch links**" scan. This test
  * pins it on a deliberately tiny (<150-file) fixture so the Relationships gate is OFF and
  * pins it on a deliberately tiny (<150-file) fixture so the Relationships gate is OFF and
  * the dynamic-dispatch-links path is the ONLY thing that can surface the hop.
  * the dynamic-dispatch-links path is the ONLY thing that can surface the hop.
  */
  */
@@ -77,7 +77,7 @@ export const outerThunk = createAsyncThunk('app/outer', async (n: number, { disp
 
 
     // The synthesized hop now surfaces (was invisible: both endpoints `constant` AND the
     // The synthesized hop now surfaces (was invisible: both endpoints `constant` AND the
     // small-repo Relationships section is off).
     // small-repo Relationships section is off).
-    expect(text).toContain('## Dynamic-dispatch links among your symbols');
+    expect(text).toContain('**Dynamic-dispatch links among your symbols');
     expect(text).toMatch(/outerThunk\s+→\s+innerThunk/);
     expect(text).toMatch(/outerThunk\s+→\s+innerThunk/);
     // It reads as a dynamic-dispatch bridge with its wiring site, not a bare `calls`.
     // It reads as a dynamic-dispatch bridge with its wiring site, not a bare `calls`.
     expect(text).toMatch(/dynamic: redux thunk @/);
     expect(text).toMatch(/dynamic: redux thunk @/);

+ 2 - 2
__tests__/mcp-staleness-banner.test.ts

@@ -175,7 +175,7 @@ describe('MCP staleness banner', () => {
 
 
     const res = await handler.execute('codegraph_status', {});
     const res = await handler.execute('codegraph_status', {});
     const text = res.content[0].text;
     const text = res.content[0].text;
-    expect(text).toContain('### Pending sync:');
+    expect(text).toContain('**Pending sync:');
     expect(text).toContain('src/charlie-only.ts');
     expect(text).toContain('src/charlie-only.ts');
     // Status embeds the info first-class, so the auto-banner is suppressed.
     // Status embeds the info first-class, so the auto-banner is suppressed.
     expect(text.startsWith('⚠️')).toBe(false);
     expect(text.startsWith('⚠️')).toBe(false);
@@ -204,7 +204,7 @@ describe('MCP staleness banner', () => {
 
 
     const res = await handler.execute('codegraph_status', {});
     const res = await handler.execute('codegraph_status', {});
     const text = res.content[0].text;
     const text = res.content[0].text;
-    expect(text).toContain('### Auto-sync disabled:');
+    expect(text).toContain('**Auto-sync disabled:');
     expect(text).toContain('OS watch/file limit exhausted');
     expect(text).toContain('OS watch/file limit exhausted');
     // status renders the notice inline, so the auto-banner is not also prepended.
     // status renders the notice inline, so the auto-banner is not also prepended.
     expect(text.startsWith('⚠️')).toBe(false);
     expect(text.startsWith('⚠️')).toBe(false);

+ 1 - 1
__tests__/node-file-view.test.ts

@@ -99,7 +99,7 @@ describe('codegraph_node file-view (Read replacement)', () => {
 
 
   it('symbolsOnly returns the structural map, not the source', async () => {
   it('symbolsOnly returns the structural map, not the source', async () => {
     const out = await text({ file: 'a.ts', symbolsOnly: true });
     const out = await text({ file: 'a.ts', symbolsOnly: true });
-    expect(out).toContain('### Symbols');
+    expect(out).toContain('**Symbols');
     expect(out).toContain('helper');
     expect(out).toContain('helper');
     expect(out).toContain('Widget');
     expect(out).toContain('Widget');
     expect(out).not.toContain('return x + 1'); // bodies are NOT included in the map
     expect(out).not.toContain('return x + 1'); // bodies are NOT included in the map

+ 4 - 4
__tests__/offload.test.ts

@@ -231,16 +231,16 @@ describe('reasoning offload', () => {
   describe('stripAgentDirectives', () => {
   describe('stripAgentDirectives', () => {
     it('drops the agent-directed header but keeps source sections', () => {
     it('drops the agent-directed header but keeps source sections', () => {
       const ctx = [
       const ctx = [
-        '## Exploration: how does X work',
+        '**Exploration: how does X work**',
         'Found 12 symbols across 3 files.',
         'Found 12 symbols across 3 files.',
         '',
         '',
-        '#### src/a.ts — foo(function)',
+        '**`src/a.ts`** — foo(function)',
         'code body',
         'code body',
       ].join('\n');
       ].join('\n');
       const stripped = stripAgentDirectives(ctx);
       const stripped = stripAgentDirectives(ctx);
-      expect(stripped).not.toContain('## Exploration:');
+      expect(stripped).not.toContain('**Exploration:');
       expect(stripped).not.toContain('Found 12 symbols');
       expect(stripped).not.toContain('Found 12 symbols');
-      expect(stripped).toContain('#### src/a.ts');
+      expect(stripped).toContain('**`src/a.ts`');
       expect(stripped).toContain('code body');
       expect(stripped).toContain('code body');
     });
     });
   });
   });

+ 4 - 1
__tests__/same-name-disambiguation.test.ts

@@ -99,7 +99,10 @@ describe('same-named symbols across apps (#764)', () => {
     expect(out).toContain('apps/billing/src/users/user.service.ts');
     expect(out).toContain('apps/billing/src/users/user.service.ts');
     // …and the billing section must list the billing controller, not admin's.
     // …and the billing section must list the billing controller, not admin's.
     const billingSection = out.slice(out.indexOf('apps/billing/src/users/user.service.ts'));
     const billingSection = out.slice(out.indexOf('apps/billing/src/users/user.service.ts'));
-    const billingBody = billingSection.slice(0, billingSection.indexOf('###', 3) > 0 ? billingSection.indexOf('###', 3) : undefined);
+    // The next definition heading is a line-start bold label (issue #778: ATX `###`
+    // headings became `**…**`); billingSection starts mid-heading, so `\n**` finds it.
+    const nextDef = billingSection.indexOf('\n**');
+    const billingBody = billingSection.slice(0, nextDef > 0 ? nextDef : undefined);
     expect(billingBody).toContain('apps/billing/src/users/user.controller.ts');
     expect(billingBody).toContain('apps/billing/src/users/user.controller.ts');
     expect(billingBody).not.toContain('apps/admin/src/users/user.controller.ts');
     expect(billingBody).not.toContain('apps/admin/src/users/user.controller.ts');
   });
   });

+ 1 - 1
scripts/agent-eval/offload-eval-metrics.mjs

@@ -48,7 +48,7 @@ for (const line of lines) {
       // "### Referenced source — verbatim" appendix). A refs call that cited nothing
       // "### Referenced source — verbatim" appendix). A refs call that cited nothing
       // valid falls back to RAW source, which is correctly counted as a raw explore below.
       // valid falls back to RAW source, which is correctly counted as a raw explore below.
       if (/Synthesized by CodeGraph|### Referenced source — verbatim/.test(text)) { offloadAnswers.push(text); exploreResults++; }
       if (/Synthesized by CodeGraph|### Referenced source — verbatim/.test(text)) { offloadAnswers.push(text); exploreResults++; }
-      else if (/Found \d+ symbols? across|## Exploration:/.test(text)) exploreResults++;
+      else if (/Found \d+ symbols? across|\*\*Exploration:/.test(text)) exploreResults++;
     }
     }
   }
   }
   if (ev.type === 'result') result = ev;
   if (ev.type === 'result') result = ev;

+ 1 - 1
scripts/agent-eval/probe-explore.mjs

@@ -36,5 +36,5 @@ console.log(text);
 console.error('\n--- PROBE STATS ---');
 console.error('\n--- PROBE STATS ---');
 console.error('output chars:', text.length);
 console.error('output chars:', text.length);
 console.error('triggerRender body present (-> setState({})):', /triggerRender[\s\S]{0,400}setState\(\{\}\)/.test(text));
 console.error('triggerRender body present (-> setState({})):', /triggerRender[\s\S]{0,400}setState\(\{\}\)/.test(text));
-console.error('App.tsx in source section:', /#### .*App\.tsx —/.test(text));
+console.error('App.tsx in source section:', /\*\*`.*App\.tsx`\*\* —/.test(text));
 try { cg.close?.(); } catch {}
 try { cg.close?.(); } catch {}

+ 56 - 39
src/mcp/tools.ts

@@ -134,7 +134,7 @@ export interface ExploreOutputBudget {
   maxCharsPerFile: number;
   maxCharsPerFile: number;
   /** Cluster gap threshold in lines — tighter clustering on small projects. */
   /** Cluster gap threshold in lines — tighter clustering on small projects. */
   gapThreshold: number;
   gapThreshold: number;
-  /** Max symbols listed in the per-file header (`#### path — sym(kind), ...`). */
+  /** Max symbols listed in the per-file header (``**`path`** — sym(kind), ...``). */
   maxSymbolsInFileHeader: number;
   maxSymbolsInFileHeader: number;
   /** Max edges shown per relationship kind in the Relationships section. */
   /** Max edges shown per relationship kind in the Relationships section. */
   maxEdgesPerRelationshipKind: number;
   maxEdgesPerRelationshipKind: number;
@@ -326,6 +326,23 @@ function numberSourceLines(slice: string, firstLineNumber: number): string {
   return out.join('\n');
   return out.join('\n');
 }
 }
 
 
+/**
+ * Unique line-prefix for a per-file source section in codegraph_explore output.
+ * Issue #778: tool results dropped ATX headings (`####`, `##`, `###`) for bold
+ * labels so Markdown-rendering MCP clients (e.g. the Claude Code VSCode
+ * extension) stop blowing every header up to H1–H4. The path is bold + a code
+ * span so it still reads as a header, and the leading ``**` `` stays a UNIQUE,
+ * greppable marker — no other explore line begins with it — that the explore
+ * truncation boundary (`handleExplore`) and the offload chunker
+ * (`reasoning/reasoner.ts`) both key off to cut on whole file sections.
+ */
+const FILE_SECTION_PREFIX = '**`';
+function fileSectionHeader(filePath: string, suffix: string): string {
+  return suffix
+    ? `${FILE_SECTION_PREFIX}${filePath}\`** — ${suffix}`
+    : `${FILE_SECTION_PREFIX}${filePath}\`**`;
+}
+
 /**
 /**
  * Per-file staleness banner emitted at the top of a tool response when the
  * Per-file staleness banner emitted at the top of a tool response when the
  * file watcher has pending events for files referenced by the response.
  * file watcher has pending events for files referenced by the response.
@@ -1350,7 +1367,7 @@ export class ToolHandler {
   private definitionHeading(group: Node[]): string {
   private definitionHeading(group: Node[]): string {
     const head = group[0]!;
     const head = group[0]!;
     const line = head.startLine ? `:${head.startLine}` : '';
     const line = head.startLine ? `:${head.startLine}` : '';
-    return `### ${head.qualifiedName} (${head.kind}) — ${head.filePath}${line}`;
+    return `**${head.qualifiedName}** (${head.kind}) — ${head.filePath}${line}`;
   }
   }
 
 
   /**
   /**
@@ -1408,7 +1425,7 @@ export class ToolHandler {
     // agent never mistakes one app's callers for another's. Narrow with
     // agent never mistakes one app's callers for another's. Narrow with
     // `file` to focus a single definition.
     // `file` to focus a single definition.
     const lines: string[] = [
     const lines: string[] = [
-      `## Callers of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)`,
+      `**Callers of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)**`,
     ];
     ];
     for (const group of groups) {
     for (const group of groups) {
       const { callers, labels } = collect(group);
       const { callers, labels } = collect(group);
@@ -1478,7 +1495,7 @@ export class ToolHandler {
 
 
     // Multiple DISTINCT definitions (#764): per-definition sections.
     // Multiple DISTINCT definitions (#764): per-definition sections.
     const lines: string[] = [
     const lines: string[] = [
-      `## Callees of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)`,
+      `**Callees of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)**`,
     ];
     ];
     for (const group of groups) {
     for (const group of groups) {
       const { callees, labels } = collect(group);
       const { callees, labels } = collect(group);
@@ -1547,7 +1564,7 @@ export class ToolHandler {
     // merging unrelated same-named classes (one UserService per monorepo app)
     // merging unrelated same-named classes (one UserService per monorepo app)
     // overstated impact and confused agents. Narrow with `file`.
     // overstated impact and confused agents. Narrow with `file`.
     const sections: string[] = [
     const sections: string[] = [
-      `## Impact of ${symbol} — ${groups.length} distinct definitions (each with its own blast radius; narrow with \`file\`)`,
+      `**Impact of ${symbol} — ${groups.length} distinct definitions (each with its own blast radius; narrow with \`file\`)**`,
     ];
     ];
     for (const group of groups) {
     for (const group of groups) {
       const head = group[0]!;
       const head = group[0]!;
@@ -1765,7 +1782,7 @@ export class ToolHandler {
         if (synthLines.length === 0 && !boundaries) return EMPTY;
         if (synthLines.length === 0 && !boundaries) return EMPTY;
         const out: string[] = [];
         const out: string[] = [];
         if (synthLines.length) out.push(
         if (synthLines.length) out.push(
-          '## Dynamic-dispatch links among your symbols',
+          '**Dynamic-dispatch links among your symbols**',
           '(synthesized — the indirect hops grep/Read would reconstruct; the `@file:line` is the wiring site)',
           '(synthesized — the indirect hops grep/Read would reconstruct; the `@file:line` is the wiring site)',
           '', ...synthLines, '');
           '', ...synthLines, '');
         if (boundaries) out.push(boundaries);
         if (boundaries) out.push(boundaries);
@@ -1880,7 +1897,7 @@ export class ToolHandler {
       if (!hasMain && synthLines.length === 0 && !boundaryText && !polyText) return EMPTY;
       if (!hasMain && synthLines.length === 0 && !boundaryText && !polyText) return EMPTY;
       const out: string[] = [];
       const out: string[] = [];
       if (hasMain) {
       if (hasMain) {
-        out.push('## Flow (call path among the symbols you queried)', '');
+        out.push('**Flow (call path among the symbols you queried)**', '');
         for (let i = 0; i < best!.length; i++) {
         for (let i = 0; i < best!.length; i++) {
           const step = best![i]!;
           const step = best![i]!;
           if (step.edge) { const sy = this.synthEdgeNote(step.edge); out.push(`   ↓ ${sy ? sy.compact : step.edge.kind}`); }
           if (step.edge) { const sy = this.synthEdgeNote(step.edge); out.push(`   ↓ ${sy ? sy.compact : step.edge.kind}`); }
@@ -1890,7 +1907,7 @@ export class ToolHandler {
       }
       }
       if (synthLines.length) {
       if (synthLines.length) {
         out.push(
         out.push(
-          '## Dynamic-dispatch links among your symbols',
+          '**Dynamic-dispatch links among your symbols**',
           '(synthesized — the indirect hops grep/Read would reconstruct; the `@file:line` is the wiring site)',
           '(synthesized — the indirect hops grep/Read would reconstruct; the `@file:line` is the wiring site)',
           '',
           '',
           ...synthLines,
           ...synthLines,
@@ -1958,7 +1975,7 @@ export class ToolHandler {
     }
     }
     if (notes.length === 0) return '';
     if (notes.length === 0) return '';
     return [
     return [
-      '## Dynamic boundaries (the static path ends at runtime dispatch)',
+      '**Dynamic boundaries (the static path ends at runtime dispatch)**',
       '',
       '',
       ...notes,
       ...notes,
       '',
       '',
@@ -2045,7 +2062,7 @@ export class ToolHandler {
     }
     }
     if (notes.length === 0) return '';
     if (notes.length === 0) return '';
     return [
     return [
-      '## Interface dispatch (a named method has many implementations)',
+      '**Interface dispatch (a named method has many implementations)**',
       '',
       '',
       ...notes,
       ...notes,
       '',
       '',
@@ -2174,7 +2191,7 @@ export class ToolHandler {
     if (entries.length === 0) return '';
     if (entries.length === 0) return '';
 
 
     return [
     return [
-      '### Blast radius — what depends on these (update/verify before editing)',
+      '**Blast radius — what depends on these (update/verify before editing)**',
       '',
       '',
       ...entries,
       ...entries,
       '',
       '',
@@ -2643,7 +2660,7 @@ export class ToolHandler {
 
 
     // Step 3: Build relationship map
     // Step 3: Build relationship map
     const lines: string[] = [
     const lines: string[] = [
-      `## Exploration: ${query}`,
+      `**Exploration: ${query}**`,
       '',
       '',
       `Found ${subgraph.nodes.size} symbols across ${fileGroups.size} files.`,
       `Found ${subgraph.nodes.size} symbols across ${fileGroups.size} files.`,
       '',
       '',
@@ -2661,7 +2678,7 @@ export class ToolHandler {
     );
     );
 
 
     if (budget.includeRelationships && significantEdges.length > 0) {
     if (budget.includeRelationships && significantEdges.length > 0) {
-      lines.push('### Relationships');
+      lines.push('**Relationships**');
       lines.push('');
       lines.push('');
 
 
       // Group edges by kind for readability
       // Group edges by kind for readability
@@ -2748,7 +2765,7 @@ export class ToolHandler {
       return false;
       return false;
     };
     };
 
 
-    lines.push('### Source Code');
+    lines.push('**Source Code**');
     lines.push('');
     lines.push('');
     lines.push('> The code below is the **verbatim, current on-disk source** of these files — re-read from disk on this call and line-numbered, byte-for-byte identical to what the Read tool returns. It is NOT a summary, outline, or stale cache. Treat each block as a Read you have already performed: do not Read a file shown here.');
     lines.push('> The code below is the **verbatim, current on-disk source** of these files — re-read from disk on this call and line-numbered, byte-for-byte identical to what the Read tool returns. It is NOT a summary, outline, or stale cache. Treat each block as a Read you have already performed: do not Read a file shown here.');
     lines.push('');
     lines.push('');
@@ -2892,7 +2909,7 @@ export class ToolHandler {
           const tag = bodyIds.size > 0
           const tag = bodyIds.size > 0
             ? 'focused (the methods you named in full, the rest as signatures — codegraph_explore a signature by name for its body; do NOT Read)'
             ? 'focused (the methods you named in full, the rest as signatures — codegraph_explore a signature by name for its body; do NOT Read)'
             : 'skeleton (signatures only — codegraph_explore a name for its full body; do NOT Read)';
             : 'skeleton (signatures only — codegraph_explore a name for its full body; do NOT Read)';
-          lines.push(`#### ${filePath} — ${names} · ${tag}`, '', '```' + lang, skel.join('\n'), '```', '');
+          lines.push(fileSectionHeader(filePath, `${names} · ${tag}`), '', '```' + lang, skel.join('\n'), '```', '');
           totalChars += skel.join('\n').length + 120;
           totalChars += skel.join('\n').length + 120;
           filesIncluded++;
           filesIncluded++;
           continue;
           continue;
@@ -2933,7 +2950,7 @@ export class ToolHandler {
         )];
         )];
         const headerNames = uniqSymbols.slice(0, budget.maxSymbolsInFileHeader);
         const headerNames = uniqSymbols.slice(0, budget.maxSymbolsInFileHeader);
         const omitted = uniqSymbols.length - headerNames.length;
         const omitted = uniqSymbols.length - headerNames.length;
-        const wholeHeader = `#### ${filePath} — ${omitted > 0 ? `${headerNames.join(', ')}, +${omitted} more` : headerNames.join(', ')}`;
+        const wholeHeader = fileSectionHeader(filePath, omitted > 0 ? `${headerNames.join(', ')}, +${omitted} more` : headerNames.join(', '));
 
 
         if (!fileNecessary && totalChars + wholeSection.length + 200 > budget.maxOutputChars) {
         if (!fileNecessary && totalChars + wholeSection.length + 200 > budget.maxOutputChars) {
           // Don't slice a whole file mid-method: an incidental file that doesn't
           // Don't slice a whole file mid-method: an incidental file that doesn't
@@ -3200,7 +3217,7 @@ export class ToolHandler {
       const headerSuffix = omittedCount > 0
       const headerSuffix = omittedCount > 0
         ? `${headerSymbols.join(', ')}, +${omittedCount} more`
         ? `${headerSymbols.join(', ')}, +${omittedCount} more`
         : headerSymbols.join(', ');
         : headerSymbols.join(', ');
-      const fileHeader = `#### ${filePath} — ${headerSuffix}`;
+      const fileHeader = fileSectionHeader(filePath, headerSuffix);
 
 
       // The total cap bounds INCIDENTAL files only. A file that DEFINES a symbol
       // The total cap bounds INCIDENTAL files only. A file that DEFINES a symbol
       // the agent named (or that's on the flow spine) renders even when the
       // the agent named (or that's on the flow spine) renders even when the
@@ -3241,7 +3258,7 @@ export class ToolHandler {
         .sort((a, b) => b[1].score - a[1].score);
         .sort((a, b) => b[1].score - a[1].score);
       const remainingFiles = [...remainingRelevant, ...peripheralFiles];
       const remainingFiles = [...remainingRelevant, ...peripheralFiles];
       if (remainingFiles.length > 0) {
       if (remainingFiles.length > 0) {
-        lines.push('### Not shown above — explore these names for their source');
+        lines.push('**Not shown above — explore these names for their source**');
         lines.push('');
         lines.push('');
         for (const [filePath, group] of remainingFiles.slice(0, 10)) {
         for (const [filePath, group] of remainingFiles.slice(0, 10)) {
           const symbols = group.nodes.map(n => `${n.name}:${n.startLine}`).join(', ');
           const symbols = group.nodes.map(n => `${n.name}:${n.startLine}`).join(', ');
@@ -3290,13 +3307,13 @@ export class ToolHandler {
 
 
     const hardCeiling = Math.min(Math.round(budget.maxOutputChars * 1.5), 25000);
     const hardCeiling = Math.min(Math.round(budget.maxOutputChars * 1.5), 25000);
     if (output.length > hardCeiling) {
     if (output.length > hardCeiling) {
-      // Cut at a FILE-SECTION boundary (the last `#### ` header before the
+      // Cut at a FILE-SECTION boundary (the last ``**` `` file header before the
       // ceiling) so we drop whole trailing file-sections rather than slicing
       // ceiling) so we drop whole trailing file-sections rather than slicing
       // through a method body — a half-rendered method just forces the Read this
       // through a method body — a half-rendered method just forces the Read this
       // tool exists to prevent. Fall back to a line boundary only if no section
       // tool exists to prevent. Fall back to a line boundary only if no section
       // header sits in the back half (degenerate single-giant-section case).
       // header sits in the back half (degenerate single-giant-section case).
       const cut = output.slice(0, hardCeiling);
       const cut = output.slice(0, hardCeiling);
-      const lastSection = cut.lastIndexOf('\n#### ');
+      const lastSection = cut.lastIndexOf('\n' + FILE_SECTION_PREFIX);
       const boundary = lastSection > hardCeiling * 0.5 ? lastSection : cut.lastIndexOf('\n');
       const boundary = lastSection > hardCeiling * 0.5 ? lastSection : cut.lastIndexOf('\n');
       const safe = boundary > 0 ? cut.slice(0, boundary) : cut;
       const safe = boundary > 0 ? cut.slice(0, boundary) : cut;
       return this.textResult(safe + '\n\n... (output truncated to budget; the source above is complete and verbatim — treat it as already Read. For any area not covered, run another codegraph_explore with the specific names — do NOT Read these files.)');
       return this.textResult(safe + '\n\n... (output truncated to budget; the source above is complete and verbatim — treat it as already Read. For any area not covered, run another codegraph_explore with the specific names — do NOT Read these files.)');
@@ -3411,7 +3428,7 @@ export class ToolHandler {
       const shownList = listed.slice(0, LIST_CAP);
       const shownList = listed.slice(0, LIST_CAP);
       out.push(
       out.push(
         '',
         '',
-        '### Other definitions',
+        '**Other definitions**',
         ...shownList.map((n) => `- \`${n.name}\` (${n.kind}) — ${n.filePath}:${n.startLine}`),
         ...shownList.map((n) => `- \`${n.name}\` (${n.kind}) — ${n.filePath}:${n.startLine}`),
       );
       );
       if (listed.length > LIST_CAP) out.push(`- … +${listed.length - LIST_CAP} more`);
       if (listed.length > LIST_CAP) out.push(`- … +${listed.length - LIST_CAP} more`);
@@ -3493,7 +3510,7 @@ export class ToolHandler {
     // symbolsOnly → the cheap structural overview, no source.
     // symbolsOnly → the cheap structural overview, no source.
     if (opts.symbolsOnly) {
     if (opts.symbolsOnly) {
       const out = [`**${filePath}** — ${nodes.length} symbol${nodes.length === 1 ? '' : 's'}, ${depSummary}`, ''];
       const out = [`**${filePath}** — ${nodes.length} symbol${nodes.length === 1 ? '' : 's'}, ${depSummary}`, ''];
-      if (nodes.length) out.push(...symbolMap('### Symbols'));
+      if (nodes.length) out.push(...symbolMap('**Symbols**'));
       else out.push('_No indexed symbols in this file._');
       else out.push('_No indexed symbols in this file._');
       out.push('', '> Drop `symbolsOnly` (or pass `offset`/`limit`) to read the source, like Read.');
       out.push('', '> Drop `symbolsOnly` (or pass `offset`/`limit`) to read the source, like Read.');
       return this.textResult(this.truncateOutput(out.join('\n')));
       return this.textResult(this.truncateOutput(out.join('\n')));
@@ -3503,7 +3520,7 @@ export class ToolHandler {
     // line is `key: <secret>`. Summarize by key and point to a real Read.
     // line is `key: <secret>`. Summarize by key and point to a real Read.
     if (CONFIG_LEAF_LANGUAGES.has(resolved.language)) {
     if (CONFIG_LEAF_LANGUAGES.has(resolved.language)) {
       const out = [`**${filePath}** — configuration/data file, ${depSummary}`, ''];
       const out = [`**${filePath}** — configuration/data file, ${depSummary}`, ''];
-      if (nodes.length) out.push(...symbolMap('### Keys (values withheld for safety)'));
+      if (nodes.length) out.push(...symbolMap('**Keys (values withheld for safety)**'));
       out.push('', '> Values may be secrets, so codegraph indexes keys only. Read the file directly if you need a value.');
       out.push('', '> Values may be secrets, so codegraph indexes keys only. Read the file directly if you need a value.');
       return this.textResult(this.truncateOutput(out.join('\n')));
       return this.textResult(this.truncateOutput(out.join('\n')));
     }
     }
@@ -3517,7 +3534,7 @@ export class ToolHandler {
     }
     }
     if (content === null) {
     if (content === null) {
       const out = [`**${filePath}** — could not read from disk (it may have moved since indexing). ${depSummary}`, ''];
       const out = [`**${filePath}** — could not read from disk (it may have moved since indexing). ${depSummary}`, ''];
-      if (nodes.length) out.push(...symbolMap('### Symbols'));
+      if (nodes.length) out.push(...symbolMap('**Symbols**'));
       out.push('', `> Read \`${filePath}\` directly for its current content.`);
       out.push('', `> Read \`${filePath}\` directly for its current content.`);
       return this.textResult(this.truncateOutput(out.join('\n')));
       return this.textResult(this.truncateOutput(out.join('\n')));
     }
     }
@@ -3614,7 +3631,7 @@ export class ToolHandler {
     const callees = collect(cg.getCallees(node.id));
     const callees = collect(cg.getCallees(node.id));
     const callers = collect(cg.getCallers(node.id));
     const callers = collect(cg.getCallers(node.id));
     if (callees.length === 0 && callers.length === 0) return '';
     if (callees.length === 0 && callers.length === 0) return '';
-    const lines: string[] = ['', '### Trail — codegraph_node any of these to follow it (no Read needed)'];
+    const lines: string[] = ['', '**Trail — codegraph_node any of these to follow it (no Read needed)**'];
     if (callees.length > 0) {
     if (callees.length > 0) {
       lines.push(`**Calls →** ${callees.slice(0, TRAIL_CAP).map(fmt).join(', ')}${callees.length > TRAIL_CAP ? `, +${callees.length - TRAIL_CAP} more` : ''}`);
       lines.push(`**Calls →** ${callees.slice(0, TRAIL_CAP).map(fmt).join(', ')}${callees.length > TRAIL_CAP ? `, +${callees.length - TRAIL_CAP} more` : ''}`);
     }
     }
@@ -3650,7 +3667,7 @@ export class ToolHandler {
     const mismatch = this.worktreeMismatchFor(args.projectPath as string | undefined);
     const mismatch = this.worktreeMismatchFor(args.projectPath as string | undefined);
 
 
     const lines: string[] = [
     const lines: string[] = [
-      '## CodeGraph Status',
+      '**CodeGraph Status**',
       '',
       '',
     ];
     ];
     if (mismatch) {
     if (mismatch) {
@@ -3681,7 +3698,7 @@ export class ToolHandler {
       );
       );
     }
     }
 
 
-    lines.push('', '### Nodes by Kind:');
+    lines.push('', '**Nodes by Kind:**');
 
 
     for (const [kind, count] of Object.entries(stats.nodesByKind)) {
     for (const [kind, count] of Object.entries(stats.nodesByKind)) {
       if ((count as number) > 0) {
       if ((count as number) > 0) {
@@ -3689,7 +3706,7 @@ export class ToolHandler {
       }
       }
     }
     }
 
 
-    lines.push('', '### Languages:');
+    lines.push('', '**Languages:**');
     for (const [lang, count] of Object.entries(stats.filesByLanguage)) {
     for (const [lang, count] of Object.entries(stats.filesByLanguage)) {
       if ((count as number) > 0) {
       if ((count as number) > 0) {
         lines.push(`- ${lang}: ${count}`);
         lines.push(`- ${lang}: ${count}`);
@@ -3703,7 +3720,7 @@ export class ToolHandler {
     if (cg.isWatcherDegraded()) {
     if (cg.isWatcherDegraded()) {
       lines.push(
       lines.push(
         '',
         '',
-        '### Auto-sync disabled:',
+        '**Auto-sync disabled:**',
         `- ${cg.getWatcherDegradedReason() ?? 'live file watching stopped'}`,
         `- ${cg.getWatcherDegradedReason() ?? 'live file watching stopped'}`,
         '- The index is frozen; Read files directly for current content.'
         '- The index is frozen; Read files directly for current content.'
       );
       );
@@ -3715,7 +3732,7 @@ export class ToolHandler {
     // banners on other tool calls.
     // banners on other tool calls.
     const pending = cg.getPendingFiles();
     const pending = cg.getPendingFiles();
     if (pending.length > 0) {
     if (pending.length > 0) {
-      lines.push('', '### Pending sync:');
+      lines.push('', '**Pending sync:**');
       const now = Date.now();
       const now = Date.now();
       for (const p of pending) {
       for (const p of pending) {
         const ageMs = Math.max(0, now - p.lastSeenMs);
         const ageMs = Math.max(0, now - p.lastSeenMs);
@@ -3806,7 +3823,7 @@ export class ToolHandler {
    * Format files as a flat list
    * Format files as a flat list
    */
    */
   private formatFilesFlat(files: { path: string; language: string; nodeCount: number }[], includeMetadata: boolean): string {
   private formatFilesFlat(files: { path: string; language: string; nodeCount: number }[], includeMetadata: boolean): string {
-    const lines: string[] = [`## Files (${files.length})`, ''];
+    const lines: string[] = [`**Files (${files.length})**`, ''];
 
 
     for (const file of files.sort((a, b) => a.path.localeCompare(b.path))) {
     for (const file of files.sort((a, b) => a.path.localeCompare(b.path))) {
       if (includeMetadata) {
       if (includeMetadata) {
@@ -3831,13 +3848,13 @@ export class ToolHandler {
       byLang.set(file.language, existing);
       byLang.set(file.language, existing);
     }
     }
 
 
-    const lines: string[] = [`## Files by Language (${files.length} total)`, ''];
+    const lines: string[] = [`**Files by Language (${files.length} total)**`, ''];
 
 
     // Sort languages by file count (descending)
     // Sort languages by file count (descending)
     const sortedLangs = [...byLang.entries()].sort((a, b) => b[1].length - a[1].length);
     const sortedLangs = [...byLang.entries()].sort((a, b) => b[1].length - a[1].length);
 
 
     for (const [lang, langFiles] of sortedLangs) {
     for (const [lang, langFiles] of sortedLangs) {
-      lines.push(`### ${lang} (${langFiles.length})`);
+      lines.push(`**${lang} (${langFiles.length})**`);
       for (const file of langFiles.sort((a, b) => a.path.localeCompare(b.path))) {
       for (const file of langFiles.sort((a, b) => a.path.localeCompare(b.path))) {
         if (includeMetadata) {
         if (includeMetadata) {
           lines.push(`- ${file.path} (${file.nodeCount} symbols)`);
           lines.push(`- ${file.path} (${file.nodeCount} symbols)`);
@@ -3889,7 +3906,7 @@ export class ToolHandler {
     }
     }
 
 
     // Render tree
     // Render tree
-    const lines: string[] = [`## Project Structure (${files.length} files)`, ''];
+    const lines: string[] = [`**Project Structure (${files.length} files)**`, ''];
 
 
     const renderNode = (node: TreeNode, prefix: string, isLast: boolean, depth: number): void => {
     const renderNode = (node: TreeNode, prefix: string, isLast: boolean, depth: number): void => {
       if (maxDepth !== undefined && depth > maxDepth) return;
       if (maxDepth !== undefined && depth > maxDepth) return;
@@ -4102,13 +4119,13 @@ export class ToolHandler {
   // =========================================================================
   // =========================================================================
 
 
   private formatSearchResults(results: SearchResult[]): string {
   private formatSearchResults(results: SearchResult[]): string {
-    const lines: string[] = [`## Search Results (${results.length} found)`, ''];
+    const lines: string[] = [`**Search Results (${results.length} found)**`, ''];
 
 
     for (const result of results) {
     for (const result of results) {
       const { node } = result;
       const { node } = result;
       const location = node.startLine ? `:${node.startLine}` : '';
       const location = node.startLine ? `:${node.startLine}` : '';
       // Compact format: one line per result with key info
       // Compact format: one line per result with key info
-      lines.push(`### ${node.name} (${node.kind})`);
+      lines.push(`**${node.name}** (${node.kind})`);
       lines.push(`${node.filePath}${location}`);
       lines.push(`${node.filePath}${location}`);
       if (node.signature) lines.push(`\`${node.signature}\``);
       if (node.signature) lines.push(`\`${node.signature}\``);
       lines.push('');
       lines.push('');
@@ -4118,7 +4135,7 @@ export class ToolHandler {
   }
   }
 
 
   private formatNodeList(nodes: Node[], title: string, labels?: Map<string, string>): string {
   private formatNodeList(nodes: Node[], title: string, labels?: Map<string, string>): string {
-    const lines: string[] = [`## ${title} (${nodes.length} found)`, ''];
+    const lines: string[] = [`**${title} (${nodes.length} found)**`, ''];
 
 
     for (const node of nodes) {
     for (const node of nodes) {
       const location = node.startLine ? `:${node.startLine}` : '';
       const location = node.startLine ? `:${node.startLine}` : '';
@@ -4153,7 +4170,7 @@ export class ToolHandler {
 
 
     // Compact format: just list affected symbols grouped by file
     // Compact format: just list affected symbols grouped by file
     const lines: string[] = [
     const lines: string[] = [
-      `## Impact: "${symbol}" affects ${nodeCount} symbols`,
+      `**Impact: "${symbol}" affects ${nodeCount} symbols**`,
       '',
       '',
     ];
     ];
 
 
@@ -4201,7 +4218,7 @@ export class ToolHandler {
   private formatNodeDetails(node: Node, code: string | null, outline?: string | null): string {
   private formatNodeDetails(node: Node, code: string | null, outline?: string | null): string {
     const location = node.startLine ? `:${node.startLine}` : '';
     const location = node.startLine ? `:${node.startLine}` : '';
     const lines: string[] = [
     const lines: string[] = [
-      `## ${node.name} (${node.kind})`,
+      `**${node.name}** (${node.kind})`,
       '',
       '',
       `**Location:** ${node.filePath}${location}`,
       `**Location:** ${node.filePath}${location}`,
     ];
     ];

+ 9 - 7
src/reasoning/reasoner.ts

@@ -124,8 +124,8 @@ CORRECTNESS OVERRIDES EVERYTHING. Being incomplete is fine; being WRONG is not 
 const SYSTEM_PROMPT_REPORT = `${ROLE}
 const SYSTEM_PROMPT_REPORT = `${ROLE}
 
 
 Produce a single self-contained exploration report, formatted exactly like the summary a thorough senior engineer hands back after investigating. Clean Markdown, in this shape:
 Produce a single self-contained exploration report, formatted exactly like the summary a thorough senior engineer hands back after investigating. Clean Markdown, in this shape:
-- Open with the one-line coverage verdict (above). Then, ONLY if covered, a title: "## <Topic> — <Flow / Trace / Overview>". If coverage is not-found, the verdict + the names to explore next is the entire reply. NO preamble ("Here is", "Now I understand").
-- Body is numbered sections with bold headers: "### 1. **<step or aspect>**", "### 2. **<...>**", …
+- Open with the one-line coverage verdict (above). Then, ONLY if covered, a bold title: "**<Topic> — <Flow / Trace / Overview>**". If coverage is not-found, the verdict + the names to explore next is the entire reply. NO preamble ("Here is", "Now I understand"). Use bold labels for headers, never Markdown ATX headings (\`#\`/\`##\`) — they render oversized in some clients.
+- Body is numbered sections with bold headers: "**1. <step or aspect>**", "**2. <...>**", …
 - Cite every location inline and in bold as **\`path/to/file.ts:line\`** (or a line range), exactly as given in the source. Bold key classes, methods, and symbols.
 - Cite every location inline and in bold as **\`path/to/file.ts:line\`** (or a line range), exactly as given in the source. Bold key classes, methods, and symbols.
 - For a flow/path question, include a call-chain diagram in a fenced code block using down-arrows:
 - For a flow/path question, include a call-chain diagram in a fenced code block using down-arrows:
   \`\`\`
   \`\`\`
@@ -135,7 +135,7 @@ Produce a single self-contained exploration report, formatted exactly like the s
   \`\`\`
   \`\`\`
 - Quote only the code lines that carry the logic, in fenced code blocks, keeping their line numbers. Keep snippets tight.
 - Quote only the code lines that carry the logic, in fenced code blocks, keeping their line numbers. Keep snippets tight.
 - Separate major sections with a "---" rule.
 - Separate major sections with a "---" rule.
-- End with "### Summary" — the end-to-end chain in one compact block.
+- End with "**Summary**" — the end-to-end chain in one compact block.
 
 
 Be precise and dense — an engineer should be able to act from this report without opening a file.`;
 Be precise and dense — an engineer should be able to act from this report without opening a file.`;
 
 
@@ -172,11 +172,13 @@ export function stripAgentDirectives(context: string): string {
   let i = 0;
   let i = 0;
   while (i < lines.length) {
   while (i < lines.length) {
     const ln = lines[i] ?? '';
     const ln = lines[i] ?? '';
-    if (/^##\s+Exploration:/.test(ln) || /^Found \d+ symbols? across \d+ files?/.test(ln)) { i++; continue; }
-    // "Not shown above" pointer section: drop header + its bullets/blanks until the next rule/heading/blockquote.
-    if (/^###\s+Not shown above/i.test(ln)) {
+    // Headers are bold labels, not ATX headings (tools.ts, issue #778): the
+    // explore header is `**Exploration: …**`, file sections start with ``**` ``.
+    if (/^\*\*Exploration:/.test(ln) || /^Found \d+ symbols? across \d+ files?/.test(ln)) { i++; continue; }
+    // "Not shown above" pointer section: drop header + its bullets/blanks until the next rule/header/blockquote.
+    if (/^\*\*Not shown above/i.test(ln)) {
       i++;
       i++;
-      while (i < lines.length && !/^(---|#{2,4}\s|>\s)/.test(lines[i] ?? '')) i++;
+      while (i < lines.length && !/^(---|\*\*|>\s)/.test(lines[i] ?? '')) i++;
       continue;
       continue;
     }
     }
     // Agent-directed blockquote notes (completeness / budget / trimmed).
     // Agent-directed blockquote notes (completeness / budget / trimmed).