From 734e812287aec8f66d58a4b1c107e93815ad2fe0 Mon Sep 17 00:00:00 2001 From: dsk-dev-ai Date: Sun, 2 Aug 2026 12:31:16 +0530 Subject: [PATCH 1/7] refactor(instructions): prepare nested generation context --- packages/core/src/services/instructions.ts | 41 +++++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/core/src/services/instructions.ts b/packages/core/src/services/instructions.ts index f9f107a3..770827b1 100644 --- a/packages/core/src/services/instructions.ts +++ b/packages/core/src/services/instructions.ts @@ -170,6 +170,26 @@ export function buildExistingInstructionsSection(ctx: ExistingInstructionsContex return lines.join("\n"); } +/** + * Build a prompt section embedding the freshly-generated root instruction content + * so nested (per-area / per-crate) generation knows what the root file already + * covers and avoids restating it. Emits nothing when no root content is provided. + */ +export function buildRootContextSection(rootContent?: string): string { + const trimmed = rootContent?.trim(); + if (!trimmed) return ""; + + return [ + "", + "## Root instructions already cover", + "The root instruction file already covers the following workspace-wide content. " + + "Do NOT repeat or restate it in this file; cover only this area/crate's unique details:", + "", + trimmed, + "" + ].join("\n"); +} + /** * Strip outer markdown code fences that LLMs sometimes wrap around generated file content. * Only removes a single outer fence (```markdown or bare ```) — internal fences are preserved. @@ -294,6 +314,8 @@ type GenerateInstructionsOptions = { strategy?: InstructionStrategy; detailDir?: string; claudeMd?: boolean; + /** Content of the already-generated root instruction file, used to avoid duplicating it in nested outputs. */ + rootContent?: string; }; export async function generateCopilotInstructions( @@ -392,6 +414,8 @@ type GenerateAreaInstructionsOptions = { strategy?: InstructionStrategy; detailDir?: string; claudeMd?: boolean; + /** Content of the already-generated root instruction file, used to avoid duplicating it in area outputs. */ + rootContent?: string; }; export async function generateAreaInstructions( @@ -700,6 +724,7 @@ async function generateNestedHub( childAreas?: Area[]; model?: string; onProgress?: (message: string) => void; + rootContent?: string; } ): Promise { const progress = options.onProgress ?? (() => {}); @@ -763,12 +788,13 @@ async function generateNestedHub( : ""; // Invoke the nested-hub skill with repo/area-specific context + const rootContextSection = buildRootContextSection(options.rootContent); const prompt = `/nested-hub Generate a lean AGENTS.md hub file (~90-120 lines).${areaContext}${parentContext} Detail files go in \`${options.detailDir}/\`.${childContext} Recommend 3-5 topics for deep-dive detail files. Each slug becomes: \`${options.detailDir}/{slug}.md\`. -${existingSection ? `\nDo NOT duplicate content from existing instruction files\n${existingSection}` : ""}`; +${rootContextSection}${existingSection ? `\nDo NOT duplicate content from existing instruction files\n${existingSection}` : ""}`; let sendError: unknown; try { @@ -800,6 +826,7 @@ async function generateNestedDetail( area?: Area; model?: string; onProgress?: (message: string) => void; + rootContent?: string; } ): Promise { const progress = options.onProgress ?? (() => {}); @@ -848,8 +875,9 @@ async function generateNestedDetail( : "Focus on the entire repository."; // Invoke the nested-detail skill with topic-specific context + const rootContextSection = buildRootContextSection(options.rootContent); const prompt = `/nested-detail Generate a deep-dive instruction file about "${options.topic.title}" for this codebase. -${areaContext} +${areaContext}${rootContextSection} Topic: ${options.topic.title} Description: ${options.topic.description}`; @@ -903,7 +931,8 @@ export async function generateNestedInstructions( area: options.area, childAreas: options.childAreas, model: options.model, - onProgress: options.onProgress + onProgress: options.onProgress, + rootContent: options.rootContent }); // Determine output paths @@ -931,7 +960,8 @@ export async function generateNestedInstructions( topic, area: options.area, model: options.model, - onProgress: options.onProgress + onProgress: options.onProgress, + rootContent: options.rootContent }); if (detailContent) { result.details.push({ @@ -980,6 +1010,7 @@ export async function generateNestedAreaInstructions( model: options.model, onProgress: options.onProgress, detailDir: options.detailDir, - claudeMd: options.claudeMd + claudeMd: options.claudeMd, + rootContent: options.rootContent }); } From 8e98a35527471e2d7bd0708ba5f5784dab4cc613 Mon Sep 17 00:00:00 2001 From: dsk-dev-ai Date: Sun, 2 Aug 2026 12:31:17 +0530 Subject: [PATCH 2/7] feat(skills): reduce duplicated workspace instructions --- plugin/skills/nested-hub/SKILL.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/plugin/skills/nested-hub/SKILL.md b/plugin/skills/nested-hub/SKILL.md index 942bd213..3da399b3 100644 --- a/plugin/skills/nested-hub/SKILL.md +++ b/plugin/skills/nested-hub/SKILL.md @@ -16,6 +16,27 @@ The hub should contain: - Coding conventions and guardrails - A "## Detailed Instructions" section listing links to detail files +## Root Context + +The prompt may include a `## Root instructions already cover` section containing the workspace-wide content from the root instruction file. That content is already loaded by AI agents from the root file: + +- **Do NOT repeat or restate it** — not as prose, not as bullets, not as paraphrased summaries. +- **Cover only this area/crate's unique details** that are not addressed by the root file. +- If a topic is covered at the root, reference it with a single link (e.g. `See [AGENTS.md](../../AGENTS.md)`). + +## Area / Crate Hubs + +When generating a hub for a specific area or crate, include **only crate-unique context**: + +- Crate purpose and summary +- File structure → responsibility mapping +- Crate-specific conventions (e.g. binary resolution, auth patterns, helper APIs) +- Internal consumers / dependencies (only if non-obvious) + +Everything that applies workspace-wide — workspace dependencies, error types, logging conventions, testing setup, and standard build/test commands — lives exclusively in the root instruction file and must **not** be duplicated here. + +Use a simple heading that matches the component, e.g. `# crate`. Do **not** use a "Copilot Instructions:" heading for AGENTS.md files. + ## Topic Recommendations At the **end** of your output, emit a fenced JSON block with recommended topics for detail files: @@ -37,7 +58,7 @@ Recommend 3-5 topics that would benefit from deep-dive detail files. Each slug b - Keep the hub **lean** — overview and guardrails only, details go in separate files - The JSON block will be parsed and removed from the final output -- Do **not** duplicate content from existing instruction files +- Do **not** duplicate content from existing instruction files or the root instruction content ## Output Contract From 775d426e1ee0a4261f08e52f710df478d0e32fea Mon Sep 17 00:00:00 2001 From: dsk-dev-ai Date: Sun, 2 Aug 2026 12:32:10 +0530 Subject: [PATCH 3/7] feat(instructions): normalize AGENTS headings --- packages/core/src/services/instructions.ts | 28 ++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/core/src/services/instructions.ts b/packages/core/src/services/instructions.ts index 770827b1..d02074ed 100644 --- a/packages/core/src/services/instructions.ts +++ b/packages/core/src/services/instructions.ts @@ -190,6 +190,25 @@ export function buildRootContextSection(rootContent?: string): string { ].join("\n"); } +/** + * Normalize the top-level heading of generated AGENTS.md content so it matches + * the file type instead of leaking copilot-instructions.md conventions. + * Rewrites "# Copilot Instructions: " → "# " using the provided + * component name (area/crate) when available, otherwise the name in the heading. + * Returns the content unchanged when no "Copilot Instructions" heading is present. + */ +export function normalizeAgentsHeading(content: string, componentName?: string): string { + const headingRe = /^#\s+Copilot Instructions(?:\s*:\s*(.*))?\s*$/imu; + const match = headingRe.exec(content); + if (!match) return content; + + const fallback = match[1]?.trim(); + const heading = componentName?.trim() || fallback; + if (!heading) return content; + + return content.replace(headingRe, `# ${heading}`); +} + /** * Strip outer markdown code fences that LLMs sometimes wrap around generated file content. * Only removes a single outer fence (```markdown or bare ```) — internal fences are preserved. @@ -939,10 +958,15 @@ export async function generateNestedInstructions( const basePath = options.area?.path ?? "."; const hubRelativePath = path.join(basePath, "AGENTS.md"); + // Normalize the heading so generated AGENTS.md files don't carry a + // copilot-instructions.md title (e.g. "# Copilot Instructions: crate"). + const componentName = options.area?.name ?? path.basename(options.repoPath); + const normalizedHubContent = normalizeAgentsHeading(hubContent, componentName); + // Hub content: prepend frontmatter if area-scoped - let finalHubContent = hubContent; + let finalHubContent = normalizedHubContent; if (options.area) { - finalHubContent = `${buildAreaFrontmatter(options.area)}\n\n${hubContent}`; + finalHubContent = `${buildAreaFrontmatter(options.area)}\n\n${normalizedHubContent}`; } const result: NestedInstructionsResult = { From f1aa05be03df6e9a925a8c1629c02e3302a943e1 Mon Sep 17 00:00:00 2001 From: dsk-dev-ai Date: Sun, 2 Aug 2026 12:35:11 +0530 Subject: [PATCH 4/7] fix(generator): respect selected instruction output --- packages/core/src/services/generator.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/core/src/services/generator.ts b/packages/core/src/services/generator.ts index 2a1945b9..36380d48 100644 --- a/packages/core/src/services/generator.ts +++ b/packages/core/src/services/generator.ts @@ -20,6 +20,8 @@ export type GenerateOptions = { selections: string[]; force: boolean; dryRun?: boolean; + /** Relative path of the generated instruction file referenced by VS Code settings. Defaults to `.github/copilot-instructions.md`. */ + instructionFile?: string; }; async function writeOrPreview( @@ -58,7 +60,7 @@ export async function generateConfigs(options: GenerateOptions): Promise Date: Sun, 2 Aug 2026 12:38:02 +0530 Subject: [PATCH 5/7] refactor(cli): propagate nested generation context --- src/commands/instructions.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/commands/instructions.ts b/src/commands/instructions.ts index e7a1297c..3bcf7322 100644 --- a/src/commands/instructions.ts +++ b/src/commands/instructions.ts @@ -75,6 +75,10 @@ export async function instructionsCommand(options: InstructionsOptions): Promise try { const dryRunFiles: { path: string; bytes: number }[] = []; + // Root instruction content, propagated to per-area generation so area/crate + // files don't duplicate what the root file already covers. + let rootContent: string | undefined; + // Generate root instructions unless --areas-only if (!options.areasOnly && !options.area) { if (strategy === "nested") { @@ -88,6 +92,7 @@ export async function instructionsCommand(options: InstructionsOptions): Promise detailDir, claudeMd }); + rootContent = nestedResult.hub.content; if (options.dryRun) { const dryFiles = [ { path: nestedResult.hub.relativePath, content: nestedResult.hub.content }, @@ -283,7 +288,8 @@ export async function instructionsCommand(options: InstructionsOptions): Promise model: options.model, onProgress: shouldLog(options) ? (msg) => progress.update(msg) : undefined, detailDir, - claudeMd + claudeMd, + rootContent }); if (options.dryRun) { const dryFiles = [ From f702d5008ed6a70237c3b9a6ce9f6393f14261de Mon Sep 17 00:00:00 2001 From: dsk-dev-ai Date: Sun, 2 Aug 2026 12:40:32 +0530 Subject: [PATCH 6/7] test(instructions): cover nested AGENTS generation --- src/services/__tests__/generator.test.ts | 33 +++++++++ src/services/__tests__/instructions.test.ts | 81 +++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/src/services/__tests__/generator.test.ts b/src/services/__tests__/generator.test.ts index 72352776..1b0109ba 100644 --- a/src/services/__tests__/generator.test.ts +++ b/src/services/__tests__/generator.test.ts @@ -80,6 +80,39 @@ describe("generateConfigs", () => { expect(reviewText).toContain("repo conventions"); }); + it("references the selected instruction file in vscode settings", async () => { + const analysis = makeAnalysis(); + await generateConfigs({ + repoPath: tmpDir, + analysis, + selections: ["vscode"], + force: false, + instructionFile: "AGENTS.md" + }); + + const content = await fs.readFile(path.join(tmpDir, ".vscode", "settings.json"), "utf8"); + const parsed = JSON.parse(content); + expect(parsed["github.copilot.chat.codeGeneration.instructions"]).toEqual([ + { file: "AGENTS.md" } + ]); + }); + + it("defaults vscode settings to .github/copilot-instructions.md", async () => { + const analysis = makeAnalysis(); + await generateConfigs({ + repoPath: tmpDir, + analysis, + selections: ["vscode"], + force: false + }); + + const content = await fs.readFile(path.join(tmpDir, ".vscode", "settings.json"), "utf8"); + const parsed = JSON.parse(content); + expect(parsed["github.copilot.chat.codeGeneration.instructions"]).toEqual([ + { file: ".github/copilot-instructions.md" } + ]); + }); + it("skips existing files without force", async () => { await fs.mkdir(path.join(tmpDir, ".vscode"), { recursive: true }); await fs.writeFile(path.join(tmpDir, ".vscode", "mcp.json"), "original", "utf8"); diff --git a/src/services/__tests__/instructions.test.ts b/src/services/__tests__/instructions.test.ts index cda36232..69eb44d3 100644 --- a/src/services/__tests__/instructions.test.ts +++ b/src/services/__tests__/instructions.test.ts @@ -9,6 +9,7 @@ import { generateAreaInstructions, generateCopilotInstructions, generateNestedInstructions, + generateNestedAreaInstructions, writeAreaInstruction, writeInstructionFile, writeNestedInstructions, @@ -17,6 +18,8 @@ import { areaInstructionPath, detectExistingInstructions, buildExistingInstructionsSection, + buildRootContextSection, + normalizeAgentsHeading, parseTopicsFromHub, stripMarkdownFences } from "@agentrc/core/services/instructions"; @@ -431,6 +434,23 @@ describe("buildExistingInstructionsSection", () => { }); }); +describe("buildRootContextSection", () => { + it("returns empty string when no root content is provided", () => { + expect(buildRootContextSection()).toBe(""); + expect(buildRootContextSection("")).toBe(""); + expect(buildRootContextSection(" ")).toBe(""); + }); + + it("embeds the root content under a do-not-repeat header", () => { + const root = "# Monorepo\n\nWorkspace crates use `{ workspace = true }`."; + const section = buildRootContextSection(root); + expect(section).toContain("## Root instructions already cover"); + expect(section).toContain("Do NOT repeat or restate it"); + expect(section).toContain("# Monorepo"); + expect(section).toContain("`{ workspace = true }`"); + }); +}); + describe("writeInstructionFile", () => { let tmpDir: string; @@ -739,6 +759,29 @@ describe("stripMarkdownFences", () => { }); }); +describe("normalizeAgentsHeading", () => { + it("returns content unchanged when no Copilot Instructions heading is present", () => { + const content = "# git crate\n\nBody."; + expect(normalizeAgentsHeading(content, "git")).toBe(content); + }); + + it("rewrites the heading to the component name", () => { + expect(normalizeAgentsHeading("# Copilot Instructions: git crate", "git")).toBe("# git"); + }); + + it("falls back to the name embedded in the heading when no component name", () => { + expect(normalizeAgentsHeading("# Copilot Instructions: git crate")).toBe("# git crate"); + }); + + it("matches the heading case-insensitively", () => { + expect(normalizeAgentsHeading("# copilot instructions: git crate", "git")).toBe("# git"); + }); + + it("returns content unchanged when heading has no name and no component name", () => { + expect(normalizeAgentsHeading("# Copilot Instructions")).toBe("# Copilot Instructions"); + }); +}); + describe("instruction generation sessions", () => { let tmpDir: string; @@ -1051,4 +1094,42 @@ describe("instruction generation sessions", () => { }) ).rejects.toThrow("Copilot CLI not logged in. Run `copilot` then `/login` to authenticate."); }); + + it("propagates root content and normalizes headings for nested area generation", async () => { + const area: Area = { + name: "git", + applyTo: "crates/git/**", + path: path.join(tmpDir, "crates", "git"), + source: "auto" + }; + const hub = createMockSession(); + const { createSession } = mockClient([hub.session]); + mockSdkTools(); + + const rootContent = "# Monorepo\n\nWorkspace crates use `{ workspace = true }`."; + let sentPrompt = ""; + + hub.session.sendAndWait.mockImplementation(async (args: { prompt: string }) => { + sentPrompt = args.prompt; + const [config] = createSession.mock.calls[0] as unknown as [ + { tools: Array<{ handler: Function }> } + ]; + await config.tools[0].handler({ + content: "```markdown\n# Copilot Instructions: git crate\n\nCrate-specific.\n```" + }); + }); + + const result = await generateNestedAreaInstructions({ + repoPath: tmpDir, + area, + detailDir: ".agents", + claudeMd: false, + rootContent + }); + + expect(result.hub.content).toContain("# git"); + expect(result.hub.content).not.toContain("Copilot Instructions"); + expect(sentPrompt).toContain("## Root instructions already cover"); + expect(sentPrompt).toContain("Workspace crates use `{ workspace = true }`"); + }); }); From 8d3640f1302fa47f20f89455b2543da0266e61b7 Mon Sep 17 00:00:00 2001 From: Darshan Kachare Date: Sun, 2 Aug 2026 12:54:32 +0530 Subject: [PATCH 7/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/core/src/services/instructions.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/core/src/services/instructions.ts b/packages/core/src/services/instructions.ts index d02074ed..2ef34f8b 100644 --- a/packages/core/src/services/instructions.ts +++ b/packages/core/src/services/instructions.ts @@ -198,15 +198,19 @@ export function buildRootContextSection(rootContent?: string): string { * Returns the content unchanged when no "Copilot Instructions" heading is present. */ export function normalizeAgentsHeading(content: string, componentName?: string): string { - const headingRe = /^#\s+Copilot Instructions(?:\s*:\s*(.*))?\s*$/imu; - const match = headingRe.exec(content); + // Only consider the first line as the document's top-level heading. + const headingRe = /^(?:\uFEFF)?#\s+Copilot Instructions(?:\s*:\s*(.*))?\s*$/iu; + const firstLineEnd = content.indexOf("\n"); + const firstLine = (firstLineEnd === -1 ? content : content.slice(0, firstLineEnd)).trimEnd(); + const match = headingRe.exec(firstLine); if (!match) return content; const fallback = match[1]?.trim(); const heading = componentName?.trim() || fallback; if (!heading) return content; - return content.replace(headingRe, `# ${heading}`); + const replacedFirstLine = firstLine.replace(headingRe, `# ${heading}`); + return firstLineEnd === -1 ? replacedFirstLine : `${replacedFirstLine}\n${content.slice(firstLineEnd + 1)}`; } /**