diff --git a/CHANGELOG.md b/CHANGELOG.md index a85a4d0b8c..6210576619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - A Swift reference to a type now links to the type's own declaration, not to a file that extends it. An `extension View { … }` or `extension Text { … }` used to stand in for SwiftUI's type, so every view, every `Text("…")` and every `Color.red` in an app linked to whichever file happened to extend it. Those files topped the most-depended-on lists and their impact reached the whole app. A bare name like `@State`, `@Test` or `Result<…>` no longer links to some other type's nested `State` or `Result`, and a qualified name like `Build.Id` links to the `Id` it names. Methods declared in an extension of an SDK type still resolve on the types that conform to it, and a protocol's methods declared in any of its extensions now connect to each conforming type's own implementation. Re-index Swift projects after upgrading. - A Vapor route now links to the handler it names. `use: SearchController.show` used to link to whichever controller's `show` came first, so routes with a common handler name, like `show`, `index` or `get`, pointed at another endpoint's code in callers, impact and `codegraph_explore` answers. Nested types like `API.PackageController.get` and handlers declared in an extension of the controller now resolve too, and `use: self.index` resolves to the collection's own `index`. Re-index Vapor projects after upgrading. - A PHP call written without a receiver, such as `redirect($url)`, `view('books.show')`, `auth()` or `basename($path)`, is a function call, and no longer links to a same-named method, field or class elsewhere in the project. These wrong links showed up in callers, impact and `codegraph_explore` answers wherever a Laravel helper or PHP built-in shared its name with a project member. Re-index PHP projects after upgrading. +- The Claude Code prompt hook no longer runs on the messages Claude Code uses to hand a subagent's report back to the main session. Before, such a long report could keep the hook busy past Claude Code's 30-second hook timeout and inject context unrelated to what you asked. (#2184) ## [1.6.1] - 2026-09-29 diff --git a/__tests__/frontload-hook.test.ts b/__tests__/frontload-hook.test.ts index 3e81e6090e..c85700b9c0 100644 --- a/__tests__/frontload-hook.test.ts +++ b/__tests__/frontload-hook.test.ts @@ -13,7 +13,7 @@ import * as os from 'os'; import * as path from 'path'; import { spawnSync } from 'node:child_process'; import { CodeGraph } from '../src'; -import { planFrontload, isTaskNotification, findIndexedSubprojectRoots, unsafeIndexRootReason, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens, PROMPT_HOOK_INJECTION_MAX, CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT, capPromptHookInjection } from '../src/directory'; +import { planFrontload, isTaskNotification, isAgentMessage, findIndexedSubprojectRoots, unsafeIndexRootReason, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens, PROMPT_HOOK_INJECTION_MAX, CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT, capPromptHookInjection } from '../src/directory'; // Make the built-in exports configurable so HOME can point at a real temp // fixture without changing the process environment or the user's home files. @@ -374,6 +374,12 @@ export class OrderStateMachine { } }); + it('stays silent on a subagent hand-back envelope, even one that names indexed symbols (#2184)', () => { + const report = 'how does OrderStateMachine work? submitOrder() calls into the state machine.'; + expect(hook(report)).toContain('Structural context from CodeGraph'); + expect(hook(`\n[Subagent hand-back] ${report}\n`)).toBe(''); + }); + it('uses MEDIUM for indexed prose segments without a strong keyword or verified token', () => { for (const prompt of ['como state machine?', 'wie state machine?']) { const output = hook(prompt); @@ -471,3 +477,20 @@ describe('system task notifications (#1832)', () => { expect(isTaskNotification('trace AuthService login')).toBe(false); }); }); + +describe('subagent hand-backs (#2184)', () => { + const handBack = '\n[Subagent hand-back] Traced how AuthService.login calls TokenStore.save and which callers are affected.\n'; + + it('skips the complete hand-back envelope', () => { + expect(isAgentMessage(handBack)).toBe(true); + expect(isAgentMessage(` \n${handBack}\n`)).toBe(true); + expect(isAgentMessage('trace AuthService login flow')).toBe(true); + }); + it('does not suppress a user question that mentions the marker', () => { + expect(isAgentMessage(`Why does ${handBack} trigger the hook?`)).toBe(false); + expect(isAgentMessage(`${handBack} Explain this.`)).toBe(false); + expect(isAgentMessage('trace AuthService')).toBe(false); + expect(isAgentMessage('trace AuthService')).toBe(false); + expect(isAgentMessage('trace AuthService login')).toBe(false); + }); +}); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 38b4e55b56..64078dfab6 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -53,7 +53,7 @@ try { import { Command } from 'commander'; import * as path from 'path'; import * as fs from 'fs'; -import { getCodeGraphDir, isInitialized, hasSchemalessDb, hasForeignDbFile, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, isTaskNotification, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection, codeGraphDirName, DEFAULT_CODEGRAPH_DIR } from '../directory'; +import { getCodeGraphDir, isInitialized, hasSchemalessDb, hasForeignDbFile, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, isTaskNotification, isAgentMessage, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection, codeGraphDirName, DEFAULT_CODEGRAPH_DIR } from '../directory'; import { extractProseCandidates } from '../search/identifier-segments'; import { detectWorktreeIndexMismatch, worktreeMismatchWarning } from '../sync/worktree'; import { createShimmerProgress } from '../ui/shimmer-progress'; @@ -1473,9 +1473,10 @@ program let input: { prompt?: string; cwd?: string } = {}; try { input = JSON.parse(raw); } catch { return; } const prompt = String(input.prompt || ''); - // System-injected task notifications are not user prompts: exit before - // any project lookup or explore work (#1832). - if (isTaskNotification(prompt)) return; + // System-injected task notifications and subagent hand-backs are not + // user prompts: exit before any project lookup or explore work (#1832, + // #2184). + if (isTaskNotification(prompt) || isAgentMessage(prompt)) return; // Gate telemetry: how often each tier fires vs. no-ops — counter names // only, NEVER prompt content (see TELEMETRY.md). This is the data that diff --git a/src/directory.ts b/src/directory.ts index 0a08df9521..d99860f04e 100644 --- a/src/directory.ts +++ b/src/directory.ts @@ -1079,3 +1079,13 @@ export function validateDirectory(projectRoot: string): { export function isTaskNotification(prompt: string): boolean { return /^\s*[\s\S]*<\/task-notification>\s*$/.test(prompt); } + +/** + * Claude Code hands a subagent's report back to the parent session as a + * `…` prompt, which UserPromptSubmit + * hooks also receive (#2184). Same rule as {@link isTaskNotification}: only a + * prompt that is entirely that envelope is skipped. + */ +export function isAgentMessage(prompt: string): boolean { + return /^\s*]*)?>[\s\S]*<\/agent-message>\s*$/.test(prompt); +}