From e2c2b6e07f8e976e2de56aab2aa499f1bfd657ec Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Fri, 28 Aug 2026 01:41:00 +0100 Subject: [PATCH 1/5] Support directory-scoped AGENTS.md instructions --- docs/ui-taste.md | 6 + docs/user/README.md | 1 + docs/user/project-instructions.md | 42 ++ src/main/ipc/register-handlers.ts | 9 +- src/main/services/agent-service.test.ts | 108 ++++ src/main/services/agent-service.ts | 68 ++- src/main/services/agent-system-prompt.test.ts | 17 + src/main/services/agent-system-prompt.ts | 40 +- src/main/services/context-estimate.ts | 1 + .../services/project-instructions.test.ts | 190 +++++- src/main/services/project-instructions.ts | 572 +++++++++++++++--- src/renderer/views/settings-dialog.ts | 44 +- .../settings-sources-instructions.test.ts | 36 +- src/shared/types/instructions.ts | 6 +- .../settings-sources-nested-instructions.png | Bin 0 -> 131859 bytes ...ettings-sources-nested-instructions.e2e.ts | 107 ++++ 16 files changed, 1130 insertions(+), 117 deletions(-) create mode 100644 docs/user/project-instructions.md create mode 100644 tests/e2e/screenshots/settings-sources-nested-instructions.png create mode 100644 tests/e2e/settings-sources-nested-instructions.e2e.ts diff --git a/docs/ui-taste.md b/docs/ui-taste.md index 512af633e6..cd3514584b 100644 --- a/docs/ui-taste.md +++ b/docs/ui-taste.md @@ -744,6 +744,12 @@ left-elision (`direction: rtl` + `text-overflow: ellipsis`, same trick as `.git- the leaf stays visible; mirror the full path on the row's `title` for the native tooltip. Spec: [`tests/e2e/settings-sources-skills.e2e.ts`](../tests/e2e/settings-sources-skills.e2e.ts). +Nested instruction rows use the badge as activation state, not origin: **active** uses the existing +accent outline when the latest turn selected that scope; **scoped** uses the quiet default outline when +the file is available but unrelated to that turn. Keep the governed directory and the explanatory +state in the detail line so sibling scopes are understandable without adding another row of chips. +Spec: [`tests/e2e/settings-sources-nested-instructions.e2e.ts`](../tests/e2e/settings-sources-nested-instructions.e2e.ts). + ## Prove visual changes with a focused e2e eval Per `AGENTS.md`, any user-visible change needs a focused WebdriverIO Electron spec that seeds the diff --git a/docs/user/README.md b/docs/user/README.md index b2d0c6f549..188c26f5e1 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -13,6 +13,7 @@ contributor and design archive — start here instead. - [Install](install.md) — macOS 26+ from a signed DMG, or run from source - [Quickstart](quickstart.md) — open a project, pick a model, send the first prompt - [Connect a model](connect-a-model.md) — API key, environment scan, or a local server +- [Project instructions](project-instructions.md) — root and directory-scoped AGENTS.md files ## Staying in control diff --git a/docs/user/project-instructions.md b/docs/user/project-instructions.md new file mode 100644 index 0000000000..4dbf03e672 --- /dev/null +++ b/docs/user/project-instructions.md @@ -0,0 +1,42 @@ +--- +title: Project instructions +description: Give Copse global, project-wide, or directory-scoped guidance with AGENTS.md files. +--- + +# Project instructions + +Instruction files let a repository carry its own build commands, conventions, and safety notes. +Copse lists every discovered source under **Settings → Customise → Instruction files**. Project +instructions stay inert until you trust the project; click a file name there to read it first. + +## Project-wide instructions + +At the project root, Copse reads these files in order: + +1. `AGENT.md` +2. `AGENTS.md` +3. `CLAUDE.md` + +Identical contents are injected once. Global `~/AGENTS.md` and `~/.claude/CLAUDE.md` load beneath +the project layer as user-owned guidance. + +## Directory-scoped instructions + +A nested `AGENTS.md` applies only when a path under its directory enters the turn's context. That +can happen because the prompt or an attachment names a file, or because the agent reads, searches, +creates, moves, or edits a path during the turn. Instructions are applied from the project root +toward the target directory, so the nearest file appears last and can refine broader conventions. +Sibling scopes stay inactive unless that sibling has a relevant path. + +When an edit tool is the first action to enter a new scope, Copse loads the applicable instruction +chain and defers that edit once. The agent sees the new rules and retries instead of changing the +file before its local guidance is available. + +Nested `AGENT.md` and `CLAUDE.md` remain root-only compatibility formats. Only `AGENTS.md` follows +the cross-client directory-scoping convention, which avoids silently changing the meaning of +vendor-specific files. + +Sources marks a nested file **active** when the latest turn used it and **scoped** when it is +available but did not apply. Discovery skips dependency, generated, vendored, cache, nested-repo, +and VCS trees; it does not follow a symlink outside the trusted workspace. Very deep or unusually +large instruction trees are bounded so they cannot consume the whole prompt. diff --git a/src/main/ipc/register-handlers.ts b/src/main/ipc/register-handlers.ts index 2c708e84d9..6d7a2a2a23 100644 --- a/src/main/ipc/register-handlers.ts +++ b/src/main/ipc/register-handlers.ts @@ -2142,12 +2142,19 @@ export function registerAllHandlers(win: BrowserWindow, registry: ToolRegistry): return listUnsandboxedProjectHooks(root) }) ipcMain.handle('instructions:list', async () => - (await loadProjectInstructionSources()).map(({ path, name, scope, content, active }) => ({ + ( + await loadProjectInstructionSources({ + useLatestNestedActivation: true, + refreshNestedDiscovery: true, + }) + ).map(({ path, name, scope, content, active, trusted, scopePath }) => ({ path, name, scope, bytes: Buffer.byteLength(content, 'utf-8'), active, + trusted, + ...(scopePath !== undefined ? { scopePath } : {}), })), ) /** diff --git a/src/main/services/agent-service.test.ts b/src/main/services/agent-service.test.ts index abdbea1f61..6457a25232 100644 --- a/src/main/services/agent-service.test.ts +++ b/src/main/services/agent-service.test.ts @@ -1,5 +1,9 @@ import { describe, it } from 'node:test' import assert from 'node:assert/strict' +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { z } from 'zod' import * as agentService from './agent-service.ts' import * as providerSelection from './providers/provider-selection.ts' import { suggestThreadTitle } from './title-generator.ts' @@ -17,6 +21,8 @@ import { type PluginToolRuntimeController, } from './plugins/plugin-tool-controller.ts' import { pluginModelValue } from '@shared/plugin-model.ts' +import { defineTool } from '@shared/types' +import { runWithWorkspaceTrust } from './security/workspace-trust.ts' // agent-service is now an orchestrator that re-exports the public surface from the // focused modules it composes. These tests pin that public surface so IPC callers @@ -233,6 +239,108 @@ describe('runAgent AgentHost decoupling', () => { assert.deepEqual(checkpoints.at(-1), result.messages.slice(0, checkpoints.at(-1)?.length)) }) + it('activates nested instructions on first file access and defers the first edit', async () => { + const root = await mkdtemp(join(tmpdir(), 'copse-agent-nested-instructions-')) + await mkdir(join(root, 'packages', 'api'), { recursive: true }) + await writeFile(join(root, 'packages', 'api', 'AGENTS.md'), 'Never edit before reading this.') + const writes: string[] = [] + const registry = new ToolRegistry() + registry.register( + defineTool({ + name: 'write_file', + description: 'Test edit tool', + parameters: z.object({ path: z.string() }), + execute: ({ path }) => { + writes.push(path) + return Promise.resolve('File written.') + }, + }), + ) + + let calls = 0 + const provider: LLMProvider = { + stream: async function* (messages) { + calls += 1 + const system = messages.find((message) => message.role === 'system') + assert.ok(system?.role === 'system') + if (calls === 1) { + assert.doesNotMatch(system.content, /Never edit before reading this/) + yield { + type: 'tool_call' as const, + toolCall: { + id: 'first-edit', + name: 'write_file', + args: { path: 'packages/api/router.ts' }, + }, + } + return + } + assert.match(system.content, /Never edit before reading this/) + if (calls === 2) { + const deferred = messages.find( + (message) => + message.role === 'tool' && + message.toolResults.some((result) => result.toolCallId === 'first-edit'), + ) + assert.ok(deferred?.role === 'tool') + assert.match(deferred.toolResults[0]?.result ?? '', /Edit deferred/) + assert.deepEqual(writes, []) + yield { + type: 'tool_call' as const, + toolCall: { + id: 'retried-edit', + name: 'write_file', + args: { path: 'packages/api/router.ts' }, + }, + } + return + } + assert.deepEqual(writes, ['packages/api/router.ts']) + yield { type: 'text' as const, text: 'Done.' } + }, + } + setDefaultPluginRegistry(new PluginRegistry()) + await setSetting('subagentsEnabled', false) + await setSetting('skillsEnabled', false) + + try { + await runWithWorkspaceTrust(root, true, () => + runWithThreadExecutionContext( + { + projectId: 'project-nested', + threadId: 'thread-nested', + projectRoot: root, + root, + checkoutMode: 'shared', + branch: null, + }, + () => + runWithActiveRunIdentity('thread-nested', () => + agentService.runAgent( + 'thread-nested', + 'Make the requested change.', + [], + { emit: () => undefined }, + registry, + { + provider, + contextWindow: 100_000, + model: 'claude-sonnet-4-6', + maxSteps: 6, + maxLlmCalls: 6, + }, + ), + ), + ), + ) + assert.equal(calls, 3) + assert.deepEqual(writes, ['packages/api/router.ts']) + } finally { + setDefaultPluginRegistry(null) + await rm(root, { recursive: true, force: true }) + } + }) + it('emits a structured terminal record with raw provider failure details', async () => { const received: StreamChunk[] = [] const host: AgentHost = { diff --git a/src/main/services/agent-service.ts b/src/main/services/agent-service.ts index 15b5bafaef..fd498a3602 100644 --- a/src/main/services/agent-service.ts +++ b/src/main/services/agent-service.ts @@ -43,7 +43,8 @@ import { } from './agent-errors.ts' import { normalizeStopReason } from '@copse/agent/headless-contract.ts' import { resolveParentGoal } from '@copse/agent/working-brief.ts' -import { buildSystemPrompt } from './agent-system-prompt.ts' +import { buildSystemPromptWithMetadata } from './agent-system-prompt.ts' +import { activateNestedInstructionSources } from './project-instructions.ts' import { hasLastUsage } from '@copse/llm/provider-usage.ts' import { clearActiveRunThread, @@ -393,6 +394,29 @@ function promptTextForSubmit(userPrompt: UserContent): string { .join('\n') } +/** Built-in file tools whose path establishes directory-scoped instruction context. */ +const INSTRUCTION_CONTEXT_PATH_FIELDS: Readonly> = { + read_file: ['path'], + list_dir: ['path'], + search_code: ['path'], + search_codebase: ['path'], + read_staged_diff: ['path'], + write_file: ['path'], + str_replace: ['path'], + delete_file: ['path'], + rename_file: ['from', 'to'], + make_directory: ['path'], +} + +function instructionContextPathsForTool(name: string, args: unknown): string[] { + const fields = INSTRUCTION_CONTEXT_PATH_FIELDS[name] + if (!fields || !isRecord(args)) return [] + return fields.flatMap((field) => { + const value = args[field] + return typeof value === 'string' && value.trim() ? [value] : [] + }) +} + /** * Fire `beforeSubmitPrompt` (B1). Returns the user-facing notice to show when a * hook halted the submit (`continue: false`), or null to proceed. Recording is @@ -1513,13 +1537,22 @@ export async function runAgent( ? options.invokedAgent : undefined - const systemPrompt = await buildSystemPrompt({ + const systemPromptBuild = await buildSystemPromptWithMetadata({ subagentsEnabled, invokedSkills, threadId, userPrompt: outboundPrompt, model, + trackInstructionActivation: true, }) + const systemPrompt = systemPromptBuild.prompt + const activeNestedInstructionPaths = new Set( + systemPromptBuild.instructionMetadata.activeNestedPaths, + ) + const activeInstructionContents = new Set( + systemPromptBuild.instructionMetadata.activeInstructionContents, + ) + let activeNestedInstructionBytes = systemPromptBuild.instructionMetadata.activeNestedBytes const messages: LLMMessage[] = [ { role: 'system', content: systemPrompt }, @@ -1828,6 +1861,37 @@ export async function runAgent( signal: AbortSignal, toolCallId: string, ): Promise => { + const instructionContextPaths = instructionContextPathsForTool(name, args) + if (instructionContextPaths.length > 0) { + const activation = await activateNestedInstructionSources( + instructionContextPaths, + activeNestedInstructionPaths, + activeInstructionContents, + activeNestedInstructionBytes, + ) + for (const path of activation.activatedPaths) { + activeNestedInstructionPaths.add(path) + } + for (const content of activation.injectedContents) { + activeInstructionContents.add(content) + activeNestedInstructionBytes += Buffer.byteLength(content, 'utf-8') + } + if (activation.block) { + const leadingSystem = trimmed.find((message) => message.role === 'system') + if (leadingSystem?.role === 'system') { + leadingSystem.content += `\n\n---\n\n${activation.block}` + } + // An edit must not land before the newly applicable rules have + // reached the model. Reads can proceed: their result and the new + // leading-system block arrive together before the next step. + if (isEditTool(name)) { + return ( + 'Edit deferred because this path activated nested AGENTS.md instructions. ' + + 'Review the newly loaded workspace instructions, then retry the same edit.' + ) + } + } + } if (isEditTool(name)) turnChangedFiles = true if (name === 'explore' && subagentsEnabled) { // ALS-scoped (not a global slot): the loop runs fanned-out diff --git a/src/main/services/agent-system-prompt.test.ts b/src/main/services/agent-system-prompt.test.ts index f171143c5a..ffb07baaf7 100644 --- a/src/main/services/agent-system-prompt.test.ts +++ b/src/main/services/agent-system-prompt.test.ts @@ -148,6 +148,23 @@ describe('buildSystemPrompt instruction layers', () => { assert.match(prompt, /not trusted/) assert.ok(prompt.includes('Global user rules')) }) + + it('activates a nested AGENTS.md from a path in the current user turn only', async () => { + await mkdir(join(tempRoot, 'packages', 'api'), { recursive: true }) + await mkdir(join(tempRoot, 'packages', 'web'), { recursive: true }) + await writeFile(join(tempRoot, 'packages', 'api', 'AGENTS.md'), 'Use API conventions.') + await writeFile(join(tempRoot, 'packages', 'web', 'AGENTS.md'), 'Use web conventions.') + const prompt = await runWithWorkspaceTrust(tempRoot, true, () => + buildSystemPrompt({ + subagentsEnabled: false, + invokedSkills: [], + userPrompt: 'Please update packages/api/src/router.ts.', + }), + ) + assert.match(prompt, /path="packages\/api\/AGENTS\.md"/) + assert.match(prompt, /Use API conventions/) + assert.doesNotMatch(prompt, /Use web conventions/) + }) }) describe('buildSystemPrompt working directory', () => { diff --git a/src/main/services/agent-system-prompt.ts b/src/main/services/agent-system-prompt.ts index 30c5c484a1..7430103b9b 100644 --- a/src/main/services/agent-system-prompt.ts +++ b/src/main/services/agent-system-prompt.ts @@ -1,4 +1,8 @@ -import { loadAgentRequestedRulesCatalog, loadInstructionLayers } from './project-instructions.ts' +import { + loadAgentRequestedRulesCatalog, + loadInstructionLayersWithMetadata, + type InstructionLayerMetadata, +} from './project-instructions.ts' import { getSetting, getSettingTrimmed } from './storage/settings.ts' import { getAgentExecutionRoot } from './execution-root.ts' import { getThreadExecutionContext } from './thread-execution-context.ts' @@ -75,8 +79,7 @@ async function buildRepositoryContext(): Promise { return `\nGit repository root: ${repositoryRoot}${subdirNote}` } -/** Assemble the system prompt for a run from base prompt + skills + instructions. */ -export async function buildSystemPrompt(opts: { +export interface BuildSystemPromptOptions { subagentsEnabled: boolean invokedSkills: string[] threadId?: string @@ -88,15 +91,31 @@ export async function buildSystemPrompt(opts: { * and the prompt stays model-agnostic. */ model?: string -}): Promise { + /** Persist nested-source activation for Settings; real local turns set this. */ + trackInstructionActivation?: boolean +} + +export interface SystemPromptBuildResult { + prompt: string + instructionMetadata: InstructionLayerMetadata +} + +/** Assemble the system prompt and retain nested-instruction runtime metadata. */ +export async function buildSystemPromptWithMetadata( + opts: BuildSystemPromptOptions, +): Promise { const { subagentsEnabled, invokedSkills, threadId } = opts const skillsToolsLine = buildSkillsToolsPromptLine() const userText = opts.userPrompt != null ? userContentToText(opts.userPrompt) : '' + const contextPaths = extractContextPathsFromText(userText) const cursorRuleContext: CursorRuleContext = { - contextPaths: extractContextPathsFromText(userText), + contextPaths, userText, } - const instructionLayers = await loadInstructionLayers({ cursorRuleContext }) + const instructionLayers = await loadInstructionLayersWithMetadata( + { cursorRuleContext, nestedContextPaths: contextPaths }, + opts.trackInstructionActivation ?? false, + ) const agentRulesCatalog = await loadAgentRequestedRulesCatalog() const basePrompt = subagentsEnabled ? BASE_SYSTEM_PROMPT : BASE_SYSTEM_PROMPT_DIRECT_READS @@ -118,7 +137,7 @@ export async function buildSystemPrompt(opts: { (threadId ? hasTerminalSessions(threadId) : hasTerminalSessions()) const customInstructions = getSettingTrimmed('customInstructions') const opus5 = opts.model != null && isOpus5Model(opts.model) - return ( + const prompt = basePrompt .replace('{SKILLS_TOOLS_LINE}', skillsToolsLine) // Must be the agent execution root, not the renderer workspace root: the @@ -147,5 +166,10 @@ export async function buildSystemPrompt(opts: { (instructionLayers.global ? `\n\n---\n\n## User instructions\n\n${instructionLayers.global}` : '') - ) + return { prompt, instructionMetadata: instructionLayers.metadata } +} + +/** Prompt-only compatibility wrapper for estimates, tests, and other callers. */ +export async function buildSystemPrompt(opts: BuildSystemPromptOptions): Promise { + return (await buildSystemPromptWithMetadata(opts)).prompt } diff --git a/src/main/services/context-estimate.ts b/src/main/services/context-estimate.ts index 2e4077b0c7..87edea5c65 100644 --- a/src/main/services/context-estimate.ts +++ b/src/main/services/context-estimate.ts @@ -57,6 +57,7 @@ export async function estimateContextBreakdown( subagentsEnabled, invokedSkills: input.invokedSkills, model, + userPrompt: input.draftText, }) // Skill blocks are part of the system prompt string; measure them separately so // they can be attributed to "Skills" instead of inflating "System prompt". diff --git a/src/main/services/project-instructions.test.ts b/src/main/services/project-instructions.test.ts index f0898c1d55..ab85692c46 100644 --- a/src/main/services/project-instructions.test.ts +++ b/src/main/services/project-instructions.test.ts @@ -1,15 +1,18 @@ import { describe, it, beforeEach, afterEach } from 'node:test' import assert from 'node:assert/strict' -import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises' +import { mkdtemp, mkdir, writeFile, rm, symlink } from 'node:fs/promises' import { join } from 'node:path' import { tmpdir } from 'node:os' import { + activateNestedInstructionSources, loadInstructionLayers, + loadInstructionLayersWithMetadata, loadProjectInstructionSources, loadAgentRequestedRulesCatalog, } from './project-instructions.ts' import { setWorkspaceRootForTest } from './workspace.ts' import { runWithWorkspaceTrust } from './security/workspace-trust.ts' +import { runWithThreadExecutionContext } from './thread-execution-context.ts' describe('project-instructions', () => { let projectRoot = '' @@ -56,6 +59,7 @@ describe('project-instructions', () => { scope: 'project', content: 'Use tabs.', active: true, + trusted: true, }, ]) const layers = await withTrust(true, () => loadInstructionLayers()) @@ -156,6 +160,190 @@ describe('project-instructions', () => { assert.match(layers.project, / { + await writeFile(join(projectRoot, 'AGENTS.md'), 'Root rules') + await mkdir(join(projectRoot, 'packages', 'api', 'src'), { recursive: true }) + await mkdir(join(projectRoot, 'packages', 'web', 'src'), { recursive: true }) + await writeFile(join(projectRoot, 'packages', 'AGENTS.md'), 'Package rules') + await writeFile(join(projectRoot, 'packages', 'api', 'AGENTS.md'), 'API rules') + await writeFile(join(projectRoot, 'packages', 'web', 'AGENTS.md'), 'Web rules') + + const sources = await withTrust(true, () => + loadProjectInstructionSources({ nestedContextPaths: ['packages/api/src/server.ts'] }), + ) + assert.deepEqual( + sources + .filter((source) => source.scopePath !== undefined) + .map((source) => ({ + name: source.name, + scopePath: source.scopePath, + active: source.active, + })), + [ + { name: 'packages/AGENTS.md', scopePath: 'packages', active: true }, + { name: 'packages/api/AGENTS.md', scopePath: 'packages/api', active: true }, + { name: 'packages/web/AGENTS.md', scopePath: 'packages/web', active: false }, + ], + ) + + const layers = await withTrust(true, () => + loadInstructionLayers({ nestedContextPaths: ['packages/api/src/server.ts'] }), + ) + assert.ok(layers.project.indexOf('Root rules') < layers.project.indexOf('Package rules')) + assert.ok(layers.project.indexOf('Package rules') < layers.project.indexOf('API rules')) + assert.doesNotMatch(layers.project, /Web rules/) + assert.match(layers.project, /path="packages\/api\/AGENTS\.md"/) + }) + + it('deduplicates and orders multiple sibling targets deterministically', async () => { + for (const name of ['api', 'web']) { + await mkdir(join(projectRoot, 'packages', name), { recursive: true }) + await writeFile(join(projectRoot, 'packages', name, 'AGENTS.md'), `${name} rules`) + } + const build = (paths: string[]): Promise => + withTrust( + true, + async () => (await loadInstructionLayers({ nestedContextPaths: paths })).project, + ) + const forward = await build(['packages/api/a.ts', 'packages/web/b.ts', 'packages/api/a.ts']) + const reverse = await build(['packages/web/b.ts', 'packages/api/a.ts']) + assert.equal(forward, reverse) + assert.ok(forward.indexOf('api rules') < forward.indexOf('web rules')) + }) + + it('keeps nested AGENT.md and CLAUDE.md root-only', async () => { + await mkdir(join(projectRoot, 'packages', 'api'), { recursive: true }) + await writeFile(join(projectRoot, 'packages', 'api', 'AGENT.md'), 'Nested singular') + await writeFile(join(projectRoot, 'packages', 'api', 'CLAUDE.md'), 'Nested Claude') + assert.deepEqual( + await withTrust(true, () => + loadProjectInstructionSources({ nestedContextPaths: ['packages/api/file.ts'] }), + ), + [], + ) + }) + + it('ignores generated trees and symlink escapes for discovery and activation', async () => { + const outside = await mkdtemp(join(tmpdir(), 'copse-panel-instructions-outside-')) + try { + await mkdir(join(projectRoot, 'packages', 'api'), { recursive: true }) + await mkdir(join(projectRoot, 'node_modules', 'dep'), { recursive: true }) + await mkdir(join(projectRoot, 'dist', 'generated'), { recursive: true }) + await writeFile(join(projectRoot, 'packages', 'api', 'AGENTS.md'), 'API rules') + await writeFile(join(projectRoot, 'node_modules', 'dep', 'AGENTS.md'), 'Dependency rules') + await writeFile(join(projectRoot, 'dist', 'generated', 'AGENTS.md'), 'Generated rules') + await writeFile(join(outside, 'AGENTS.md'), 'Outside rules') + await writeFile(join(outside, 'file.ts'), 'outside') + await symlink(join(outside, 'AGENTS.md'), join(projectRoot, 'packages', 'AGENTS.md')) + await symlink(outside, join(projectRoot, 'packages', 'api', 'outside')) + + const sources = await withTrust(true, () => + loadProjectInstructionSources({ + nestedContextPaths: ['packages/api/outside/file.ts'], + }), + ) + assert.deepEqual( + sources.map((source) => ({ name: source.name, active: source.active })), + [{ name: 'packages/api/AGENTS.md', active: false }], + ) + } finally { + await rm(outside, { recursive: true, force: true }) + } + }) + + it('bounds an unusually deep active chain while retaining the nearest rules', async () => { + const segments: string[] = [] + for (let depth = 1; depth <= 12; depth++) { + segments.push(`d${String(depth)}`) + const dir = join(projectRoot, ...segments) + await mkdir(dir, { recursive: true }) + await writeFile(join(dir, 'AGENTS.md'), `rules-${String(depth)}`) + } + const target = `${segments.join('/')}/file.ts` + const sources = await withTrust(true, () => + loadProjectInstructionSources({ nestedContextPaths: [target] }), + ) + const active = sources.filter((source) => source.scopePath !== undefined && source.active) + assert.equal(active.length, 8) + assert.ok(active.some((source) => source.content === 'rules-12')) + assert.ok(!active.some((source) => source.content === 'rules-1')) + }) + + it('keeps later scopes inactive after the prompt-wide byte budget is exhausted', async () => { + const activePaths = new Set() + const activeContents = new Set() + let activeBytes = 0 + + for (let index = 0; index < 7; index++) { + const scope = `scope-${String(index)}` + await mkdir(join(projectRoot, scope), { recursive: true }) + await writeFile(join(projectRoot, scope, 'AGENTS.md'), `${scope}: ${'x'.repeat(10 * 1024)}`) + } + + await withTrust(true, async () => { + for (let index = 0; index < 7; index++) { + const activation = await activateNestedInstructionSources( + [`scope-${String(index)}/file.ts`], + activePaths, + activeContents, + activeBytes, + ) + for (const path of activation.activatedPaths) activePaths.add(path) + for (const content of activation.injectedContents) { + activeContents.add(content) + activeBytes += Buffer.byteLength(content, 'utf-8') + } + if (index < 6) { + assert.equal(activation.injectedPaths.length, 1) + } else { + assert.deepEqual(activation.activatedPaths, []) + assert.equal(activation.block, '') + } + } + }) + }) + + it('reports worktree activation against the stable project root in Sources', async () => { + const worktreeRoot = await mkdtemp(join(tmpdir(), 'copse-panel-instructions-worktree-')) + const projectAlias = `${projectRoot}-alias` + try { + await symlink(projectRoot, projectAlias, 'dir') + for (const root of [projectRoot, worktreeRoot]) { + await mkdir(join(root, 'packages', 'api'), { recursive: true }) + await writeFile(join(root, 'packages', 'api', 'AGENTS.md'), 'API worktree rules') + } + + await withTrust(true, () => + runWithThreadExecutionContext( + { + projectId: 'project', + threadId: 'thread', + projectRoot: projectAlias, + root: worktreeRoot, + checkoutMode: 'worktree', + branch: 'codex/thread', + }, + () => + loadInstructionLayersWithMetadata( + { nestedContextPaths: ['packages/api/file.ts'] }, + true, + ), + ), + ) + + const sources = await withTrust(true, () => + loadProjectInstructionSources({ + useLatestNestedActivation: true, + refreshNestedDiscovery: true, + }), + ) + assert.equal(sources.find((source) => source.name === 'packages/api/AGENTS.md')?.active, true) + } finally { + await rm(projectAlias, { force: true }) + await rm(worktreeRoot, { recursive: true, force: true }) + } + }) + it('gates Cursor rules with the same trust gate', async () => { await mkdir(join(projectRoot, '.cursor', 'rules'), { recursive: true }) await writeFile( diff --git a/src/main/services/project-instructions.ts b/src/main/services/project-instructions.ts index 9dff3d37ba..5814c39079 100644 --- a/src/main/services/project-instructions.ts +++ b/src/main/services/project-instructions.ts @@ -1,6 +1,6 @@ import * as fsp from 'node:fs/promises' import { homedir } from 'node:os' -import { join } from 'node:path' +import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path' import type { InstructionScope } from '@shared/types/instructions.ts' import { buildAgentRequestedRulesCatalog, @@ -8,68 +8,325 @@ import { loadCursorRuleSources, type CursorRuleContext, } from './skills/cursor-rules.ts' -import { getWorkspaceRoot } from './workspace.ts' +import { getAgentExecutionRoot, getAgentProjectRoot } from './execution-root.ts' import { isWorkspaceTrusted } from './security/workspace-trust.ts' /** * Project-root instruction files, in precedence order. * - * `AGENT.md` / `AGENTS.md` are the cross-tool convention; `CLAUDE.md` is Claude Code's. - * We load whichever are present so Copse behaves the same regardless of which assistant - * seeded the repo. Identical content (repos often symlink `AGENTS.md` → `CLAUDE.md`) is - * loaded once. + * Identical content is loaded once. Only `AGENTS.md` receives nested, + * directory-scoped semantics: `AGENT.md` and `CLAUDE.md` remain root-only + * compatibility formats. */ export const PROJECT_INSTRUCTION_FILES = ['AGENT.md', 'AGENTS.md', 'CLAUDE.md'] as const -/** - * User-global instruction files, relative to the home directory, in precedence order. - * These form a lower-precedence layer beneath the project files — always-on personal - * steering that applies across every workspace, mirroring how most assistants layer a - * global file under the per-repo one. - */ +/** User-global instruction files, relative to the home directory, in precedence order. */ export const GLOBAL_INSTRUCTION_FILES = ['AGENTS.md', join('.claude', 'CLAUDE.md')] as const +const NESTED_INSTRUCTION_FILE = 'AGENTS.md' +const MAX_NESTED_DISCOVERY_DEPTH = 16 +const MAX_NESTED_DISCOVERY_DIRECTORIES = 10_000 +const MAX_NESTED_DISCOVERED_FILES = 200 +const MAX_NESTED_CONTEXT_PATHS = 64 +const MAX_ACTIVE_NESTED_FILES = 8 +const MAX_NESTED_FILE_BYTES = 32 * 1024 +const MAX_ACTIVE_NESTED_BYTES = 64 * 1024 +const NESTED_DISCOVERY_CACHE_MS = 1_000 + +/** Generated, vendored, or cache directories that cannot own workspace guidance. */ +const NESTED_SKIP_DIRS = new Set([ + 'node_modules', + '.git', + 'dist', + 'dist-test', + 'dist-types', + 'dist-test-iso', + 'out', + 'build', + 'target', + 'vendor', + 'coverage', + '.next', + '.nuxt', + '.svelte-kit', + '.turbo', + '.cache', + '.venv', + 'venv', + '__pycache__', +]) + export interface ProjectInstructionSource { /** Absolute path of the file on disk. */ path: string - /** Bare filename (e.g. `CLAUDE.md`), for display. */ + /** Workspace-relative display name, or the user-global relative path. */ name: string /** Whether the file is user-global or project-scoped. */ scope: InstructionScope /** Trimmed file contents. */ content: string - /** - * Whether the file feeds the system prompt. Project-scoped files are inert - * until the workspace is trusted (context-provenance plan, Phase 2) — the - * same gate that keeps project MCP servers from spawning (#100). Discovered - * so the UI can surface them; excluded from the prompt while `false`. - */ + /** Whether the file feeds the current/most recently assembled turn prompt. */ active: boolean + /** False for project text until the workspace trust gate is granted. */ + trusted: boolean + /** Workspace-relative directory governed by a nested AGENTS.md. */ + scopePath?: string +} + +interface NestedInstructionSource { + path: string + name: string + content: string + scopePath: string +} + +interface NestedDiscoveryState { + directories: number + sources: NestedInstructionSource[] } +/** + * Most recently assembled real-turn activation, for Settings → Sources. + * + * Store workspace-relative source names under the stable project root. A turn + * may execute in an isolated worktree while Settings reads the primary project; + * absolute execution-root paths would never compare equal across that boundary. + */ +const lastNestedActivationByProjectRoot = new Map>() +const nestedDiscoveryCache = new Map< + string, + { expiresAt: number; sources: NestedInstructionSource[] } +>() + async function readTrimmed(path: string): Promise { try { const content = (await fsp.readFile(path, 'utf-8')).trim() return content || null } catch { - return null // missing file is normal + return null + } +} + +function isWithinRoot(path: string, root: string): boolean { + const rel = relative(root, path) + return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`)) +} + +async function canonicalPath(path: string): Promise { + try { + return await fsp.realpath(path) + } catch { + return resolve(path) + } +} + +/** Read a project file only when its symlink target remains inside the workspace. */ +async function readProjectTrimmed( + path: string, + root: string, + maxBytes?: number, +): Promise { + try { + const [canonicalRoot, canonicalFile] = await Promise.all([ + fsp.realpath(root), + fsp.realpath(path), + ]) + if (!isWithinRoot(canonicalFile, canonicalRoot)) return null + if (maxBytes === undefined) return await readTrimmed(canonicalFile) + + const handle = await fsp.open(canonicalFile, 'r') + try { + const buffer = Buffer.alloc(maxBytes + 1) + const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0) + const truncated = bytesRead > maxBytes + const content = buffer.subarray(0, Math.min(bytesRead, maxBytes)).toString('utf-8').trim() + if (!content) return null + return truncated + ? `${content}\n\n[Copse truncated this nested AGENTS.md at ${String(maxBytes)} bytes.]` + : content + } finally { + await handle.close() + } + } catch { + return null + } +} + +function displayPath(root: string, path: string): string { + return relative(root, path).split(sep).join('/') +} + +async function walkNestedInstructionFiles( + root: string, + dir: string, + depth: number, + state: NestedDiscoveryState, +): Promise { + if ( + depth > MAX_NESTED_DISCOVERY_DEPTH || + state.directories >= MAX_NESTED_DISCOVERY_DIRECTORIES || + state.sources.length >= MAX_NESTED_DISCOVERED_FILES + ) { + return + } + state.directories += 1 + + let entries + try { + entries = await fsp.readdir(dir, { withFileTypes: true }) + } catch { + return + } + entries.sort((a, b) => a.name.localeCompare(b.name)) + + // A nested checkout/submodule owns its own instructions. `.git` can be a file + // in a worktree, so match by name rather than only directory type. + if (depth > 0 && entries.some((entry) => entry.name === '.git')) return + + if (depth > 0) { + const instruction = entries.find((entry) => entry.name === NESTED_INSTRUCTION_FILE) + if (instruction?.isFile() || instruction?.isSymbolicLink()) { + const path = join(dir, instruction.name) + const content = await readProjectTrimmed(path, root, MAX_NESTED_FILE_BYTES) + if (content) { + state.sources.push({ + path, + name: displayPath(root, path), + content, + scopePath: displayPath(root, dir), + }) + } + } + } + + for (const entry of entries) { + if ( + state.directories >= MAX_NESTED_DISCOVERY_DIRECTORIES || + state.sources.length >= MAX_NESTED_DISCOVERED_FILES + ) { + break + } + if (!entry.isDirectory() || NESTED_SKIP_DIRS.has(entry.name)) continue + await walkNestedInstructionFiles(root, join(dir, entry.name), depth + 1, state) + } +} + +async function discoverNestedInstructionSources( + root: string, + refresh = false, +): Promise { + const key = resolve(root) + const cached = nestedDiscoveryCache.get(key) + if (!refresh && cached && cached.expiresAt > Date.now()) return cached.sources + const state: NestedDiscoveryState = { directories: 0, sources: [] } + await walkNestedInstructionFiles(root, root, 0, state) + const sources = state.sources.sort((a, b) => a.name.localeCompare(b.name)) + nestedDiscoveryCache.set(key, { + expiresAt: Date.now() + NESTED_DISCOVERY_CACHE_MS, + sources, + }) + return sources +} + +/** Resolve a context path without letting a symlinked ancestor escape the workspace. */ +async function normalizeContextPathWithinRoot( + root: string, + rawPath: string, +): Promise { + const trimmed = rawPath.trim() + if (!trimmed || trimmed.includes('\0')) return null + const absolute = isAbsolute(trimmed) ? resolve(trimmed) : resolve(root, trimmed) + if (!isWithinRoot(absolute, resolve(root))) return null + + const canonicalRoot = await canonicalPath(root) + let existingAncestor = absolute + for (;;) { + try { + const canonicalAncestor = await fsp.realpath(existingAncestor) + if (!isWithinRoot(canonicalAncestor, canonicalRoot)) return null + break + } catch { + const parent = dirname(existingAncestor) + if (parent === existingAncestor) return null + existingAncestor = parent + } + } + + const rel = relative(resolve(root), absolute).split(sep).join('/') + return rel === '.' ? '' : rel +} + +function scopeDepth(scopePath: string): number { + return scopePath.split('/').filter(Boolean).length +} + +function instructionPrecedence(a: NestedInstructionSource, b: NestedInstructionSource): number { + return scopeDepth(a.scopePath) - scopeDepth(b.scopePath) || a.name.localeCompare(b.name) +} + +async function selectNestedInstructionSources( + root: string, + sources: NestedInstructionSource[], + contextPaths: readonly string[], +): Promise { + const normalized = await Promise.all( + [...new Set(contextPaths)] + .slice(0, MAX_NESTED_CONTEXT_PATHS) + .map((path) => normalizeContextPathWithinRoot(root, path)), + ) + const targets = normalized.filter((path): path is string => path !== null) + if (targets.length === 0) return [] + + const applicable = sources + .filter((source) => + targets.some( + (target) => target === source.scopePath || target.startsWith(`${source.scopePath}/`), + ), + ) + .sort(instructionPrecedence) + + // Keep nearest rules under a pathological chain, then restore broad→narrow order. + const retained: NestedInstructionSource[] = [] + let retainedBytes = 0 + for (const source of [...applicable].reverse()) { + if (retained.length >= MAX_ACTIVE_NESTED_FILES) break + const bytes = Buffer.byteLength(source.content, 'utf-8') + if (retainedBytes + bytes > MAX_ACTIVE_NESTED_BYTES) continue + retained.push(source) + retainedBytes += bytes + } + return retained.sort(instructionPrecedence) +} + +function deduplicateSources(resolved: ProjectInstructionSource[]): ProjectInstructionSource[] { + const sources: ProjectInstructionSource[] = [] + const contentIndexes = new Map() + for (const source of resolved) { + const existingIndex = contentIndexes.get(source.content) + if (existingIndex === undefined) { + contentIndexes.set(source.content, sources.length) + sources.push(source) + continue + } + const existing = sources[existingIndex] + // An inactive nested sibling must not shadow the identical source that matched this turn. + if (existing && !existing.active && source.active) sources[existingIndex] = source } + return sources } export interface ProjectInstructionOptions { /** Turn context for Auto-Attached / Manual Cursor rules (issue #636). */ cursorRuleContext?: CursorRuleContext + /** Workspace paths relevant to this turn, used for nested AGENTS.md activation. */ + nestedContextPaths?: readonly string[] + /** Settings-only: report the most recently assembled real turn's activation. */ + useLatestNestedActivation?: boolean + /** Explicit Sources reload bypasses the short prompt-build discovery cache. */ + refreshNestedDiscovery?: boolean } -/** - * Discover the instruction files feeding the system prompt, global layer first then - * project. Identical content is loaded once (across both layers), so a repo whose - * `AGENTS.md` matches the user's global file is not injected twice. - * - * Cursor rules: Always + legacy always; Auto-Attached when `cursorRuleContext` - * paths match; Manual when `@`-mentioned. Agent-Requested rules are catalogued - * separately via {@link loadAgentRequestedRulesCatalog}. - */ +/** Discover instruction sources, global layer first then project. */ export async function loadProjectInstructionSources( opts: ProjectInstructionOptions = {}, ): Promise { @@ -79,23 +336,60 @@ export async function loadProjectInstructionSources( for (const rel of GLOBAL_INSTRUCTION_FILES) { const path = join(home, rel) const content = await readTrimmed(path) - if (content) resolved.push({ path, name: rel, scope: 'global', content, active: true }) + if (content) { + resolved.push({ + path, + name: rel, + scope: 'global', + content, + active: true, + trusted: true, + }) + } } - const root = getWorkspaceRoot() - if (root) { - // Instruction text is one approval away from execution for an agent with - // run_shell, so a cloned repo's AGENTS.md gets the same trust gate as its - // .mcp.json: discovered and listed, but inert until the user trusts the - // workspace. - const trusted = isWorkspaceTrusted(root) + const root = getAgentExecutionRoot() + const projectRoot = getAgentProjectRoot() + if (root && projectRoot) { + const trusted = isWorkspaceTrusted(projectRoot) for (const name of PROJECT_INSTRUCTION_FILES) { const path = join(root, name) - const content = await readTrimmed(path) - if (content) resolved.push({ path, name, scope: 'project', content, active: trusted }) + const content = await readProjectTrimmed(path, root) + if (content) { + resolved.push({ + path, + name, + scope: 'project', + content, + active: trusted, + trusted, + }) + } + } + + const nested = await discoverNestedInstructionSources(root, opts.refreshNestedDiscovery) + const explicitlyActive = + opts.nestedContextPaths !== undefined + ? new Set( + (await selectNestedInstructionSources(root, nested, opts.nestedContextPaths)).map( + (source) => source.path, + ), + ) + : null + const latestActive = opts.useLatestNestedActivation + ? (lastNestedActivationByProjectRoot.get(await canonicalPath(projectRoot)) ?? + new Set()) + : new Set() + for (const source of nested) { + resolved.push({ + ...source, + scope: 'project', + active: trusted && (explicitlyActive?.has(source.path) ?? latestActive.has(source.name)), + trusted, + }) } - // Cursor project rules (`.cursor/rules/*.mdc` + legacy `.cursorrules`) — project text, - // applied after the top-level instruction files. + + // Cursor project rules are applied after AGENTS.md layers. for (const rule of await loadCursorRuleSources(root, opts.cursorRuleContext ?? {})) { resolved.push({ path: rule.path, @@ -103,42 +397,29 @@ export async function loadProjectInstructionSources( scope: 'project', content: rule.content, active: trusted, + trusted, }) } } - // De-duplicate identical content across files and scopes (e.g. a repo whose `AGENTS.md` - // matches the user's global file, or a rule copied into `AGENT.md`). - const sources: ProjectInstructionSource[] = [] - const seenContent = new Set() - for (const source of resolved) { - if (seenContent.has(source.content)) continue - seenContent.add(source.content) - sources.push(source) - } - return sources + return deduplicateSources(resolved) } -/** The two instruction layers the system prompt places separately. */ export interface InstructionLayers { - /** - * Workspace-authored block: provenance guidance plus one - * `` envelope per active project source. When the - * workspace is untrusted this is instead a short Copse-authored note naming - * the inert files, so the agent can explain why they are not applied. - * Empty when the workspace has no instruction files at all. - */ project: string - /** User-global instruction text, joined plainly — the user keeps the last word. */ global: string } -/** - * Neutralise any opening or closing `project_instructions` tag inside a source - * body so instruction content cannot forge or terminate its own envelope. - * Only the `<` of an offending tag is entity-escaped; everything else passes - * through verbatim. Same pattern as the external-content tool-result envelope. - */ +export interface InstructionLayerMetadata { + activeNestedPaths: string[] + activeInstructionContents: string[] + activeNestedBytes: number +} + +export interface InstructionLayersWithMetadata extends InstructionLayers { + metadata: InstructionLayerMetadata +} + function escapeInstructionTag(text: string): string { return text.replace(/<(?=\s*\/?\s*project_instructions)/gi, '<') } @@ -147,8 +428,6 @@ function escapeAttr(value: string): string { return value.replace(/&/g, '&').replace(/"/g, '"').replace(/ - `\n` + - `${escapeInstructionTag(s.content)}\n`, + (source) => + `\n` + + `${escapeInstructionTag(source.content)}\n`, ) .join('\n\n') return `## Workspace instructions\n\n${WORKSPACE_INSTRUCTIONS_GUIDANCE}\n\n${envelopes}` @@ -177,34 +456,143 @@ function buildGatedNote(names: string[]): string { ) } -/** - * Instruction layers for the system prompt. The project layer is wrapped in - * provenance envelopes and placed early (demoted below Copse steering); the - * global layer stays plain and is appended last. - */ -export async function loadInstructionLayers( +/** Build prompt layers and expose the active set to the local runtime. */ +export async function loadInstructionLayersWithMetadata( opts: ProjectInstructionOptions = {}, -): Promise { + trackActivation = false, +): Promise { const sources = await loadProjectInstructionSources(opts) const global = sources - .filter((s) => s.scope === 'global') - .map((s) => s.content) + .filter((source) => source.scope === 'global') + .map((source) => source.content) .join('\n\n') - const project = sources.filter((s) => s.scope === 'project') - const activeProject = project.filter((s) => s.active) - if (activeProject.length > 0) return { project: buildProjectBlock(activeProject), global } - if (project.length > 0) return { project: buildGatedNote(project.map((s) => s.name)), global } - return { project: '', global } + const project = sources.filter((source) => source.scope === 'project') + const activeProject = project.filter((source) => source.active) + const activeNestedPaths = project + .filter((source) => source.active && source.scopePath !== undefined) + .map((source) => source.path) + + const root = getAgentExecutionRoot() + const projectRoot = getAgentProjectRoot() + if (trackActivation && root && projectRoot) { + lastNestedActivationByProjectRoot.set( + await canonicalPath(projectRoot), + new Set( + project + .filter((source) => source.active && source.scopePath !== undefined) + .map((source) => source.name), + ), + ) + } + + const metadata: InstructionLayerMetadata = { + activeNestedPaths, + activeInstructionContents: sources + .filter((source) => source.active) + .map((source) => source.content), + activeNestedBytes: project + .filter((source) => source.active && source.scopePath !== undefined) + .reduce((total, source) => total + Buffer.byteLength(source.content, 'utf-8'), 0), + } + if (activeProject.length > 0) { + return { project: buildProjectBlock(activeProject), global, metadata } + } + if (project.length > 0) { + return { project: buildGatedNote(project.map((source) => source.name)), global, metadata } + } + return { project: '', global, metadata } } -/** - * Agent-requested Cursor rules catalog for the system prompt (empty when none). - * Trust-gated like the rules themselves — an untrusted repo does not get to - * advertise files for the agent to go read. - */ +/** Backwards-compatible prompt-only view used by tests and non-runtime callers. */ +export async function loadInstructionLayers( + opts: ProjectInstructionOptions = {}, +): Promise { + const { project, global } = await loadInstructionLayersWithMetadata(opts) + return { project, global } +} + +export interface NestedInstructionActivation { + block: string + activatedPaths: string[] + injectedPaths: string[] + injectedContents: string[] +} + +/** Activate instructions for a file tool that introduced a path after turn start. */ +export async function activateNestedInstructionSources( + contextPaths: readonly string[], + alreadyActivePaths: ReadonlySet, + alreadyActiveContents: ReadonlySet, + alreadyActiveNestedBytes: number, +): Promise { + const root = getAgentExecutionRoot() + const projectRoot = getAgentProjectRoot() + if (!root || !projectRoot || !isWorkspaceTrusted(projectRoot)) { + return { block: '', activatedPaths: [], injectedPaths: [], injectedContents: [] } + } + + const selected = await selectNestedInstructionSources( + root, + await discoverNestedInstructionSources(root), + contextPaths, + ) + const candidates = selected.filter( + (source) => !alreadyActivePaths.has(source.path) && !alreadyActiveContents.has(source.content), + ) + const fresh: NestedInstructionSource[] = [] + let remainingFiles = Math.max(0, MAX_ACTIVE_NESTED_FILES - alreadyActivePaths.size) + let remainingBytes = Math.max(0, MAX_ACTIVE_NESTED_BYTES - alreadyActiveNestedBytes) + for (const source of [...candidates].reverse()) { + const bytes = Buffer.byteLength(source.content, 'utf-8') + if (remainingFiles <= 0 || bytes > remainingBytes) continue + fresh.push(source) + remainingFiles -= 1 + remainingBytes -= bytes + } + fresh.sort(instructionPrecedence) + const injectedPaths = fresh.map((source) => source.path) + const injectedPathSet = new Set(injectedPaths) + // Sources skipped by the prompt-wide caps stay scoped rather than being + // reported as active. Identical content that is already present does count + // as active without consuming the budget twice. + const activatedPaths = selected + .filter( + (source) => + alreadyActivePaths.has(source.path) || + alreadyActiveContents.has(source.content) || + injectedPathSet.has(source.path), + ) + .map((source) => source.path) + const activatedPathSet = new Set(activatedPaths) + const projectKey = await canonicalPath(projectRoot) + const latest = new Set(lastNestedActivationByProjectRoot.get(projectKey) ?? []) + for (const source of selected) { + if (activatedPathSet.has(source.path)) latest.add(source.name) + } + lastNestedActivationByProjectRoot.set(projectKey, latest) + + if (fresh.length === 0) { + return { block: '', activatedPaths, injectedPaths: [], injectedContents: [] } + } + const promptSources: ProjectInstructionSource[] = fresh.map((source) => ({ + ...source, + scope: 'project', + active: true, + trusted: true, + })) + return { + block: buildProjectBlock(promptSources), + activatedPaths, + injectedPaths, + injectedContents: fresh.map((source) => source.content), + } +} + +/** Agent-requested Cursor rules catalog for the system prompt (empty when none). */ export async function loadAgentRequestedRulesCatalog(): Promise { - const root = getWorkspaceRoot() - if (!root || !isWorkspaceTrusted(root)) return '' + const root = getAgentExecutionRoot() + const projectRoot = getAgentProjectRoot() + if (!root || !projectRoot || !isWorkspaceTrusted(projectRoot)) return '' const rules = await discoverCursorRules(root) return buildAgentRequestedRulesCatalog(rules) } diff --git a/src/renderer/views/settings-dialog.ts b/src/renderer/views/settings-dialog.ts index 076ec6eea2..7998724158 100644 --- a/src/renderer/views/settings-dialog.ts +++ b/src/renderer/views/settings-dialog.ts @@ -1007,13 +1007,16 @@ export function mountSettingsDialog(store: AppStore, api: ApiClient): void {
Instruction files

- Files appended to the system prompt, in precedence order. Global steering + Files available to the system prompt, in precedence order. Global steering (~/AGENTS.md, ~/.claude/CLAUDE.md) loads first, then project AGENT.md/AGENTS.md (cross-tool), - CLAUDE.md (Claude Code), and always-applied Cursor rules + CLAUDE.md (Claude Code), directory-scoped nested + AGENTS.md, and always-applied Cursor rules (.cursor/rules/*.mdc with alwaysApply: true, plus .cursorrules). Auto-attached and manually @-mentioned - rules also join this list for the turn that activates them. + rules also join this list for the turn that activates them. Nested + AGENT.md and CLAUDE.md remain root-only compatibility + formats.

Loading… @@ -2659,15 +2662,34 @@ export function mountSettingsDialog(store: AppStore, api: ApiClient): void { * fix for "discovered but not loaded" sits on the thing reporting it. */ function makeInstructionRow(file: ProjectInstructionSummary): HTMLElement { + const nestedStatus = + file.scopePath === undefined + ? '' + : file.active + ? ` · scope: ${file.scopePath}/ · active this turn` + : ` · scope: ${file.scopePath}/ · activates when a path under this directory enters context` const detail = `${file.path} · ${formatByteSize(file.bytes)}` + - (file.active ? '' : ' · inert until you trust this workspace — click the badge to trust it') - const row = makeSourceRow(file.name, file.active ? file.scope : 'not loaded', detail, { - badgeClass: !file.active + (file.trusted + ? nestedStatus + : ' · inert until you trust this workspace — click the badge to trust it') + const badge = !file.trusted + ? 'not loaded' + : file.scopePath !== undefined + ? file.active + ? 'active' + : 'scoped' + : file.scope + const row = makeSourceRow(file.name, badge, detail, { + badgeClass: !file.trusted ? 'sources-badge-untrusted' - : file.scope === 'project' - ? 'sources-badge-project' - : undefined, + : file.scopePath !== undefined + ? file.active + ? 'sources-badge-auto' + : undefined + : file.scope === 'project' + ? 'sources-badge-project' + : undefined, titleAction: { label: `Open ${file.name}`, run: () => { @@ -2675,7 +2697,7 @@ export function mountSettingsDialog(store: AppStore, api: ApiClient): void { }, }, }) - if (file.active) return row + if (file.trusted) return row const badgeEl = row.querySelector('.sources-badge') if (badgeEl) { @@ -2708,7 +2730,7 @@ export function mountSettingsDialog(store: AppStore, api: ApiClient): void { fillSourceList( '#sources-instructions-list', instructions.map((f) => makeInstructionRow(f)), - 'No instruction files (add AGENT.md, AGENTS.md, or CLAUDE.md to the workspace root, or ~/AGENTS.md globally).', + 'No instruction files (add AGENT.md, AGENTS.md, or CLAUDE.md to the workspace root; nested directories may add AGENTS.md; or add ~/AGENTS.md globally).', ) const kindLabel: Record = { diff --git a/src/renderer/views/settings-sources-instructions.test.ts b/src/renderer/views/settings-sources-instructions.test.ts index f9347b0fbd..42bca715f7 100644 --- a/src/renderer/views/settings-sources-instructions.test.ts +++ b/src/renderer/views/settings-sources-instructions.test.ts @@ -24,6 +24,19 @@ function untrusted(): ProjectInstructionSummary { scope: 'project', bytes: 7129, active: false, + trusted: false, + } +} + +function nested(name: string, scopePath: string, active: boolean): ProjectInstructionSummary { + return { + path: `/workspace/${name}`, + name, + scope: 'project', + bytes: 256, + active, + trusted: true, + scopePath, } } @@ -60,7 +73,7 @@ function stubApi(initial: ProjectInstructionSummary[]): Harness { unsandboxedProjectHooks: () => Promise.resolve([]), setTrusted: (trusted: boolean) => { trustCalls.push(trusted) - files = files.map((file) => ({ ...file, active: trusted })) + files = files.map((file) => ({ ...file, active: trusted, trusted })) return Promise.resolve([]) }, }, @@ -109,6 +122,27 @@ describe('settings sources → instructions', () => { assert.doesNotMatch(detail, /7129 B/) }) + it('shows the latest turn activation separately from a nested file scope', async () => { + const list = await openInstructions( + stubApi([ + nested('packages/api/AGENTS.md', 'packages/api', true), + nested('packages/web/AGENTS.md', 'packages/web', false), + ]).api, + ) + const rows = [...list.querySelectorAll('.sources-row')] + const api = rows.find((row) => row.textContent.includes('packages/api/AGENTS.md')) + const web = rows.find((row) => row.textContent.includes('packages/web/AGENTS.md')) + assert.ok(api) + assert.ok(web) + assert.equal(api.querySelector('.sources-badge')?.textContent, 'active') + assert.match(api.querySelector('.sources-row-detail')?.textContent ?? '', /active this turn/) + assert.equal(web.querySelector('.sources-badge')?.textContent, 'scoped') + assert.match( + web.querySelector('.sources-row-detail')?.textContent ?? '', + /activates when a path under this directory enters context/, + ) + }) + it('points at the badge as the way to trust an inert file', async () => { const list = await openInstructions(stubApi([untrusted()]).api) const badge = trustBadge(list) diff --git a/src/shared/types/instructions.ts b/src/shared/types/instructions.ts index b94f972a03..585780a8f6 100644 --- a/src/shared/types/instructions.ts +++ b/src/shared/types/instructions.ts @@ -11,6 +11,10 @@ export interface ProjectInstructionSummary { scope: InstructionScope /** Byte length of the trimmed content fed to the prompt. */ bytes: number - /** False when discovered but inert (project file in an untrusted workspace). */ + /** Whether the source was loaded into the current or most recently assembled prompt. */ active: boolean + /** Project trust gate, separate from conditional nested activation. */ + trusted: boolean + /** Workspace-relative directory governed by a nested AGENTS.md. */ + scopePath?: string } diff --git a/tests/e2e/screenshots/settings-sources-nested-instructions.png b/tests/e2e/screenshots/settings-sources-nested-instructions.png new file mode 100644 index 0000000000000000000000000000000000000000..48f89d70bbbd00fa2b3c9e4a9d88924d4acbc49a GIT binary patch literal 131859 zcmeFZWmK1I_cba=hcwdN-6bI{-O?eQ0!m9rH`3h=5`svlG$`HO-QB-)@jhoi`#;Y4 z@Q(NUJBIti7KYrv>t1WFx#pY~p~{NVs7M4zFJ8Prm3b$j`r-v__KO$LPYBT9Zzfi$ z5?{Q4!mtt-S2ld{!Yw^vq(c^36c7HyhgY!vr!{XR@I4}akmtf#!0K!4B463 zT=RnbVh!sCz16qXcV$-kWMdX`Kq7^X=H1EG_UJNW--gB)6dM0xPBee8K(pvh?XzMc z-&$B_LeB4}mGVo9hBs~9tB!LgV*Q`HiLG$AX$hU?loog^Xi4y{DR4$jOdd5@#<1?j zmOpt7s#<;ak!q{a)whBWZX2BFuWE0$N+A-nkTSc~OT#KWbQUbd_^)_mBVE8N`3f@c z$R7wU-K1}(QDk&toonl48*Qb;qwsHTlglxt-7CC|6Il_PTlkAmFYvOEJMAau`wExC zi|G;}A^tZ){5KLWLf=6k<@R3Bu^W|l;iM?bJ5|vAOw-UyW2f$K8n2<(!y;*w-_PW3F+RkuI z|9}_tpFiyOr#?_ok-ST8gUofNWoar$G>8gV8}r~HJ#J1`hb*I=4rWe^^P+T2VlsZc2O3Qg& zx5m=<+-EaEvr%g3YTn*wCnwl^;m6Hdc-_ZEkD?5hF{Qa$yC!gCBKHSB@A|bZGMtC_ zw=L5Z$=tU8dw>z{QHDh8+CR-vi3Kjer;=@1Z-YGAyWDgphnKZI-E~mGE_hG(X@ie< zcXjw3qL{+5x#EvT!eu!^o8?K7Xrbi{M{VIyUomb;R7(6W7Z=$6JzTrYD!vh7j;+ADxNf zXq8+d(@BIpn3_+1e^NLadUM?f zc!;(q#yCek$|6t(9*>(zhM0>ZJ{CjiGJpJvgXR2QAGbkTy}7N%@_Cm%IlT#>URnH8 zCLjLnKbU`khU@WK#x(G{)R-pYznoAroBKT`$NJY20-kVXi}}pLVWr*IA0Dl6P55qy z(g!|FYM^?MWxm<{^J7xIZqHxZbcf&8-^**eadqWWI0_~*&i7zQ|Lvn~UZ_`#>u*zp zzSP>d)!5E{%5a_<^#RZF8SZ>hP+}Ct`*t(QbHoIPzEb&pAF*w;wXgOo->-iYd3AI!Q?-t(YrE?6yD10-|LkV{+czTq zSkfLeLyz5pBtvg^qD63aY&WIu=UW|0xLQWO3Ga2RQp^&`DZ%q^`8=JICc*6j;J~?^ z&zFg_8ZU$mj7IAIHEW(`*L>QekjArFkmY&SztJDp_HZVrsShXfTqAGcWd6vpw!{l% zh-!WQ%Uw5T%{bs0BZ+%^F|L&5eP^W_t3RC@s59xtknzTK>QFKeype;u{YuFFG6X

yhSW0xpU*6?rv%y3z$?EezoWuD?N2}u1 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≧vvV@I>|k}JbROIO?HbP@hhCVt&P0b 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 { + mkdirSync(E2E_SCREENSHOT_DIR, { recursive: true }) + resetUserData() + workspaceRoot = mkdtempSync(join(tmpdir(), 'copse-e2e-nested-instructions-')) + mkdirSync(join(workspaceRoot, 'packages', 'api', 'src'), { recursive: true }) + mkdirSync(join(workspaceRoot, 'packages', 'web', 'src'), { recursive: true }) + writeFileSync(join(workspaceRoot, 'AGENTS.md'), 'Root workspace conventions.\n', 'utf8') + writeFileSync( + join(workspaceRoot, 'packages', 'api', 'AGENTS.md'), + 'API package conventions.\n', + 'utf8', + ) + writeFileSync( + join(workspaceRoot, 'packages', 'web', 'AGENTS.md'), + 'Web package conventions.\n', + 'utf8', + ) + + seedEmptyProject(workspaceRoot, PROJECT_ID, { + subagentsEnabled: false, + model: 'claude-sonnet-4-6', + }) + writeSeedConfig({ + projects: [{ id: PROJECT_ID, path: workspaceRoot, name: 'workspace' }], + activeProjectId: PROJECT_ID, + trustedWorkspaceRoots: [realpathSync(workspaceRoot)], + [`threads:${PROJECT_ID}`]: [], + }) + await browser.reloadSession() + }) + + after(() => { + resetUserData() + rmSync(workspaceRoot, { recursive: true, force: true }) + }) + + it('shows active and inactive directory scopes after a path activates one branch', async () => { + await $('.prompt-input').waitForExist({ timeout: 30_000 }) + await setComposerValue('Review packages/api/src/router.ts and explain its role.') + await $('.submit-btn').click() + await $('.messages-list .msg-assistant').waitForExist({ timeout: 30_000 }) + await waitForAgentIdle(30_000) + + await $('[aria-label="Settings"]').click() + const dialog = $('#settings-dialog') + await expect(dialog).toBeDisplayed() + await dialog.$('button[data-section="customise"]').click() + const list = dialog.$('#sources-instructions-list') + await browser.waitUntil( + async () => { + const text = await list.getText() + return text.includes('packages/api/AGENTS.md') && text.includes('packages/web/AGENTS.md') + }, + { timeout: 15_000, timeoutMsg: 'expected both nested AGENTS.md sources' }, + ) + + const apiRow = list.$('.sources-row*=packages/api/AGENTS.md') + const webRow = list.$('.sources-row*=packages/web/AGENTS.md') + await expect(apiRow.$('.sources-badge')).toHaveText('active', { ignoreCase: true }) + await expect(webRow.$('.sources-badge')).toHaveText('scoped', { ignoreCase: true }) + assert.match(await apiRow.$('.sources-row-detail').getText(), /scope: packages\/api\//) + assert.match(await apiRow.$('.sources-row-detail').getText(), /active this turn/) + assert.match( + await webRow.$('.sources-row-detail').getText(), + /activates when a path under this directory enters context/, + ) + + await browser.execute(() => { + const rows = document.querySelectorAll('#sources-instructions-list .sources-row') + for (const row of rows) { + const detail = row.querySelector('.sources-row-detail') + if (!detail?.textContent) continue + const parts = detail.textContent.split(' · ') + parts[0] = `/${row.querySelector('.sources-row-title')?.textContent ?? ''}` + detail.textContent = parts.join(' · ') + } + document + .querySelector('#sources-instructions-list') + ?.closest('fieldset') + ?.scrollIntoView({ block: 'start' }) + }) + await saveElementScreenshot( + 'fieldset:has(#sources-instructions-list)', + 'settings-sources-nested-instructions.png', + ) + }) +}) From ae94b3b30970cd7a6f1eb7cef5085a3de2a9b330 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 10:04:17 +0000 Subject: [PATCH 2/5] perf(instructions): discover nested AGENTS.md once per turn The nested AGENTS.md walk was cached for one second, so a turn re-walked up to 10,000 directories before nearly every file tool call. A turn now creates a discovery memo (`createNestedInstructionTurn`) that the system prompt build seeds with one fresh walk; every `activateNestedInstructionSources` call of that turn reuses it. Callers outside a turn (the composer estimate) share a 30 s cache, and Settings still forces a reload. An edit tool whose path is an AGENTS.md invalidates both the memo and the shared cache after it runs, so the next file tool call sees the file the agent wrote, moved, or removed. `run_shell` writes are not observed. Each mid-turn activation now adds a one-line transcript notice naming the file, emitted as a text chunk at the next step boundary rather than between a tool call and its result (a text chunk there would start a new assistant bubble and strand the tool card). Discovery records when it stopped at a directory, file, or depth cap: project sources carry `discoveryTruncated` and the walk logs a warning. A nested file that repeats an already-listed source stays listed and is marked `duplicateOf` instead of being dropped; its content is still injected once. The latest-activation record for Settings is now keyed per thread under the project root, so concurrent threads no longer overwrite each other; Settings reads the most recent thread. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011jaQbed96h8aSZLijfiHNB --- src/main/services/agent-service.test.ts | 138 +++++++++ src/main/services/agent-service.ts | 51 +++- src/main/services/agent-system-prompt.ts | 12 +- .../services/project-instructions.test.ts | 118 ++++++++ src/main/services/project-instructions.ts | 263 +++++++++++++++--- 5 files changed, 535 insertions(+), 47 deletions(-) diff --git a/src/main/services/agent-service.test.ts b/src/main/services/agent-service.test.ts index 6457a25232..7fb975d476 100644 --- a/src/main/services/agent-service.test.ts +++ b/src/main/services/agent-service.test.ts @@ -341,6 +341,144 @@ describe('runAgent AgentHost decoupling', () => { } }) + it('discovers nested instructions once per turn, notices activations, and re-walks after an AGENTS.md write', async () => { + const root = await mkdtemp(join(tmpdir(), 'copse-agent-nested-discovery-')) + await mkdir(join(root, 'packages', 'api'), { recursive: true }) + await mkdir(join(root, 'packages', 'web'), { recursive: true }) + await writeFile(join(root, 'packages', 'api', 'AGENTS.md'), 'API rules here.') + const registry = new ToolRegistry() + registry.register( + defineTool({ + name: 'read_file', + description: 'Test read tool', + parameters: z.object({ path: z.string() }), + execute: () => Promise.resolve('contents'), + }), + ) + registry.register( + defineTool({ + name: 'write_file', + description: 'Test write tool', + parameters: z.object({ path: z.string(), content: z.string() }), + execute: async ({ path, content }) => { + await writeFile(join(root, path), content) + return 'File written.' + }, + }), + ) + + const readTool = ( + id: string, + path: string, + ): { type: 'tool_call'; toolCall: { id: string; name: string; args: { path: string } } } => ({ + type: 'tool_call', + toolCall: { id, name: 'read_file', args: { path } }, + }) + let calls = 0 + const provider: LLMProvider = { + stream: async function* (messages) { + calls += 1 + const system = messages.find((message) => message.role === 'system') + assert.ok(system?.role === 'system') + switch (calls) { + case 1: + assert.doesNotMatch(system.content, /API rules here/) + yield readTool('read-api', 'packages/api/a.ts') + return + case 2: + assert.match(system.content, /API rules here/) + // Appears mid-turn without going through a tool: the turn's memo + // does not see it, so the next read must not activate it. + await writeFile(join(root, 'packages', 'web', 'AGENTS.md'), 'Web rules here.') + yield readTool('read-web-stale', 'packages/web/b.ts') + return + case 3: + assert.doesNotMatch(system.content, /Web rules here/) + yield { + type: 'tool_call' as const, + toolCall: { + id: 'write-web-agents', + name: 'write_file', + args: { path: 'packages/web/AGENTS.md', content: 'Web rules here.' }, + }, + } + return + case 4: + assert.doesNotMatch(system.content, /Web rules here/) + // A different path than the stale read: the loop skips a repeat of + // a recent call's exact arguments. + yield readTool('read-web-fresh', 'packages/web/c.ts') + return + default: + assert.match(system.content, /Web rules here/) + yield { type: 'text' as const, text: 'Done.' } + } + }, + } + const received: StreamChunk[] = [] + setDefaultPluginRegistry(new PluginRegistry()) + await setSetting('subagentsEnabled', false) + await setSetting('skillsEnabled', false) + + try { + await runWithWorkspaceTrust(root, true, () => + runWithThreadExecutionContext( + { + projectId: 'project-nested-discovery', + threadId: 'thread-nested-discovery', + projectRoot: root, + root, + checkoutMode: 'shared', + branch: null, + }, + () => + runWithActiveRunIdentity('thread-nested-discovery', () => + agentService.runAgent( + 'thread-nested-discovery', + 'Look around.', + [], + { emit: (_threadId, chunk) => received.push(chunk) }, + registry, + { + provider, + contextWindow: 100_000, + model: 'claude-sonnet-4-6', + maxSteps: 8, + maxLlmCalls: 8, + }, + ), + ), + ), + ) + assert.equal(calls, 5) + + // One transcript line per activation, landing after that call's result — + // never between a tool call and its result, where it would strand the card. + const notices = received.flatMap((chunk, index) => + chunk.type === 'text' && chunk.text.includes('Loaded directory-scoped instructions') + ? [{ index, text: chunk.text }] + : [], + ) + assert.deepEqual( + notices.map((notice) => notice.text), + [ + '_Loaded directory-scoped instructions from `packages/api/AGENTS.md`._\n\n', + '_Loaded directory-scoped instructions from `packages/web/AGENTS.md`._\n\n', + ], + ) + const resultIndex = (toolCallId: string): number => + received.findIndex( + (chunk) => chunk.type === 'tool_result' && chunk.toolCallId === toolCallId, + ) + assert.ok(resultIndex('read-api') >= 0) + assert.ok(notices[0] && notices[0].index > resultIndex('read-api')) + assert.ok(notices[1] && notices[1].index > resultIndex('read-web-fresh')) + } finally { + setDefaultPluginRegistry(null) + await rm(root, { recursive: true, force: true }) + } + }) + it('emits a structured terminal record with raw provider failure details', async () => { const received: StreamChunk[] = [] const host: AgentHost = { diff --git a/src/main/services/agent-service.ts b/src/main/services/agent-service.ts index fd498a3602..5cd02a1bad 100644 --- a/src/main/services/agent-service.ts +++ b/src/main/services/agent-service.ts @@ -44,7 +44,11 @@ import { import { normalizeStopReason } from '@copse/agent/headless-contract.ts' import { resolveParentGoal } from '@copse/agent/working-brief.ts' import { buildSystemPromptWithMetadata } from './agent-system-prompt.ts' -import { activateNestedInstructionSources } from './project-instructions.ts' +import { + activateNestedInstructionSources, + createNestedInstructionTurn, + invalidateNestedInstructionDiscoveryForWrite, +} from './project-instructions.ts' import { hasLastUsage } from '@copse/llm/provider-usage.ts' import { clearActiveRunThread, @@ -417,6 +421,19 @@ function instructionContextPathsForTool(name: string, args: unknown): string[] { }) } +/** + * One transcript line per nested AGENTS.md that joined the prompt mid-turn, in + * the same italic-note voice as the model-fallback notice. Emitted as a text + * chunk between steps — never between a `tool_call` and its `tool_result`, + * where a text chunk would start a new assistant bubble and strand the card. + */ +function nestedInstructionsActivatedNotice(names: readonly string[]): string { + return ( + names.map((name) => `_Loaded directory-scoped instructions from \`${name}\`._`).join('\n') + + '\n\n' + ) +} + /** * Fire `beforeSubmitPrompt` (B1). Returns the user-facing notice to show when a * hook halted the submit (`continue: false`), or null to proceed. Recording is @@ -1537,6 +1554,9 @@ export async function runAgent( ? options.invokedAgent : undefined + // One nested-AGENTS.md walk per turn: the prompt build below seeds the memo + // and every file tool call of this turn reuses it. + const nestedInstructionTurn = createNestedInstructionTurn() const systemPromptBuild = await buildSystemPromptWithMetadata({ subagentsEnabled, invokedSkills, @@ -1544,6 +1564,7 @@ export async function runAgent( userPrompt: outboundPrompt, model, trackInstructionActivation: true, + nestedInstructionTurn, }) const systemPrompt = systemPromptBuild.prompt const activeNestedInstructionPaths = new Set( @@ -1553,6 +1574,16 @@ export async function runAgent( systemPromptBuild.instructionMetadata.activeInstructionContents, ) let activeNestedInstructionBytes = systemPromptBuild.instructionMetadata.activeNestedBytes + // Names of nested files activated by a tool call, held until the next step + // boundary so the notice lands after that call's tool_result. + const pendingNestedInstructionNotices: string[] = [] + const flushNestedInstructionNotices = (): void => { + if (pendingNestedInstructionNotices.length === 0) return + sendChunk({ + type: 'text', + text: nestedInstructionsActivatedNotice(pendingNestedInstructionNotices.splice(0)), + }) + } const messages: LLMMessage[] = [ { role: 'system', content: systemPrompt }, @@ -1868,6 +1899,7 @@ export async function runAgent( activeNestedInstructionPaths, activeInstructionContents, activeNestedInstructionBytes, + nestedInstructionTurn, ) for (const path of activation.activatedPaths) { activeNestedInstructionPaths.add(path) @@ -1876,6 +1908,7 @@ export async function runAgent( activeInstructionContents.add(content) activeNestedInstructionBytes += Buffer.byteLength(content, 'utf-8') } + pendingNestedInstructionNotices.push(...activation.injectedNames) if (activation.block) { const leadingSystem = trimmed.find((message) => message.role === 'system') if (leadingSystem?.role === 'system') { @@ -2007,6 +2040,15 @@ export async function runAgent( const startedAt = Date.now() try { const raw = await runParentTool(name, args, signal, toolCallId) + // The agent just wrote, moved, or removed an AGENTS.md: the turn's + // discovery memo no longer describes the tree, so the next file tool + // call re-walks. `run_shell` writes are not seen here (documented). + if (isEditTool(name)) { + invalidateNestedInstructionDiscoveryForWrite( + instructionContextPathsForTool(name, args), + nestedInstructionTurn, + ) + } fireAfterToolUseHook({ threadId, turnTreeId, @@ -2063,6 +2105,9 @@ export async function runAgent( // `messages` above is `trimmed`, mutated in place as turns land, so // this persists everything the previous step produced. checkpointHistory() + // The previous step's tool results have been streamed; a notice + // here cannot come between a tool call and its result. + flushNestedInstructionNotices() }, recordStreamCut: (record) => { recordStreamCut(record, model) @@ -2092,6 +2137,9 @@ export async function runAgent( } }, }) + // A loop that stopped on a tool step (step cap, abort) still owes + // the notice for what that step activated. + flushNestedInstructionNotices() const subUsage = getAccumulatedSubagentUsage() if (subUsage.inputTokens || subUsage.outputTokens) { @@ -2149,6 +2197,7 @@ export async function runAgent( onLlmCall: (count: number): void => { setHookRunStep(count) checkpointHistory() + flushNestedInstructionNotices() }, recordStreamCut: (record) => { recordStreamCut(record, model) diff --git a/src/main/services/agent-system-prompt.ts b/src/main/services/agent-system-prompt.ts index 7430103b9b..90955aeae1 100644 --- a/src/main/services/agent-system-prompt.ts +++ b/src/main/services/agent-system-prompt.ts @@ -2,6 +2,7 @@ import { loadAgentRequestedRulesCatalog, loadInstructionLayersWithMetadata, type InstructionLayerMetadata, + type NestedInstructionTurn, } from './project-instructions.ts' import { getSetting, getSettingTrimmed } from './storage/settings.ts' import { getAgentExecutionRoot } from './execution-root.ts' @@ -93,6 +94,11 @@ export interface BuildSystemPromptOptions { model?: string /** Persist nested-source activation for Settings; real local turns set this. */ trackInstructionActivation?: boolean + /** + * The turn's nested-discovery memo. The build walks the tree once into it so + * the turn's tool calls reuse that walk; estimates omit it and share a cache. + */ + nestedInstructionTurn?: NestedInstructionTurn } export interface SystemPromptBuildResult { @@ -113,7 +119,11 @@ export async function buildSystemPromptWithMetadata( userText, } const instructionLayers = await loadInstructionLayersWithMetadata( - { cursorRuleContext, nestedContextPaths: contextPaths }, + { + cursorRuleContext, + nestedContextPaths: contextPaths, + ...(opts.nestedInstructionTurn ? { nestedInstructionTurn: opts.nestedInstructionTurn } : {}), + }, opts.trackInstructionActivation ?? false, ) const agentRulesCatalog = await loadAgentRequestedRulesCatalog() diff --git a/src/main/services/project-instructions.test.ts b/src/main/services/project-instructions.test.ts index ab85692c46..6aac002d34 100644 --- a/src/main/services/project-instructions.test.ts +++ b/src/main/services/project-instructions.test.ts @@ -5,6 +5,8 @@ import { join } from 'node:path' import { tmpdir } from 'node:os' import { activateNestedInstructionSources, + createNestedInstructionTurn, + invalidateNestedInstructionDiscoveryForWrite, loadInstructionLayers, loadInstructionLayersWithMetadata, loadProjectInstructionSources, @@ -303,6 +305,122 @@ describe('project-instructions', () => { }) }) + it('walks the tree once per turn and re-walks only after an AGENTS.md write', async () => { + await mkdir(join(projectRoot, 'packages', 'api'), { recursive: true }) + await mkdir(join(projectRoot, 'packages', 'web'), { recursive: true }) + await writeFile(join(projectRoot, 'packages', 'api', 'AGENTS.md'), 'API rules') + const turn = createNestedInstructionTurn() + const activate = (paths: string[]): ReturnType => + withTrust(true, () => activateNestedInstructionSources(paths, new Set(), new Set(), 0, turn)) + + const first = await activate(['packages/api/a.ts']) + assert.deepEqual(first.injectedNames, ['packages/api/AGENTS.md']) + + // A file that appears mid-turn is invisible to the same turn: the memo is + // reused rather than the tree re-walked before every tool call. + await writeFile(join(projectRoot, 'packages', 'web', 'AGENTS.md'), 'Web rules') + const stale = await activate(['packages/web/b.ts']) + assert.deepEqual(stale.injectedNames, []) + + // A write to something other than an AGENTS.md keeps the memo… + assert.equal(invalidateNestedInstructionDiscoveryForWrite(['packages/web/b.ts'], turn), false) + assert.deepEqual((await activate(['packages/web/b.ts'])).injectedNames, []) + // …while a write to the AGENTS.md itself forgets it, so the next call sees the file. + assert.equal( + invalidateNestedInstructionDiscoveryForWrite(['packages/web/AGENTS.md'], turn), + true, + ) + assert.deepEqual((await activate(['packages/web/b.ts'])).injectedNames, [ + 'packages/web/AGENTS.md', + ]) + }) + + it('flags project sources when discovery stops at a cap', async () => { + // Depth is the cheapest cap to hit: one directory beyond the limit. + const segments: string[] = [] + for (let depth = 1; depth <= 17; depth++) segments.push(`d${String(depth)}`) + await mkdir(join(projectRoot, ...segments), { recursive: true }) + await writeFile(join(projectRoot, ...segments, 'AGENTS.md'), 'Too deep') + await writeFile(join(projectRoot, 'AGENTS.md'), 'Root rules') + + const sources = await withTrust(true, () => + loadProjectInstructionSources({ refreshNestedDiscovery: true }), + ) + assert.deepEqual( + sources.map((source) => ({ name: source.name, truncated: source.discoveryTruncated })), + [{ name: 'AGENTS.md', truncated: true }], + ) + + await rm(join(projectRoot, 'd1'), { recursive: true, force: true }) + const complete = await withTrust(true, () => + loadProjectInstructionSources({ refreshNestedDiscovery: true }), + ) + assert.equal(complete[0]?.discoveryTruncated, undefined) + }) + + it('lists a nested file that repeats the root rules as a duplicate, injected once', async () => { + await writeFile(join(projectRoot, 'AGENTS.md'), 'Shared rules') + await mkdir(join(projectRoot, 'packages', 'api'), { recursive: true }) + await writeFile(join(projectRoot, 'packages', 'api', 'AGENTS.md'), 'Shared rules') + + const sources = await withTrust(true, () => + loadProjectInstructionSources({ nestedContextPaths: ['packages/api/a.ts'] }), + ) + assert.deepEqual( + sources.map((source) => ({ name: source.name, duplicateOf: source.duplicateOf })), + [ + { name: 'AGENTS.md', duplicateOf: undefined }, + { name: 'packages/api/AGENTS.md', duplicateOf: 'AGENTS.md' }, + ], + ) + const layers = await withTrust(true, () => + loadInstructionLayers({ nestedContextPaths: ['packages/api/a.ts'] }), + ) + assert.equal(layers.project.match(/ { + for (const name of ['api', 'web']) { + await mkdir(join(projectRoot, 'packages', name), { recursive: true }) + await writeFile(join(projectRoot, 'packages', name, 'AGENTS.md'), `${name} rules`) + } + const inThread = (threadId: string, fn: () => Promise): Promise => + withTrust(true, () => + runWithThreadExecutionContext( + { + projectId: 'project', + threadId, + projectRoot, + root: projectRoot, + checkoutMode: 'shared', + branch: null, + }, + fn, + ), + ) + const activeNames = ( + sources: { name: string; active: boolean; scopePath?: string }[], + ): string[] => sources.filter((s) => s.scopePath !== undefined && s.active).map((s) => s.name) + + await inThread('thread-a', () => + loadInstructionLayersWithMetadata({ nestedContextPaths: ['packages/api/a.ts'] }, true), + ) + await inThread('thread-b', () => + loadInstructionLayersWithMetadata({ nestedContextPaths: ['packages/web/b.ts'] }, true), + ) + + const latest = { useLatestNestedActivation: true } + assert.deepEqual( + activeNames(await inThread('thread-a', () => loadProjectInstructionSources(latest))), + ['packages/api/AGENTS.md'], + ) + // Settings runs outside any thread and reads the most recent turn. + assert.deepEqual( + activeNames(await withTrust(true, () => loadProjectInstructionSources(latest))), + ['packages/web/AGENTS.md'], + ) + }) + it('reports worktree activation against the stable project root in Sources', async () => { const worktreeRoot = await mkdtemp(join(tmpdir(), 'copse-panel-instructions-worktree-')) const projectAlias = `${projectRoot}-alias` diff --git a/src/main/services/project-instructions.ts b/src/main/services/project-instructions.ts index 5814c39079..4217bb555f 100644 --- a/src/main/services/project-instructions.ts +++ b/src/main/services/project-instructions.ts @@ -1,6 +1,6 @@ import * as fsp from 'node:fs/promises' import { homedir } from 'node:os' -import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path' +import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path' import type { InstructionScope } from '@shared/types/instructions.ts' import { buildAgentRequestedRulesCatalog, @@ -10,6 +10,7 @@ import { } from './skills/cursor-rules.ts' import { getAgentExecutionRoot, getAgentProjectRoot } from './execution-root.ts' import { isWorkspaceTrusted } from './security/workspace-trust.ts' +import { getThreadExecutionContext } from './thread-execution-context.ts' /** * Project-root instruction files, in precedence order. @@ -31,7 +32,12 @@ const MAX_NESTED_CONTEXT_PATHS = 64 const MAX_ACTIVE_NESTED_FILES = 8 const MAX_NESTED_FILE_BYTES = 32 * 1024 const MAX_ACTIVE_NESTED_BYTES = 64 * 1024 -const NESTED_DISCOVERY_CACHE_MS = 1_000 +/** + * How long a walk serves callers outside a turn (the composer's context + * estimate). A turn never relies on this: it walks once at turn start and + * memoises the result for its own tool calls (see {@link NestedInstructionTurn}). + */ +const NESTED_DISCOVERY_CACHE_MS = 30_000 /** Generated, vendored, or cache directories that cannot own workspace guidance. */ const NESTED_SKIP_DIRS = new Set([ @@ -71,6 +77,17 @@ export interface ProjectInstructionSource { trusted: boolean /** Workspace-relative directory governed by a nested AGENTS.md. */ scopePath?: string + /** + * Name of the earlier source whose content this nested file repeats. The + * content is injected once, through that source; this entry stays listed so + * Sources does not silently hide a file the workspace ships. + */ + duplicateOf?: string + /** + * True on project sources when the nested walk stopped at a directory, file, + * or depth cap, so the listed nested files may be incomplete. + */ + discoveryTruncated?: boolean } interface NestedInstructionSource { @@ -80,24 +97,82 @@ interface NestedInstructionSource { scopePath: string } +/** One walk of the execution root for nested AGENTS.md files. */ +export interface NestedInstructionDiscovery { + sources: NestedInstructionSource[] + /** Directories visited. */ + directories: number + /** The walk stopped at a cap before covering the whole tree. */ + truncated: boolean +} + interface NestedDiscoveryState { directories: number sources: NestedInstructionSource[] + truncated: boolean +} + +/** + * Per-turn discovery memo. The turn walks the tree once — at turn start, when + * the system prompt is assembled — and every tool call of that turn reuses the + * result instead of re-walking up to {@link MAX_NESTED_DISCOVERY_DIRECTORIES} + * directories per call. A write to an AGENTS.md invalidates the memo so the + * next call sees the file the agent just created or changed. + */ +export interface NestedInstructionTurn { + /** Resolved execution root → the walk (shared by concurrent tool calls). */ + readonly discoveries: Map> +} + +export function createNestedInstructionTurn(): NestedInstructionTurn { + return { discoveries: new Map() } +} + +interface NestedActivationRecord { + names: ReadonlySet + at: number } /** * Most recently assembled real-turn activation, for Settings → Sources. * - * Store workspace-relative source names under the stable project root. A turn - * may execute in an isolated worktree while Settings reads the primary project; - * absolute execution-root paths would never compare equal across that boundary. + * Keyed by the stable project root, then by thread: concurrent turns on + * different threads of one project must not overwrite each other's record. + * Settings has no thread of its own, so it reads the most recently updated + * thread. Names are workspace-relative because a turn may execute in an + * isolated worktree while Settings reads the primary project; absolute + * execution-root paths would never compare equal across that boundary. */ -const lastNestedActivationByProjectRoot = new Map>() +const lastNestedActivationByProjectRoot = new Map>() const nestedDiscoveryCache = new Map< string, - { expiresAt: number; sources: NestedInstructionSource[] } + { expiresAt: number; discovery: NestedInstructionDiscovery } >() +function activationThreadKey(): string { + return getThreadExecutionContext()?.threadId ?? '' +} + +function latestNestedActivation(projectKey: string): ReadonlySet { + const byThread = lastNestedActivationByProjectRoot.get(projectKey) + if (!byThread) return new Set() + const threadKey = activationThreadKey() + const own = threadKey ? byThread.get(threadKey) : undefined + if (own) return own.names + let latest: NestedActivationRecord | undefined + for (const record of byThread.values()) { + if (!latest || record.at > latest.at) latest = record + } + return latest?.names ?? new Set() +} + +function recordNestedActivation(projectKey: string, names: ReadonlySet): void { + const byThread = + lastNestedActivationByProjectRoot.get(projectKey) ?? new Map() + byThread.set(activationThreadKey(), { names, at: Date.now() }) + lastNestedActivationByProjectRoot.set(projectKey, byThread) +} + async function readTrimmed(path: string): Promise { try { const content = (await fsp.readFile(path, 'utf-8')).trim() @@ -167,6 +242,7 @@ async function walkNestedInstructionFiles( state.directories >= MAX_NESTED_DISCOVERY_DIRECTORIES || state.sources.length >= MAX_NESTED_DISCOVERED_FILES ) { + state.truncated = true return } state.directories += 1 @@ -200,32 +276,83 @@ async function walkNestedInstructionFiles( } for (const entry of entries) { + if (!entry.isDirectory() || NESTED_SKIP_DIRS.has(entry.name)) continue if ( state.directories >= MAX_NESTED_DISCOVERY_DIRECTORIES || state.sources.length >= MAX_NESTED_DISCOVERED_FILES ) { + state.truncated = true break } - if (!entry.isDirectory() || NESTED_SKIP_DIRS.has(entry.name)) continue await walkNestedInstructionFiles(root, join(dir, entry.name), depth + 1, state) } } +async function walkNestedInstructionTree( + key: string, + root: string, +): Promise { + const state: NestedDiscoveryState = { directories: 0, sources: [], truncated: false } + await walkNestedInstructionFiles(root, root, 0, state) + const discovery: NestedInstructionDiscovery = { + sources: state.sources.sort((a, b) => a.name.localeCompare(b.name)), + directories: state.directories, + truncated: state.truncated, + } + if (discovery.truncated) { + console.warn( + `[instructions] nested AGENTS.md discovery under ${root} stopped early ` + + `(${String(discovery.directories)} directories, ${String(discovery.sources.length)} files): ` + + 'nested instruction files beyond the cap are not loaded.', + ) + } + nestedDiscoveryCache.set(key, { expiresAt: Date.now() + NESTED_DISCOVERY_CACHE_MS, discovery }) + return discovery +} + +interface NestedDiscoveryOptions { + /** Walk once per turn; later calls of the same turn reuse the memo. */ + turn?: NestedInstructionTurn | undefined + /** Explicit reload (Settings → Sources): ignore any cached walk. */ + refresh?: boolean | undefined +} + async function discoverNestedInstructionSources( root: string, - refresh = false, -): Promise { + opts: NestedDiscoveryOptions = {}, +): Promise { const key = resolve(root) + if (opts.turn) { + const memo = opts.turn.discoveries.get(key) + if (memo) return memo + // The turn's first look is always a fresh walk: an AGENTS.md added between + // turns must apply to this one, whatever an estimate cached moments ago. + const walk = walkNestedInstructionTree(key, root) + opts.turn.discoveries.set(key, walk) + return walk + } const cached = nestedDiscoveryCache.get(key) - if (!refresh && cached && cached.expiresAt > Date.now()) return cached.sources - const state: NestedDiscoveryState = { directories: 0, sources: [] } - await walkNestedInstructionFiles(root, root, 0, state) - const sources = state.sources.sort((a, b) => a.name.localeCompare(b.name)) - nestedDiscoveryCache.set(key, { - expiresAt: Date.now() + NESTED_DISCOVERY_CACHE_MS, - sources, - }) - return sources + if (!opts.refresh && cached && cached.expiresAt > Date.now()) return cached.discovery + return walkNestedInstructionTree(key, root) +} + +/** + * Forget the current execution root's discovery after the agent wrote, moved, + * or removed a nested AGENTS.md, so the next tool call re-walks and sees it. + * Only file paths named AGENTS.md count; other writes keep the memo. Returns + * whether anything was invalidated. + */ +export function invalidateNestedInstructionDiscoveryForWrite( + writtenPaths: readonly string[], + turn?: NestedInstructionTurn, +): boolean { + if (!writtenPaths.some((path) => basename(path.trim()) === NESTED_INSTRUCTION_FILE)) return false + const root = getAgentExecutionRoot() + if (!root) return false + const key = resolve(root) + nestedDiscoveryCache.delete(key) + turn?.discoveries.delete(key) + return true } /** Resolve a context path without letting a symlinked ancestor escape the workspace. */ @@ -298,19 +425,36 @@ async function selectNestedInstructionSources( return retained.sort(instructionPrecedence) } +/** + * Identical content is injected once, through the earliest source in precedence + * order. A root or global duplicate is dropped from the list (the same text is + * already listed under its higher-precedence name); a nested duplicate stays + * listed and is marked, so Sources shows every directory-scoped file the + * workspace ships rather than hiding one because it repeats the root rules. + */ function deduplicateSources(resolved: ProjectInstructionSource[]): ProjectInstructionSource[] { const sources: ProjectInstructionSource[] = [] const contentIndexes = new Map() for (const source of resolved) { const existingIndex = contentIndexes.get(source.content) - if (existingIndex === undefined) { + const existing = existingIndex === undefined ? undefined : sources[existingIndex] + if (existingIndex === undefined || !existing) { contentIndexes.set(source.content, sources.length) sources.push(source) continue } - const existing = sources[existingIndex] - // An inactive nested sibling must not shadow the identical source that matched this turn. - if (existing && !existing.active && source.active) sources[existingIndex] = source + if (source.scopePath === undefined) continue + // An inactive nested sibling must not shadow the identical nested source + // that matched this turn: the active copy carries the content, the other + // one is the duplicate. (A root or global copy is never inactive while a + // nested one is active — both sit behind the same trust gate.) + if (existing.scopePath !== undefined && !existing.active && source.active) { + sources[existingIndex] = { ...existing, duplicateOf: source.name } + contentIndexes.set(source.content, sources.length) + sources.push(source) + continue + } + sources.push({ ...source, duplicateOf: existing.name }) } return sources } @@ -322,8 +466,10 @@ export interface ProjectInstructionOptions { nestedContextPaths?: readonly string[] /** Settings-only: report the most recently assembled real turn's activation. */ useLatestNestedActivation?: boolean - /** Explicit Sources reload bypasses the short prompt-build discovery cache. */ + /** Explicit Sources reload bypasses the cached discovery. */ refreshNestedDiscovery?: boolean + /** The running turn's discovery memo; the turn-start walk seeds it. */ + nestedInstructionTurn?: NestedInstructionTurn } /** Discover instruction sources, global layer first then project. */ @@ -367,7 +513,11 @@ export async function loadProjectInstructionSources( } } - const nested = await discoverNestedInstructionSources(root, opts.refreshNestedDiscovery) + const discovery = await discoverNestedInstructionSources(root, { + turn: opts.nestedInstructionTurn, + refresh: opts.refreshNestedDiscovery, + }) + const nested = discovery.sources const explicitlyActive = opts.nestedContextPaths !== undefined ? new Set( @@ -377,8 +527,7 @@ export async function loadProjectInstructionSources( ) : null const latestActive = opts.useLatestNestedActivation - ? (lastNestedActivationByProjectRoot.get(await canonicalPath(projectRoot)) ?? - new Set()) + ? latestNestedActivation(await canonicalPath(projectRoot)) : new Set() for (const source of nested) { resolved.push({ @@ -388,6 +537,14 @@ export async function loadProjectInstructionSources( trusted, }) } + if (discovery.truncated) { + // Every project row carries the flag, root files included: with no nested + // file found before the cap, they are the only rows that can say the list + // is short. + for (const source of resolved) { + if (source.scope === 'project') source.discoveryTruncated = true + } + } // Cursor project rules are applied after AGENTS.md layers. for (const rule of await loadCursorRuleSources(root, opts.cursorRuleContext ?? {})) { @@ -467,15 +624,18 @@ export async function loadInstructionLayersWithMetadata( .map((source) => source.content) .join('\n\n') const project = sources.filter((source) => source.scope === 'project') - const activeProject = project.filter((source) => source.active) - const activeNestedPaths = project - .filter((source) => source.active && source.scopePath !== undefined) - .map((source) => source.path) + // A marked duplicate is listed, not injected: its content already rides on + // the source it duplicates. + const activeProject = project.filter( + (source) => source.active && source.duplicateOf === undefined, + ) + const activeNested = activeProject.filter((source) => source.scopePath !== undefined) + const activeNestedPaths = activeNested.map((source) => source.path) const root = getAgentExecutionRoot() const projectRoot = getAgentProjectRoot() if (trackActivation && root && projectRoot) { - lastNestedActivationByProjectRoot.set( + recordNestedActivation( await canonicalPath(projectRoot), new Set( project @@ -490,9 +650,10 @@ export async function loadInstructionLayersWithMetadata( activeInstructionContents: sources .filter((source) => source.active) .map((source) => source.content), - activeNestedBytes: project - .filter((source) => source.active && source.scopePath !== undefined) - .reduce((total, source) => total + Buffer.byteLength(source.content, 'utf-8'), 0), + activeNestedBytes: activeNested.reduce( + (total, source) => total + Buffer.byteLength(source.content, 'utf-8'), + 0, + ), } if (activeProject.length > 0) { return { project: buildProjectBlock(activeProject), global, metadata } @@ -515,25 +676,38 @@ export interface NestedInstructionActivation { block: string activatedPaths: string[] injectedPaths: string[] + /** Workspace-relative names of `injectedPaths`, for the transcript notice. */ + injectedNames: string[] injectedContents: string[] } -/** Activate instructions for a file tool that introduced a path after turn start. */ +const NO_ACTIVATION: NestedInstructionActivation = { + block: '', + activatedPaths: [], + injectedPaths: [], + injectedNames: [], + injectedContents: [], +} + +/** + * Activate instructions for a file tool that introduced a path after turn + * start. Pass the turn's memo so the call reuses the turn-start walk; without + * one the shared cache serves the request. + */ export async function activateNestedInstructionSources( contextPaths: readonly string[], alreadyActivePaths: ReadonlySet, alreadyActiveContents: ReadonlySet, alreadyActiveNestedBytes: number, + turn?: NestedInstructionTurn, ): Promise { const root = getAgentExecutionRoot() const projectRoot = getAgentProjectRoot() - if (!root || !projectRoot || !isWorkspaceTrusted(projectRoot)) { - return { block: '', activatedPaths: [], injectedPaths: [], injectedContents: [] } - } + if (!root || !projectRoot || !isWorkspaceTrusted(projectRoot)) return NO_ACTIVATION const selected = await selectNestedInstructionSources( root, - await discoverNestedInstructionSources(root), + (await discoverNestedInstructionSources(root, { turn })).sources, contextPaths, ) const candidates = selected.filter( @@ -565,15 +739,13 @@ export async function activateNestedInstructionSources( .map((source) => source.path) const activatedPathSet = new Set(activatedPaths) const projectKey = await canonicalPath(projectRoot) - const latest = new Set(lastNestedActivationByProjectRoot.get(projectKey) ?? []) + const latest = new Set(latestNestedActivation(projectKey)) for (const source of selected) { if (activatedPathSet.has(source.path)) latest.add(source.name) } - lastNestedActivationByProjectRoot.set(projectKey, latest) + recordNestedActivation(projectKey, latest) - if (fresh.length === 0) { - return { block: '', activatedPaths, injectedPaths: [], injectedContents: [] } - } + if (fresh.length === 0) return { ...NO_ACTIVATION, activatedPaths } const promptSources: ProjectInstructionSource[] = fresh.map((source) => ({ ...source, scope: 'project', @@ -584,6 +756,7 @@ export async function activateNestedInstructionSources( block: buildProjectBlock(promptSources), activatedPaths, injectedPaths, + injectedNames: fresh.map((source) => source.name), injectedContents: fresh.map((source) => source.content), } } From f44e2290cd93a3c1c4edb36cf763dd612d8bdc5c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 10:04:17 +0000 Subject: [PATCH 3/5] feat(settings): show nested discovery truncation and duplicates in Sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `instructions:list` passes `duplicateOf` and `discoveryTruncated` through to the renderer. Sources → Instruction files marks a nested file whose text repeats a listed one with a quiet "duplicate" badge and says which file carries it, and appends a single note under the list when nested discovery stopped at its cap, so a missing deep file no longer looks unwritten. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011jaQbed96h8aSZLijfiHNB --- docs/ui-taste.md | 7 ++-- src/main/ipc/register-handlers.ts | 32 ++++++++++++----- src/renderer/views/settings-dialog.ts | 32 ++++++++++++----- .../settings-sources-instructions.test.ts | 36 ++++++++++++++++++- src/shared/types/instructions.ts | 4 +++ 5 files changed, 90 insertions(+), 21 deletions(-) diff --git a/docs/ui-taste.md b/docs/ui-taste.md index cd3514584b..64d8c3b459 100644 --- a/docs/ui-taste.md +++ b/docs/ui-taste.md @@ -746,8 +746,11 @@ the leaf stays visible; mirror the full path on the row's `title` for the native Nested instruction rows use the badge as activation state, not origin: **active** uses the existing accent outline when the latest turn selected that scope; **scoped** uses the quiet default outline when -the file is available but unrelated to that turn. Keep the governed directory and the explanatory -state in the detail line so sibling scopes are understandable without adding another row of chips. +the file is available but unrelated to that turn; **duplicate** (quiet outline) marks a nested file +whose text repeats one already listed, so it is loaded once through that one. Keep the governed +directory and the explanatory state in the detail line so sibling scopes are understandable without +adding another row of chips. When discovery stopped at its cap, a single `.sources-empty` line under +the list says the list may be incomplete — a note, not a row. Spec: [`tests/e2e/settings-sources-nested-instructions.e2e.ts`](../tests/e2e/settings-sources-nested-instructions.e2e.ts). ## Prove visual changes with a focused e2e eval diff --git a/src/main/ipc/register-handlers.ts b/src/main/ipc/register-handlers.ts index 6d7a2a2a23..4d6e626897 100644 --- a/src/main/ipc/register-handlers.ts +++ b/src/main/ipc/register-handlers.ts @@ -2147,15 +2147,29 @@ export function registerAllHandlers(win: BrowserWindow, registry: ToolRegistry): useLatestNestedActivation: true, refreshNestedDiscovery: true, }) - ).map(({ path, name, scope, content, active, trusted, scopePath }) => ({ - path, - name, - scope, - bytes: Buffer.byteLength(content, 'utf-8'), - active, - trusted, - ...(scopePath !== undefined ? { scopePath } : {}), - })), + ).map( + ({ + path, + name, + scope, + content, + active, + trusted, + scopePath, + duplicateOf, + discoveryTruncated, + }) => ({ + path, + name, + scope, + bytes: Buffer.byteLength(content, 'utf-8'), + active, + trusted, + ...(scopePath !== undefined ? { scopePath } : {}), + ...(duplicateOf !== undefined ? { duplicateOf } : {}), + ...(discoveryTruncated ? { discoveryTruncated } : {}), + }), + ), ) /** * Read one instruction file for display (Settings → Sources opens it in the diff --git a/src/renderer/views/settings-dialog.ts b/src/renderer/views/settings-dialog.ts index 7998724158..6023b0f475 100644 --- a/src/renderer/views/settings-dialog.ts +++ b/src/renderer/views/settings-dialog.ts @@ -2665,9 +2665,11 @@ export function mountSettingsDialog(store: AppStore, api: ApiClient): void { const nestedStatus = file.scopePath === undefined ? '' - : file.active - ? ` · scope: ${file.scopePath}/ · active this turn` - : ` · scope: ${file.scopePath}/ · activates when a path under this directory enters context` + : file.duplicateOf !== undefined + ? ` · scope: ${file.scopePath}/ · identical to ${file.duplicateOf}, loaded once through it` + : file.active + ? ` · scope: ${file.scopePath}/ · active this turn` + : ` · scope: ${file.scopePath}/ · activates when a path under this directory enters context` const detail = `${file.path} · ${formatByteSize(file.bytes)}` + (file.trusted @@ -2675,16 +2677,18 @@ export function mountSettingsDialog(store: AppStore, api: ApiClient): void { : ' · inert until you trust this workspace — click the badge to trust it') const badge = !file.trusted ? 'not loaded' - : file.scopePath !== undefined - ? file.active - ? 'active' - : 'scoped' - : file.scope + : file.duplicateOf !== undefined + ? 'duplicate' + : file.scopePath !== undefined + ? file.active + ? 'active' + : 'scoped' + : file.scope const row = makeSourceRow(file.name, badge, detail, { badgeClass: !file.trusted ? 'sources-badge-untrusted' : file.scopePath !== undefined - ? file.active + ? file.active && file.duplicateOf === undefined ? 'sources-badge-auto' : undefined : file.scope === 'project' @@ -2732,6 +2736,16 @@ export function mountSettingsDialog(store: AppStore, api: ApiClient): void { instructions.map((f) => makeInstructionRow(f)), 'No instruction files (add AGENT.md, AGENTS.md, or CLAUDE.md to the workspace root; nested directories may add AGENTS.md; or add ~/AGENTS.md globally).', ) + // Discovery is bounded; say so rather than let a missing nested file + // look like it was never written. + if (instructions.some((f) => f.discoveryTruncated)) { + const note = document.createElement('span') + note.className = 'sources-empty' + note.id = 'sources-instructions-truncated' + note.textContent = + 'Nested AGENTS.md discovery stopped at its directory limit, so this list may be incomplete. Deeper files are not loaded.' + qsRequired(overlay, '#sources-instructions-list').append(note) + } const kindLabel: Record = { always: 'always', diff --git a/src/renderer/views/settings-sources-instructions.test.ts b/src/renderer/views/settings-sources-instructions.test.ts index 42bca715f7..2e4a499c98 100644 --- a/src/renderer/views/settings-sources-instructions.test.ts +++ b/src/renderer/views/settings-sources-instructions.test.ts @@ -28,7 +28,12 @@ function untrusted(): ProjectInstructionSummary { } } -function nested(name: string, scopePath: string, active: boolean): ProjectInstructionSummary { +function nested( + name: string, + scopePath: string, + active: boolean, + extra: Partial = {}, +): ProjectInstructionSummary { return { path: `/workspace/${name}`, name, @@ -37,6 +42,7 @@ function nested(name: string, scopePath: string, active: boolean): ProjectInstru active, trusted: true, scopePath, + ...extra, } } @@ -143,6 +149,34 @@ describe('settings sources → instructions', () => { ) }) + it('keeps a nested duplicate of the root rules listed, marked rather than hidden', async () => { + const list = await openInstructions( + stubApi([ + nested('packages/api/AGENTS.md', 'packages/api', true, { duplicateOf: 'AGENTS.md' }), + ]).api, + ) + const row = list.querySelector('.sources-row') + assert.ok(row) + assert.equal(row.querySelector('.sources-badge')?.textContent, 'duplicate') + assert.match( + row.querySelector('.sources-row-detail')?.textContent ?? '', + /identical to AGENTS\.md, loaded once through it/, + ) + assert.equal(list.querySelector('#sources-instructions-truncated'), null) + }) + + it('says when nested discovery stopped short instead of listing silently', async () => { + const list = await openInstructions( + stubApi([ + nested('packages/api/AGENTS.md', 'packages/api', false, { discoveryTruncated: true }), + ]).api, + ) + assert.match( + list.querySelector('#sources-instructions-truncated')?.textContent ?? '', + /this list may be incomplete/, + ) + }) + it('points at the badge as the way to trust an inert file', async () => { const list = await openInstructions(stubApi([untrusted()]).api) const badge = trustBadge(list) diff --git a/src/shared/types/instructions.ts b/src/shared/types/instructions.ts index 585780a8f6..66d766669e 100644 --- a/src/shared/types/instructions.ts +++ b/src/shared/types/instructions.ts @@ -17,4 +17,8 @@ export interface ProjectInstructionSummary { trusted: boolean /** Workspace-relative directory governed by a nested AGENTS.md. */ scopePath?: string + /** Name of the listed source this nested file repeats; its text is loaded once, via that one. */ + duplicateOf?: string + /** Nested discovery stopped at a cap, so nested files may be missing from the list. */ + discoveryTruncated?: boolean } From cc4252e7f8b76537e79fe5b5dbc27a5f83ca84e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 10:04:17 +0000 Subject: [PATCH 4/5] docs(user): narrow the nested instructions activation claim Name the built-in file tools that bring a path into context and say that `run_shell` writes, ACP agents, and subagent reads do not activate a nested AGENTS.md. Describe the once-per-turn discovery, the re-walk after the agent writes an AGENTS.md, the transcript notice, the duplicate badge, and the truncation note in Sources. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011jaQbed96h8aSZLijfiHNB --- docs/user/project-instructions.md | 32 +++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/docs/user/project-instructions.md b/docs/user/project-instructions.md index 4dbf03e672..ec0b047137 100644 --- a/docs/user/project-instructions.md +++ b/docs/user/project-instructions.md @@ -22,21 +22,33 @@ the project layer as user-owned guidance. ## Directory-scoped instructions -A nested `AGENTS.md` applies only when a path under its directory enters the turn's context. That -can happen because the prompt or an attachment names a file, or because the agent reads, searches, -creates, moves, or edits a path during the turn. Instructions are applied from the project root -toward the target directory, so the nearest file appears last and can refine broader conventions. -Sibling scopes stay inactive unless that sibling has a relevant path. +A nested `AGENTS.md` applies only when a path under its directory enters the turn's context. A +path enters context when the prompt or an attachment names it, or when one of Copse's built-in +file tools touches it: `read_file`, `list_dir`, `search_code`, `search_codebase`, +`read_staged_diff`, `write_file`, `str_replace`, `delete_file`, `rename_file`, and +`make_directory`. Nothing else activates a nested file: a `run_shell` command that reads or writes +under the directory, an ACP agent's own file access, and a subagent's reads do not count. +Instructions are applied from the project root toward the target directory, so the nearest file +appears last and can refine broader conventions. Sibling scopes stay inactive unless that sibling +has a relevant path. When an edit tool is the first action to enter a new scope, Copse loads the applicable instruction chain and defers that edit once. The agent sees the new rules and retries instead of changing the -file before its local guidance is available. +file before its local guidance is available. Each activation adds a one-line note to the +transcript naming the file that was loaded. + +Copse looks for nested files once per turn, when it assembles the prompt, and reuses that result +for every tool call of the turn. A file the agent itself writes, moves, or removes with a file tool +is picked up straight away: writing an `AGENTS.md` makes the next file tool call look again. A file +that appears by any other route — a shell command, an external editor — is seen by the next turn. Nested `AGENT.md` and `CLAUDE.md` remain root-only compatibility formats. Only `AGENTS.md` follows the cross-client directory-scoping convention, which avoids silently changing the meaning of vendor-specific files. -Sources marks a nested file **active** when the latest turn used it and **scoped** when it is -available but did not apply. Discovery skips dependency, generated, vendored, cache, nested-repo, -and VCS trees; it does not follow a symlink outside the trusted workspace. Very deep or unusually -large instruction trees are bounded so they cannot consume the whole prompt. +Sources marks a nested file **active** when the latest turn used it, **scoped** when it is +available but did not apply, and **duplicate** when its text repeats a file already listed (the +text is loaded once, through that file). Discovery skips dependency, generated, vendored, cache, +nested-repo, and VCS trees; it does not follow a symlink outside the trusted workspace. Very deep +or unusually large instruction trees are bounded so they cannot consume the whole prompt; when +discovery stops at that bound, Sources says the list may be incomplete. From d61678f601a1dac57047d5b6845dcf2f121148ac Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 20:46:50 +0000 Subject: [PATCH 5/5] build(protocol): bump API_PROTOCOL_VERSION for the nested-instructions shape `ProjectInstructionSummary` gains a required `trusted: boolean` alongside the optional `scopePath` / `duplicateOf` / `discoveryTruncated`. A new *optional* result field would have been additive, but a required one is not, so `gen-api-protocol.mts --compare-ref` classifies `instructions:list` and `client.instructions.list` as breaking and fails the gate at version 2. The gate did not exist when this branch forked; it arrived on `main` with the protocol freeze, so this is the first run that has had to answer for the shape change. Bumping is the right side of the trade: `trusted` is a real property of every instruction source, and weakening it to optional purely to stay additive would put a lie in the type. The manifest is regenerated with `gen:api-protocol`, not edited: only its `version` moves, because the manifest records channel arity and binding names, which this change leaves alone. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BCcGaKPRL3fmRpLR3rK5WX --- schemas/api-protocol.manifest.json | 2 +- src/shared/api-protocol.mts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/schemas/api-protocol.manifest.json b/schemas/api-protocol.manifest.json index 3786da0ab4..9f9613417a 100644 --- a/schemas/api-protocol.manifest.json +++ b/schemas/api-protocol.manifest.json @@ -1,6 +1,6 @@ { "$comment": "Generated by scripts/gen-api-protocol.mts from ApiClient (src/preload/api.d.ts) and the preload bindings; do not edit by hand. The full JSON Schema is a build output (dist/schemas/api-protocol.schema.json). See docs/api-protocol.md.", - "version": 2, + "version": 3, "channels": { "invoke": { "acp:auto-setup": { diff --git a/src/shared/api-protocol.mts b/src/shared/api-protocol.mts index 64b007500d..af22a72ecc 100644 --- a/src/shared/api-protocol.mts +++ b/src/shared/api-protocol.mts @@ -20,4 +20,4 @@ * handshake and refuses a peer that speaks a different one rather than letting * mismatched shapes reach the handler table. */ -export const API_PROTOCOL_VERSION = 2 as const +export const API_PROTOCOL_VERSION = 3 as const