diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5a56637..316bbad 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -124,7 +124,7 @@ Projects are the container for context. There are two kinds: The scratchpad is modeled inside the same workspace system instead of as a separate subsystem. That keeps the UI and session model consistent while still allowing special rules for scratchpad behavior. Each scratchpad session receives its own working directory under the shared scratchpad root, so session-created files stay isolated from other scratchpad conversations. -Project-backed entries also persist scanned Copilot customization metadata discovered from repository files such as `.github/copilot-instructions.md`, `AGENTS.md`, `.github/agents/*.agent.md`, and `.github/prompts/*.prompt.md`. The main process owns that scan step and stores the normalized results on the project record so repo instructions and enabled custom agent profiles can participate in later run execution without turning the renderer into a filesystem crawler. +Project-backed entries also persist scanned Copilot customization metadata discovered from repository files such as `.github/copilot-instructions.md`, `AGENTS.md`, `CLAUDE.md`, `.claude/CLAUDE.md`, recursive `.github/instructions/**/*.instructions.md`, recursive `.github/agents/**/*.agent.md`, and recursive `.github/prompts/**/*.prompt.md`. The main process owns that scan step, strips Markdown front matter where supported, classifies instruction files by application mode (`always`, `file`, `task`, `manual`), and stores the normalized results on the project record so repo instructions and enabled custom agent profiles can participate in later run execution without turning the renderer into a filesystem crawler. For git-backed projects, the main process also owns background git refreshes, captures a structured pre-run working-tree snapshot on each run record, and persists a post-run git change summary after project-backed turns complete. It also owns all git write operations exposed by Aryx — selective discard, staging, commit, push/pull/fetch, and branch lifecycle actions — so the renderer never shells out directly or manipulates repository state on its own. @@ -235,7 +235,7 @@ The same boundary also supports server-scoped sidecar commands that do not requi For project-backed sessions, the sidecar also discovers GitHub Copilot CLI hook definitions from `.github/hooks/*.json` under the repository root. Those files are parsed and merged once per run bundle, then projected onto the SDK session hook delegates. Hook commands run synchronously in the sidecar through the platform shell, with stdin JSON payloads shaped to match Copilot CLI hook expectations as closely as the SDK allows. Hook failures are logged to stderr and treated as non-fatal diagnostics, while `preToolUse` hook outputs can still deny a tool call before Aryx falls back to its built-in approval policy. -The `run-turn` command now also carries a project-instruction payload derived from scanned repo customization files. The main process composes that payload from repo-level instruction files and merges enabled discovered custom agent profiles into the primary pattern agent's Copilot configuration before sending the command across the stdio boundary. The sidecar then folds those project instructions into the final SDK system message alongside the agent's own instructions and runtime guidance. +The `run-turn` command now also carries a project-instruction payload derived from scanned repo customization files. The main process composes that payload from always-on repo instructions plus formatted file-scoped and task-scoped `.instructions.md` entries, while omitting manual-only instruction files from automatic injection. It also merges enabled discovered custom agent profiles into the primary pattern agent's Copilot configuration before sending the command across the stdio boundary. The sidecar then folds those project instructions into the final SDK system message alongside the agent's own instructions and runtime guidance. For handoff workflows, the sidecar now also enables Agent Framework JSON checkpointing backed by a per-turn filesystem store under local app data. Each saved checkpoint is surfaced to the main process, which pairs the durable Agent Framework checkpoint with an in-memory rollback snapshot of `session.messages` and the active run timeline events. If the sidecar child process exits unexpectedly during the same app lifetime, Aryx restores the latest snapshot, clears pending approval/user-input/MCP-auth state for that run, and retries the `run-turn` request once with `resumeFromCheckpoint`. Checkpoint directories are deleted after the turn completes, cancels, or fails. This recovery path is intentionally scoped to same-app sidecar restarts; full app-restart workflow rehydration would require durable rollback snapshots in addition to the Agent Framework checkpoint payloads. diff --git a/README.md b/README.md index 998b884..a705ff2 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Aryx is a desktop app that turns GitHub Copilot into a full workspace. Connect r | Mid-turn steering | Send follow-up messages while an agent is running — input is injected immediately | | Plan review & questions | Agents propose plans and ask clarifying questions before acting | | Run timeline | Structured history of tool calls, delegations, hooks, and context usage | -| Copilot customization | Auto-discovers instructions, agent profiles, and prompt files from your repo | +| Copilot customization | Auto-discovers repo instructions (`copilot-instructions.md`, `AGENTS.md`, `CLAUDE.md`, `.instructions.md`), agent profiles, and prompt files from your repo | | Model & effort tuning | Choose models, adjust reasoning effort, and set interaction modes per session | ### Developer tooling diff --git a/src/main/services/customizationScanner.ts b/src/main/services/customizationScanner.ts index 7513f09..bf9c4d1 100644 --- a/src/main/services/customizationScanner.ts +++ b/src/main/services/customizationScanner.ts @@ -7,6 +7,7 @@ import { mergeProjectCustomizationState, normalizeProjectCustomizationState, type ProjectAgentProfile, + type ProjectInstructionApplicationMode, type ProjectCustomizationState, type ProjectInstructionFile, type ProjectPromptFile, @@ -42,34 +43,29 @@ export class ProjectCustomizationScanner { previous: ProjectCustomizationState, ): Promise { const previousByPath = new Map(previous.instructions.map((instruction) => [instruction.sourcePath, instruction])); - const sourcePaths = ['.github\\copilot-instructions.md', 'AGENTS.md'] as const; + const sourcePaths = [ + '.github\\copilot-instructions.md', + 'AGENTS.md', + 'CLAUDE.md', + '.claude\\CLAUDE.md', + ] as const; const instructions: ProjectInstructionFile[] = []; for (const sourcePath of sourcePaths) { const filePath = join(projectPath, ...sourcePath.split('\\')); - const contents = await this.readProjectFile(filePath); - if (contents.kind === 'missing') { - continue; + const instruction = await this.scanInstructionFile(filePath, sourcePath, previousByPath, 'always'); + if (instruction) { + instructions.push(instruction); } + } - if (contents.kind === 'retain-previous') { - const existing = previousByPath.get(sourcePath); - if (existing) { - instructions.push(existing); - } - continue; + const instructionFilePaths = await this.listProjectFiles(join(projectPath, '.github', 'instructions'), '.instructions.md'); + for (const filePath of instructionFilePaths) { + const sourcePath = toProjectSourcePath(projectPath, filePath); + const instruction = await this.scanInstructionFile(filePath, sourcePath, previousByPath); + if (instruction) { + instructions.push(instruction); } - - const content = contents.value.trim(); - if (!content) { - continue; - } - - instructions.push({ - id: buildProjectCustomizationItemId('instruction', sourcePath), - sourcePath, - content, - }); } return instructions; @@ -170,7 +166,7 @@ export class ProjectCustomizationScanner { promptFiles.push({ id: buildProjectCustomizationItemId('prompt', sourcePath), - name: basename(filePath, '.prompt.md'), + name: readOptionalString(parsedFile.attributes, ['name']) ?? basename(filePath, '.prompt.md'), description: readOptionalString(parsedFile.attributes, ['description']), agent: readOptionalString(parsedFile.attributes, ['agent']), template, @@ -183,19 +179,31 @@ export class ProjectCustomizationScanner { } private async listProjectFiles(directoryPath: string, suffix: string): Promise { + const filePaths: string[] = []; + await this.collectProjectFiles(directoryPath, suffix, filePaths); + return filePaths.sort((left, right) => left.localeCompare(right)); + } + + private async collectProjectFiles(directoryPath: string, suffix: string, filePaths: string[]): Promise { try { const entries = await readdir(directoryPath, { withFileTypes: true }); - return entries - .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(suffix)) - .map((entry) => join(directoryPath, entry.name)) - .sort((left, right) => left.localeCompare(right)); + for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { + const entryPath = join(directoryPath, entry.name); + if (entry.isDirectory()) { + await this.collectProjectFiles(entryPath, suffix, filePaths); + continue; + } + + if (entry.isFile() && entry.name.toLowerCase().endsWith(suffix)) { + filePaths.push(entryPath); + } + } } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - return []; + return; } console.warn(`[aryx customization] Failed to read directory ${directoryPath}:`, error); - return []; } } @@ -218,6 +226,56 @@ export class ProjectCustomizationScanner { return { kind: 'retain-previous' }; } } + + private async scanInstructionFile( + filePath: string, + sourcePath: string, + previousByPath: ReadonlyMap, + applicationMode?: ProjectInstructionApplicationMode, + ): Promise { + const contents = await this.readProjectFile(filePath); + if (contents.kind === 'missing') { + return undefined; + } + + if (contents.kind === 'retain-previous') { + return previousByPath.get(sourcePath); + } + + const parsedFile = parseProjectFrontmatter(contents.value, sourcePath); + if (!parsedFile) { + return previousByPath.get(sourcePath); + } + + const content = parsedFile.body.trim(); + if (!content) { + return undefined; + } + + const description = readOptionalString(parsedFile.attributes, ['description']); + const applyTo = readOptionalString(parsedFile.attributes, ['applyTo']); + const instruction: ProjectInstructionFile = { + id: buildProjectCustomizationItemId('instruction', sourcePath), + sourcePath, + content, + applicationMode: applicationMode ?? resolveInstructionApplicationMode(applyTo, description), + }; + + const name = readOptionalString(parsedFile.attributes, ['name']); + if (name) { + instruction.name = name; + } + + if (description) { + instruction.description = description; + } + + if (applyTo) { + instruction.applyTo = applyTo; + } + + return instruction; + } } function parseProjectFrontmatter( @@ -309,6 +367,25 @@ function readOptionalString( return undefined; } +function resolveInstructionApplicationMode( + applyTo: string | undefined, + description: string | undefined, +): ProjectInstructionApplicationMode { + if (isMatchAllInstructionGlob(applyTo)) { + return 'always'; + } + + if (applyTo) { + return 'file'; + } + + if (description) { + return 'task'; + } + + return 'manual'; +} + function readOptionalStringArray(value: unknown): string[] | undefined { if (value === undefined) { return undefined; @@ -368,3 +445,12 @@ function normalizeYamlValue(value: unknown): unknown { function isPlainObject(value: unknown): value is Record { return !!value && typeof value === 'object' && !Array.isArray(value); } + +function isMatchAllInstructionGlob(value: string | undefined): boolean { + if (!value) { + return false; + } + + const normalizedValue = value.trim().replaceAll('\\', '/'); + return normalizedValue === '**' || normalizedValue === '**/*'; +} diff --git a/src/shared/domain/projectCustomization.ts b/src/shared/domain/projectCustomization.ts index 1c0e436..da28f05 100644 --- a/src/shared/domain/projectCustomization.ts +++ b/src/shared/domain/projectCustomization.ts @@ -1,7 +1,13 @@ +export type ProjectInstructionApplicationMode = 'always' | 'file' | 'task' | 'manual'; + export interface ProjectInstructionFile { id: string; sourcePath: string; content: string; + name?: string; + description?: string; + applyTo?: string; + applicationMode: ProjectInstructionApplicationMode; } export interface ProjectAgentProfileMcpServerConfig { @@ -111,8 +117,34 @@ export function listEnabledProjectAgentProfiles( export function resolveProjectInstructionsContent( state?: Partial, ): string | undefined { - const content = normalizeProjectCustomizationState(state).instructions - .map((instruction) => instruction.content) + const instructions = normalizeProjectCustomizationState(state).instructions; + const contentBlocks = instructions + .filter((instruction) => instruction.applicationMode === 'always') + .map((instruction) => instruction.content); + + const fileScopedInstructions = instructions.filter((instruction) => instruction.applicationMode === 'file'); + if (fileScopedInstructions.length > 0) { + contentBlocks.push( + formatProjectInstructionSection( + 'Repository file-scoped instructions:', + 'Apply each instruction only when working on files whose relative workspace path matches the listed glob.', + fileScopedInstructions, + ), + ); + } + + const taskScopedInstructions = instructions.filter((instruction) => instruction.applicationMode === 'task'); + if (taskScopedInstructions.length > 0) { + contentBlocks.push( + formatProjectInstructionSection( + 'Repository task-scoped instructions:', + 'Apply each instruction only when the current task matches its description.', + taskScopedInstructions, + ), + ); + } + + const content = contentBlocks .filter((value) => value.length > 0) .join('\n\n') .trim(); @@ -139,11 +171,35 @@ export function setProjectAgentProfileEnabled( } function normalizeProjectInstructionFile(file: ProjectInstructionFile): ProjectInstructionFile { - return { + const normalizedSourcePath = normalizePathLikeString(file.sourcePath); + const normalizedName = normalizeOptionalString(file.name); + const normalizedDescription = normalizeOptionalString(file.description); + const normalizedApplyTo = normalizeOptionalString(file.applyTo); + const normalizedInstruction: ProjectInstructionFile = { id: file.id.trim(), - sourcePath: normalizePathLikeString(file.sourcePath), + sourcePath: normalizedSourcePath, content: file.content.trim(), + applicationMode: normalizeInstructionApplicationMode( + file.applicationMode, + normalizedSourcePath, + normalizedApplyTo, + normalizedDescription, + ), }; + + if (normalizedName) { + normalizedInstruction.name = normalizedName; + } + + if (normalizedDescription) { + normalizedInstruction.description = normalizedDescription; + } + + if (normalizedApplyTo) { + normalizedInstruction.applyTo = normalizedApplyTo; + } + + return normalizedInstruction; } function normalizeProjectAgentProfile(profile: ProjectAgentProfile): ProjectAgentProfile { @@ -216,6 +272,38 @@ function compareProjectFiles( return left.sourcePath.localeCompare(right.sourcePath) || left.id.localeCompare(right.id); } +function formatProjectInstructionSection( + title: string, + guidance: string, + instructions: ReadonlyArray, +): string { + return [`${title}\n${guidance}`, ...instructions.map(formatProjectInstructionEntry)] + .filter((block) => block.length > 0) + .join('\n\n') + .trim(); +} + +function formatProjectInstructionEntry(instruction: ProjectInstructionFile): string { + const lines = [`Source: ${instruction.sourcePath}`]; + + if (instruction.name) { + lines.push(`Name: ${instruction.name}`); + } + + if (instruction.description) { + lines.push(`Description: ${instruction.description}`); + } + + if (instruction.applyTo) { + lines.push(`ApplyTo: ${instruction.applyTo}`); + } + + lines.push('Instructions:'); + lines.push(instruction.content); + + return lines.join('\n'); +} + function normalizeOptionalMcpServers( value?: Record, ): Record | undefined { @@ -242,6 +330,19 @@ function normalizeOptionalString(value?: string): string | undefined { return trimmed ? trimmed : undefined; } +function normalizeInstructionApplicationMode( + value: ProjectInstructionApplicationMode | undefined, + sourcePath: string, + applyTo?: string, + description?: string, +): ProjectInstructionApplicationMode { + if (value === 'always' || value === 'file' || value === 'task' || value === 'manual') { + return value; + } + + return inferInstructionApplicationMode(sourcePath, applyTo, description); +} + function normalizeOptionalStringArray(values?: ReadonlyArray): string[] | undefined { if (!values) { return undefined; @@ -254,6 +355,43 @@ function normalizePathLikeString(value: string): string { return value.trim().replaceAll('/', '\\'); } +function inferInstructionApplicationMode( + sourcePath: string, + applyTo?: string, + description?: string, +): ProjectInstructionApplicationMode { + if (isAlwaysOnInstructionSource(sourcePath) || isMatchAllInstructionGlob(applyTo)) { + return 'always'; + } + + if (applyTo) { + return 'file'; + } + + if (description) { + return 'task'; + } + + return 'manual'; +} + +function isAlwaysOnInstructionSource(sourcePath: string): boolean { + const normalizedSourcePath = normalizePathLikeString(sourcePath).toLowerCase(); + return normalizedSourcePath === '.github\\copilot-instructions.md' + || normalizedSourcePath === 'agents.md' + || normalizedSourcePath === 'claude.md' + || normalizedSourcePath === '.claude\\claude.md'; +} + +function isMatchAllInstructionGlob(value?: string): boolean { + if (!value) { + return false; + } + + const normalizedValue = value.trim().replaceAll('\\', '/'); + return normalizedValue === '**' || normalizedValue === '**/*'; +} + function normalizeYamlValue(value: unknown): unknown { if (Array.isArray(value)) { return value.map(normalizeYamlValue); diff --git a/tests/main/appServiceProjectCustomization.test.ts b/tests/main/appServiceProjectCustomization.test.ts index 5a793ab..d864603 100644 --- a/tests/main/appServiceProjectCustomization.test.ts +++ b/tests/main/appServiceProjectCustomization.test.ts @@ -131,11 +131,13 @@ describe('AryxAppService project customization', () => { id: 'instruction-repo', sourcePath: '.github\\copilot-instructions.md', content: 'Use TypeScript.', + applicationMode: 'always', }, { id: 'instruction-agents', sourcePath: 'AGENTS.md', content: 'Prefer focused tests.', + applicationMode: 'always', }, ], agentProfiles: [ @@ -200,6 +202,86 @@ describe('AryxAppService project customization', () => { ]); }); + test('sendSessionMessage formats file-scoped and task-scoped project instructions for the sidecar', async () => { + const workspace = createWorkspaceSeed(); + const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single'); + if (!pattern) { + throw new Error('Expected a single-agent pattern in the workspace seed.'); + } + + const project = createProject({ + customization: { + instructions: [ + { + id: 'instruction-repo', + sourcePath: '.github\\copilot-instructions.md', + content: 'Use TypeScript.', + applicationMode: 'always', + }, + { + id: 'instruction-react', + sourcePath: '.github\\instructions\\frontend\\react.instructions.md', + name: 'React Standards', + description: 'React file conventions', + applyTo: '**/*.tsx', + content: 'Use hooks.', + applicationMode: 'file', + }, + { + id: 'instruction-planning', + sourcePath: '.github\\instructions\\tasks\\planning.instructions.md', + description: 'Planning workflows', + content: 'Create phased plans before implementation.', + applicationMode: 'task', + }, + { + id: 'instruction-manual', + sourcePath: '.github\\instructions\\manual.instructions.md', + content: 'Never auto-apply me.', + applicationMode: 'manual', + }, + ], + agentProfiles: [], + promptFiles: [], + lastScannedAt: TIMESTAMP, + }, + }); + const session = createSession(project.id, pattern.id); + + workspace.projects = [project]; + workspace.sessions = [session]; + workspace.selectedProjectId = project.id; + workspace.selectedPatternId = pattern.id; + workspace.selectedSessionId = session.id; + + let command: RunTurnCommand | undefined; + const service = createService(workspace, pattern, { + captureRunTurn: (capturedCommand) => { + command = capturedCommand; + }, + }); + + await service.sendSessionMessage(session.id, 'Handle the frontend task.'); + + expect(command?.projectInstructions).toBe( + 'Use TypeScript.\n\n' + + 'Repository file-scoped instructions:\n' + + 'Apply each instruction only when working on files whose relative workspace path matches the listed glob.\n\n' + + 'Source: .github\\instructions\\frontend\\react.instructions.md\n' + + 'Name: React Standards\n' + + 'Description: React file conventions\n' + + 'ApplyTo: **/*.tsx\n' + + 'Instructions:\n' + + 'Use hooks.\n\n' + + 'Repository task-scoped instructions:\n' + + 'Apply each instruction only when the current task matches its description.\n\n' + + 'Source: .github\\instructions\\tasks\\planning.instructions.md\n' + + 'Description: Planning workflows\n' + + 'Instructions:\n' + + 'Create phased plans before implementation.', + ); + }); + test('setProjectAgentProfileEnabled persists the updated enabled state', async () => { const workspace = createWorkspaceSeed(); const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single'); diff --git a/tests/main/customizationScanner.test.ts b/tests/main/customizationScanner.test.ts index 39d243f..067d823 100644 --- a/tests/main/customizationScanner.test.ts +++ b/tests/main/customizationScanner.test.ts @@ -20,14 +20,22 @@ async function createTempDirectory(): Promise { } describe('ProjectCustomizationScanner', () => { - test('discovers project instructions, custom agents, and prompt files', async () => { + test('discovers recursive instruction, agent, and prompt files with metadata', async () => { const projectPath = await createTempDirectory(); - await mkdir(join(projectPath, '.github', 'agents'), { recursive: true }); - await mkdir(join(projectPath, '.github', 'prompts'), { recursive: true }); + await mkdir(join(projectPath, '.claude'), { recursive: true }); + await mkdir(join(projectPath, '.github', 'agents', 'docs'), { recursive: true }); + await mkdir(join(projectPath, '.github', 'instructions', 'frontend'), { recursive: true }); + await mkdir(join(projectPath, '.github', 'instructions', 'tasks'), { recursive: true }); + await mkdir(join(projectPath, '.github', 'prompts', 'docs'), { recursive: true }); await writeFile( join(projectPath, '.github', 'copilot-instructions.md'), - '# Repo instructions\nUse TypeScript.\n', + `--- +applyTo: "**" +--- +# Repo instructions +Use TypeScript. +`, 'utf8', ); await writeFile( @@ -36,7 +44,37 @@ describe('ProjectCustomizationScanner', () => { 'utf8', ); await writeFile( - join(projectPath, '.github', 'agents', 'readme-specialist.agent.md'), + join(projectPath, '.claude', 'CLAUDE.md'), + '# Claude guidance\nPrefer cohesive designs.\n', + 'utf8', + ); + await writeFile( + join(projectPath, '.github', 'instructions', 'frontend', 'react.instructions.md'), + `--- +name: React Standards +description: React file conventions +applyTo: "**/*.tsx" +--- +Use hooks and keep components focused. +`, + 'utf8', + ); + await writeFile( + join(projectPath, '.github', 'instructions', 'tasks', 'planning.instructions.md'), + `--- +description: Planning workflows +--- +Create phased plans before implementation. +`, + 'utf8', + ); + await writeFile( + join(projectPath, '.github', 'instructions', 'manual.instructions.md'), + 'Only attach manually.', + 'utf8', + ); + await writeFile( + join(projectPath, '.github', 'agents', 'docs', 'readme-specialist.agent.md'), `--- name: readme-specialist description: README specialist @@ -56,8 +94,9 @@ Focus on repository documentation only. 'utf8', ); await writeFile( - join(projectPath, '.github', 'prompts', 'explain-code.prompt.md'), + join(projectPath, '.github', 'prompts', 'docs', 'explain-code.prompt.md'), `--- +name: explain-selected-code agent: agent description: Generate a clear explanation --- @@ -71,15 +110,46 @@ Audience: \${input:audience:Who is this for?} const scanned = await new ProjectCustomizationScanner().scanProject(projectPath); expect(scanned.instructions).toEqual([ + { + id: expect.any(String), + sourcePath: '.claude\\CLAUDE.md', + content: '# Claude guidance\nPrefer cohesive designs.', + applicationMode: 'always', + }, { id: expect.any(String), sourcePath: '.github\\copilot-instructions.md', content: '# Repo instructions\nUse TypeScript.', + applyTo: '**', + applicationMode: 'always', + }, + { + id: expect.any(String), + sourcePath: '.github\\instructions\\frontend\\react.instructions.md', + name: 'React Standards', + description: 'React file conventions', + applyTo: '**/*.tsx', + content: 'Use hooks and keep components focused.', + applicationMode: 'file', + }, + { + id: expect.any(String), + sourcePath: '.github\\instructions\\manual.instructions.md', + content: 'Only attach manually.', + applicationMode: 'manual', + }, + { + id: expect.any(String), + sourcePath: '.github\\instructions\\tasks\\planning.instructions.md', + description: 'Planning workflows', + content: 'Create phased plans before implementation.', + applicationMode: 'task', }, { id: expect.any(String), sourcePath: 'AGENTS.md', content: '# Agent guidance\nPrefer clear tests.', + applicationMode: 'always', }, ]); expect(scanned.agentProfiles).toEqual([ @@ -97,14 +167,14 @@ Audience: \${input:audience:Who is this for?} type: 'local', }, }, - sourcePath: '.github\\agents\\readme-specialist.agent.md', + sourcePath: '.github\\agents\\docs\\readme-specialist.agent.md', enabled: true, }, ]); expect(scanned.promptFiles).toEqual([ { id: expect.any(String), - name: 'explain-code', + name: 'explain-selected-code', description: 'Generate a clear explanation', agent: 'agent', template: 'Explain the following code:\n${input:code:Paste your code here}\nAudience: ${input:audience:Who is this for?}', @@ -112,7 +182,7 @@ Audience: \${input:audience:Who is this for?} { name: 'code', placeholder: 'Paste your code here' }, { name: 'audience', placeholder: 'Who is this for?' }, ], - sourcePath: '.github\\prompts\\explain-code.prompt.md', + sourcePath: '.github\\prompts\\docs\\explain-code.prompt.md', }, ]); expect(scanned.lastScannedAt).toEqual(expect.any(String));