diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f5dc50..5a9574b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.73.0 - 2026-06-01 + +- Discover repo-relative Copilot instruction folders configured through VS Code `chat.instructionsFilesLocations` in `.vscode/settings.json` and committed `*.code-workspace` files. +- Include configured `*.instructions.md` files in context health and context security scans instead of limiting coverage to `.github/instructions`. +- Apply the `copilot-missing-applyto` context-health finding to configured Copilot instruction files as well as default path-scoped instruction files. + ## 0.72.0 - 2026-06-01 - Add `copilot-missing-applyto` to context health audits so `.github/instructions/**/*.instructions.md` files without `applyTo` frontmatter are no longer treated as fully scoped Copilot guidance. diff --git a/README.md b/README.md index d87d7ca..d8163e3 100644 --- a/README.md +++ b/README.md @@ -359,7 +359,7 @@ contextforge pack --task "review auth regression" --budget 20000 --sessions --ou Or use the GitHub Action before npm publishing is complete: ```yaml -- uses: grnbtqdbyx-create/contextforge@v0.72.0 +- uses: grnbtqdbyx-create/contextforge@v0.73.0 with: min-context-score: 60 min-cache-score: 60 @@ -498,7 +498,7 @@ contextforge cost-estimate [--demo] [--json] [--summary contextforge-cost-estima contextforge review-kit [--demo] [--base main] [--output contextforge-review-kit.md] contextforge artifact-map [--output docs/artifacts.md] contextforge publish-readiness [--json] [--summary contextforge-publish-readiness.md] -contextforge init [--all] [--github-action] [--pr-comment-workflow] [--agents-md] [--claude-md] [--copilot-instructions] [--project-name "My App"] [--action-ref grnbtqdbyx-create/contextforge@v0.72.0] [--force] +contextforge init [--all] [--github-action] [--pr-comment-workflow] [--agents-md] [--claude-md] [--copilot-instructions] [--project-name "My App"] [--action-ref grnbtqdbyx-create/contextforge@v0.73.0] [--force] ``` Local session scans are bounded by default. Use `--max-session-files` and @@ -583,7 +583,7 @@ See [docs/research/adjacent-tools.md](docs/research/adjacent-tools.md). ## Current Status -ContextForge v0.72.0 is a public MVP CLI with: +ContextForge v0.73.0 is a public MVP CLI with: - Claude Code and Codex JSONL fixture scanners - bounded local session scanning fallbacks @@ -600,6 +600,7 @@ ContextForge v0.72.0 is a public MVP CLI with: - committed Claude Code settings audits for bypass modes, broad Bash allow rules, remote shell hooks, wildcard HTTP hooks, and missing sensitive-file denies - Claude Code project subagent and custom slash-command discovery for `.claude/agents/**/*.md` and `.claude/commands/**/*.md` - GitHub Copilot customization discovery for `.github/copilot-instructions.md`, `.github/instructions/**/*.instructions.md`, `.github/prompts/**/*.prompt.md`, `.github/agents/**/*.md`, `.github/agents/**/*.agent.md`, and project skills under `.github/skills`, `.claude/skills`, and `.agents/skills` +- VS Code `chat.instructionsFilesLocations` discovery for repo-relative custom Copilot instruction folders - GitHub Copilot path-scoped instruction checks that flag `.github/instructions/**/*.instructions.md` files missing `applyTo` frontmatter - GitHub Copilot hook security scanning for `.github/hooks/*.json` and committed `.github/copilot/settings*.json` - VS Code Copilot workspace settings security scanning for `.vscode/settings.json` and committed `*.code-workspace` files @@ -732,6 +733,7 @@ ContextForge v0.72.0 is a public MVP CLI with: - **v0.70.0:** doctor, proof-pack, and scorecard reports surface Claude settings, agentic workflow, and GitHub Actions hardening evidence in one readiness path. - **v0.71.0:** GitHub Actions audits catch missing Node 24 JavaScript action runtime opt-ins and document the known runner annotation behavior. - **v0.72.0:** Context health audits catch path-scoped Copilot instruction files missing `applyTo` frontmatter. +- **v0.73.0:** Context discovery follows repo-relative VS Code `chat.instructionsFilesLocations` folders for custom Copilot instruction files. - **Next:** first approved npm publish and external launch outreach. Release preparation lives in [docs/release-checklist.md](docs/release-checklist.md). diff --git a/contextforge-agent-surface-map.md b/contextforge-agent-surface-map.md index 84575e7..73c1131 100644 --- a/contextforge-agent-surface-map.md +++ b/contextforge-agent-surface-map.md @@ -9,7 +9,7 @@ It shows which repo-level prompt, settings, tool, and workflow surfaces ContextF | --- | --- | --- | --- | | OpenAI Codex | `AGENTS.md`, `CLAUDE.md`, root `README.md`, MCP config files | Codex and other coding agents need concise repo guidance, safe local tool access, and predictable context before long tasks. | `contextforge agents-md-audit`, `contextforge security-audit`, `contextforge mcp-audit`, `contextforge pack` | | Claude Code | `CLAUDE.md`, `.claude/settings*.json`, `.claude/skills/*/SKILL.md`, `.claude/agents/**/*.md`, `.claude/commands/**/*.md` | Claude Code reads project memory, settings, skills, subagents, and command prompts from committed repo files that can affect permissions, tool use, and context cost. | `contextforge claude-audit`, `contextforge security-audit`, `contextforge agents-md-audit`, `contextforge pack` | -| GitHub Copilot | `.github/copilot-instructions.md`, `.github/instructions/**/*.instructions.md`, `.github/prompts/**/*.prompt.md`, `.github/agents/**/*.md`, `.github/hooks/*.json`, `.github/copilot/settings*.json`, `.vscode/settings.json`, `*.code-workspace` | Copilot customization can add always-on instructions, reusable prompts, custom agents, hooks, and workspace instruction text that reviewers may miss. | `contextforge security-audit`, `contextforge agents-md-audit`, `contextforge pack`, GitHub Actions SARIF upload | +| GitHub Copilot | `.github/copilot-instructions.md`, `.github/instructions/**/*.instructions.md`, repo-relative `chat.instructionsFilesLocations`, `.github/prompts/**/*.prompt.md`, `.github/agents/**/*.md`, `.github/hooks/*.json`, `.github/copilot/settings*.json`, `.vscode/settings.json`, `*.code-workspace` | Copilot customization can add always-on instructions, custom instruction folders, reusable prompts, custom agents, hooks, and workspace instruction text that reviewers may miss. | `contextforge security-audit`, `contextforge agents-md-audit`, `contextforge pack`, GitHub Actions SARIF upload | | MCP tool configs | `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, Claude and Codex MCP config files | MCP servers expose tools to agents; committed configs can hide hardcoded secrets, remote shell installers, unpinned packages, auto-approval, broad permissions, or symlinks. | `contextforge mcp-audit --summary contextforge-mcp-audit.md --sarif contextforge-mcp.sarif` | | Cursor, Cline, Gemini, and Windsurf-style agents | `.cursor/rules/**/*.mdc`, `.cursorrules`, `.clinerules/**/*.{md,txt}`, `.clinerules`, `GEMINI.md`, `.windsurfrules`, `.windsurf/rules/**/*.{md,mdc,txt}`, MCP config files | Adjacent coding agents consume repo-local rules, memories, and tool configs that can become stale, broad, or unsafe. | `contextforge security-audit`, `contextforge mcp-audit`, `contextforge pack` | diff --git a/contextforge-publish-readiness.md b/contextforge-publish-readiness.md index cbf7a4c..ba26e44 100644 --- a/contextforge-publish-readiness.md +++ b/contextforge-publish-readiness.md @@ -2,11 +2,11 @@ Status: **warn** -Package: `contextforge@0.72.0` +Package: `contextforge@0.73.0` | Check | Status | Detail | | --- | --- | --- | -| Package metadata | pass | contextforge@0.72.0 is public-package ready with bin dist/cli.js | +| Package metadata | pass | contextforge@0.73.0 is public-package ready with bin dist/cli.js | | Package provenance metadata | pass | repository, homepage, and issue tracker point at grnbtqdbyx-create/contextforge for npm provenance readers | | Trusted publishing workflow | pass | npm Trusted Publishing uses GitHub OIDC, manual dispatch, dry-run default, and environment approval | | Release artifact attestation | pass | GitHub artifact attestation covers the packed npm tarball before the same tarball is published | diff --git a/docs/copilot-instructions.md b/docs/copilot-instructions.md index 709df04..80a4fe3 100644 --- a/docs/copilot-instructions.md +++ b/docs/copilot-instructions.md @@ -12,6 +12,9 @@ Covered files: - `.github/copilot-instructions.md` - `.github/instructions/**/*.instructions.md` +- repo-relative `*.instructions.md` files under folders enabled by + `chat.instructionsFilesLocations` in `.vscode/settings.json` or committed + `*.code-workspace` files - `.github/prompts/**/*.prompt.md` - `.github/agents/**/*.md` - `.github/agents/**/*.agent.md` @@ -31,6 +34,9 @@ Why this matters: - Path-scoped `.github/instructions/**/*.instructions.md` files need an `applyTo` frontmatter pattern when maintainers expect automatic application; ContextForge flags missing scopes as `copilot-missing-applyto`. +- Teams can move instruction files into custom repo folders with VS Code + `chat.instructionsFilesLocations`; ContextForge follows enabled repo-relative + locations so those rules do not fall outside the audit loop. - Prompt files, custom agents, and project skills can carry reusable task guidance that affects how Copilot plans or executes work. - Copilot hooks can execute shell commands at agent lifecycle points, so they diff --git a/docs/github-action.md b/docs/github-action.md index 003b6bd..54ac233 100644 --- a/docs/github-action.md +++ b/docs/github-action.md @@ -36,7 +36,7 @@ refuses to overwrite existing files by default: ```bash contextforge init --github-action --force -contextforge init --github-action --action-ref grnbtqdbyx-create/contextforge@v0.72.0 +contextforge init --github-action --action-ref grnbtqdbyx-create/contextforge@v0.73.0 ``` `contextforge init --pr-comment-workflow` writes a separate @@ -71,7 +71,7 @@ jobs: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: fetch-depth: 0 - - uses: grnbtqdbyx-create/contextforge@v0.72.0 + - uses: grnbtqdbyx-create/contextforge@v0.73.0 with: min-context-score: 60 min-cache-score: 60 diff --git a/docs/release-checklist.md b/docs/release-checklist.md index cef10e2..02b64ae 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -30,6 +30,7 @@ - [x] Context pack generation shows a budget ledger and measures final output against the requested token budget. - [x] GitHub Copilot instruction files are discovered, audited, and scaffolded with the same context hygiene loop as AGENTS.md and CLAUDE.md. - [x] GitHub Copilot path-scoped instruction files are checked for missing `applyTo` frontmatter. +- [x] VS Code `chat.instructionsFilesLocations` folders are discovered when they point at repo-relative Copilot instruction files. - [x] GitHub Copilot prompt files, custom agents, and project skills are discovered and audited when present. - [x] GitHub Copilot hook configs are scanned for unsafe shell commands and context-security risk when present. - [x] VS Code Copilot workspace settings are scanned for risky committed instruction text when present. diff --git a/docs/research/adjacent-tools.md b/docs/research/adjacent-tools.md index dc36119..36c734a 100644 --- a/docs/research/adjacent-tools.md +++ b/docs/research/adjacent-tools.md @@ -589,3 +589,9 @@ scope may require manual attachment or other selection behavior. Missing scopes are easy to miss in agent-authored repos, so `contextforge agents-md-audit` now reports `copilot-missing-applyto` before maintainers assume path-scoped guidance is deterministically active. +ContextForge v0.73.0 follows the customization location setting itself: +repo-relative folders enabled through VS Code `chat.instructionsFilesLocations` +in `.vscode/settings.json` or committed `*.code-workspace` files are scanned for +`*.instructions.md`. That closes a practical gap for teams that keep Copilot +rules outside `.github/instructions` but still expect the same context-health +and context-security proof. diff --git a/llms-full.txt b/llms-full.txt index 215f8d0..a91aea0 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -25,6 +25,8 @@ separate tools: - Copilot path-scoped instruction checks that flag `.github/instructions/**/*.instructions.md` files without `applyTo` frontmatter +- VS Code `chat.instructionsFilesLocations` discovery for repo-relative custom + Copilot instruction folders - context security checks for prompt injection, secret exfiltration, unsafe shell instructions, hidden directives, and permission escalation - Claude Code project subagent and custom command checks for committed diff --git a/llms.txt b/llms.txt index d012b58..fa2bd41 100644 --- a/llms.txt +++ b/llms.txt @@ -60,7 +60,7 @@ Codex and Claude can act on. - [Artifact Map](docs/artifacts.md): Generated catalog of ContextForge artifacts and fast paths. - [npm Publish](docs/npm-publish.md): Trusted Publishing workflow, provenance metadata, and publish-readiness checks. - [Agent Context Init](docs/agent-context-init.md): Minimal `AGENTS.md`, `CLAUDE.md`, and Copilot instruction scaffolding. -- [GitHub Copilot Customization](docs/copilot-instructions.md): Copilot instruction, `applyTo` scope, prompt file, custom agent, project skill, hook, and VS Code workspace settings security coverage. +- [GitHub Copilot Customization](docs/copilot-instructions.md): Copilot instruction, `applyTo` scope, configured instruction-location, prompt file, custom agent, project skill, hook, and VS Code workspace settings security coverage. - [Agent Action Plan](docs/agent-action-plan.md): `contextforge plan` and audit handoff artifacts for Codex/Claude. - [Security Audit](docs/security-audit.md): Prompt/context poisoning checks for repo instruction files, Claude Code subagents, custom slash commands, Copilot prompts, hooks, and workspace settings. - [Security Benchmark](docs/security-benchmark.md): Public malicious-context fixtures and expected findings. diff --git a/package.json b/package.json index 002462c..8e15f15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "contextforge", - "version": "0.72.0", + "version": "0.73.0", "description": "Agent context gate for Codex, Claude Code, GitHub Copilot, MCP, Cursor, Cline, Gemini, and Windsurf repos.", "type": "module", "packageManager": "pnpm@11.2.2", diff --git a/src/analyzers/contextHealth.ts b/src/analyzers/contextHealth.ts index e0f4104..7946854 100644 --- a/src/analyzers/contextHealth.ts +++ b/src/analyzers/contextHealth.ts @@ -80,7 +80,7 @@ export async function auditContextFiles(options: { rootDir?: string } = {}): Pro } function isCopilotPathScopedInstruction(relativePath: string): boolean { - return /^\.github\/instructions\/.+\.instructions\.md$/i.test(relativePath); + return /(^|\/).+\.instructions\.md$/i.test(relativePath); } function hasApplyToFrontmatter(content: string): boolean { diff --git a/src/cli.ts b/src/cli.ts index 8802757..65728e3 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -920,7 +920,7 @@ Usage: contextforge surface-inventory [--json] [--output contextforge-agent-surface-inventory.md] contextforge surface-diff [--base main] [--json] [--output contextforge-agent-surface-diff.md] contextforge publish-readiness [--json] [--summary contextforge-publish-readiness.md] - contextforge init [--all] [--github-action] [--pr-comment-workflow] [--agents-md] [--claude-md] [--copilot-instructions] [--project-name "My App"] [--action-ref grnbtqdbyx-create/contextforge@v0.72.0] [--force] + contextforge init [--all] [--github-action] [--pr-comment-workflow] [--agents-md] [--claude-md] [--copilot-instructions] [--project-name "My App"] [--action-ref grnbtqdbyx-create/contextforge@v0.73.0] [--force] Session scan safety: --max-session-files 50 newest JSONL files to scan per provider diff --git a/src/init/githubAction.ts b/src/init/githubAction.ts index 05ca336..095cf61 100644 --- a/src/init/githubAction.ts +++ b/src/init/githubAction.ts @@ -1,7 +1,7 @@ import { access, mkdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; -export const DEFAULT_GITHUB_ACTION_REF = 'grnbtqdbyx-create/contextforge@v0.72.0'; +export const DEFAULT_GITHUB_ACTION_REF = 'grnbtqdbyx-create/contextforge@v0.73.0'; export interface GithubActionScaffoldOptions { rootDir: string; diff --git a/src/report/agentSurfaceMap.ts b/src/report/agentSurfaceMap.ts index 720e2a9..69401c1 100644 --- a/src/report/agentSurfaceMap.ts +++ b/src/report/agentSurfaceMap.ts @@ -23,8 +23,8 @@ const rows: AgentSurfaceRow[] = [ }, { ecosystem: 'GitHub Copilot', - surface: '`.github/copilot-instructions.md`, `.github/instructions/**/*.instructions.md`, `.github/prompts/**/*.prompt.md`, `.github/agents/**/*.md`, `.github/hooks/*.json`, `.github/copilot/settings*.json`, `.vscode/settings.json`, `*.code-workspace`', - whyItMatters: 'Copilot customization can add always-on instructions, reusable prompts, custom agents, hooks, and workspace instruction text that reviewers may miss.', + surface: '`.github/copilot-instructions.md`, `.github/instructions/**/*.instructions.md`, repo-relative `chat.instructionsFilesLocations`, `.github/prompts/**/*.prompt.md`, `.github/agents/**/*.md`, `.github/hooks/*.json`, `.github/copilot/settings*.json`, `.vscode/settings.json`, `*.code-workspace`', + whyItMatters: 'Copilot customization can add always-on instructions, custom instruction folders, reusable prompts, custom agents, hooks, and workspace instruction text that reviewers may miss.', coverage: '`contextforge security-audit`, `contextforge agents-md-audit`, `contextforge pack`, GitHub Actions SARIF upload', source: '[GitHub Copilot custom agents](https://docs.github.com/en/copilot/concepts/agents/copilot-cli/about-custom-agents), GitHub Copilot customization docs' }, diff --git a/src/utils/contextFiles.ts b/src/utils/contextFiles.ts index 0cdbadc..fa841b8 100644 --- a/src/utils/contextFiles.ts +++ b/src/utils/contextFiles.ts @@ -1,3 +1,4 @@ +import { promises as fs } from 'node:fs'; import path from 'node:path'; import { listFiles } from './files.js'; @@ -12,11 +13,13 @@ export interface ContextFileRef { } export async function listContextFiles(rootDir: string): Promise { - return listNamedContextFiles(rootDir, (relativePath) => isNamedContextPath(relativePath, CONTEXT_FILE_NAMES)); + const files = await listNamedContextFiles(rootDir, (relativePath) => isNamedContextPath(relativePath, CONTEXT_FILE_NAMES)); + return mergeContextFiles(files, await listConfiguredCopilotInstructionFiles(rootDir)); } export async function listSecurityContextFiles(rootDir: string): Promise { - return listNamedContextFiles(rootDir, (relativePath) => isSecurityContextPath(relativePath)); + const files = await listNamedContextFiles(rootDir, (relativePath) => isSecurityContextPath(relativePath)); + return mergeContextFiles(files, await listConfiguredCopilotInstructionFiles(rootDir)); } async function listNamedContextFiles(rootDir: string, matchesContextPath: (relativePath: string) => boolean): Promise { @@ -34,6 +37,113 @@ function isFixtureOrTestContext(relativePath: string): boolean { return firstSegment === 'fixtures' || firstSegment === 'tests'; } +async function listConfiguredCopilotInstructionFiles(rootDir: string): Promise { + const settingsFiles = await listFiles(rootDir, (filePath) => { + const relativePath = path.relative(rootDir, filePath).split(path.sep).join('/'); + return relativePath === '.vscode/settings.json' || /\.code-workspace$/i.test(relativePath); + }); + const configuredLocations = new Set(); + + for (const settingsFile of settingsFiles) { + const relativePath = path.relative(rootDir, settingsFile).split(path.sep).join('/'); + const settings = await readCopilotInstructionSettings(settingsFile, relativePath); + const locations = getRecord(settings?.['chat.instructionsFilesLocations']); + if (!locations) continue; + for (const [location, enabled] of Object.entries(locations)) { + if (enabled === true && isRepositoryRelativeLocation(location)) { + configuredLocations.add(normalizeInstructionLocation(location).replace(/\/+$/, '')); + } + } + } + + const files: ContextFileRef[] = []; + for (const location of configuredLocations) { + const absoluteLocation = path.join(rootDir, location); + const instructionFiles = await listFiles(absoluteLocation, (filePath) => /\.instructions\.md$/i.test(filePath)); + files.push( + ...instructionFiles.map((absolutePath) => ({ + absolutePath, + relativePath: path.relative(rootDir, absolutePath).split(path.sep).join('/') + })) + ); + } + + return files.filter((file) => !isFixtureOrTestContext(file.relativePath)); +} + +async function readCopilotInstructionSettings(settingsFile: string, relativePath: string): Promise | undefined> { + try { + const parsed = JSON.parse(stripJsonComments(await fs.readFile(settingsFile, 'utf8'))) as unknown; + const root = getRecord(parsed); + if (!root) return undefined; + return /\.code-workspace$/i.test(relativePath) ? getRecord(root.settings) : root; + } catch { + return undefined; + } +} + +function stripJsonComments(input: string): string { + let output = ''; + let inString = false; + let escaped = false; + for (let index = 0; index < input.length; index += 1) { + const char = input[index]; + const next = input[index + 1]; + if (inString) { + output += char; + if (escaped) { + escaped = false; + } else if (char === '\\') { + escaped = true; + } else if (char === '"') { + inString = false; + } + continue; + } + if (char === '"') { + inString = true; + output += char; + } else if (char === '/' && next === '/') { + while (index < input.length && input[index] !== '\n') index += 1; + output += '\n'; + } else if (char === '/' && next === '*') { + index += 2; + while (index < input.length && !(input[index] === '*' && input[index + 1] === '/')) index += 1; + index += 1; + } else { + output += char; + } + } + return output.replace(/,\s*([}\]])/g, '$1'); +} + +function getRecord(value: unknown): Record | undefined { + return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : undefined; +} + +function isRepositoryRelativeLocation(location: string): boolean { + const normalized = normalizeInstructionLocation(location); + return ( + normalized.length > 0 && + !normalized.startsWith('/') && + !normalized.startsWith('~') && + !normalized.includes('://') && + !normalized.split('/').includes('..') + ); +} + +function normalizeInstructionLocation(location: string): string { + return location.replace(/\\/g, '/').split(path.sep).join('/'); +} + +function mergeContextFiles(files: ContextFileRef[], configuredFiles: ContextFileRef[]): ContextFileRef[] { + const byPath = new Map(); + for (const file of [...files, ...configuredFiles]) { + byPath.set(file.relativePath, file); + } + return [...byPath.values()].sort((left, right) => left.relativePath.localeCompare(right.relativePath)); +} + export function isCopilotInstructionPath(relativePath: string): boolean { const normalized = relativePath.split(path.sep).join('/'); return normalized === COPILOT_INSTRUCTIONS_FILE || /^\.github\/instructions\/.+\.instructions\.md$/i.test(normalized); diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 20d1d64..83f5643 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -62,7 +62,7 @@ describe('CLI help command', () => { it('prints the current default GitHub Action ref in init examples', async () => { const { stdout } = await execFileAsync('pnpm', ['contextforge', 'help']); - expect(stdout).toContain('--action-ref grnbtqdbyx-create/contextforge@v0.72.0'); + expect(stdout).toContain('--action-ref grnbtqdbyx-create/contextforge@v0.73.0'); }); }); diff --git a/tests/contextFiles.test.ts b/tests/contextFiles.test.ts index c27e96e..9b9c408 100644 --- a/tests/contextFiles.test.ts +++ b/tests/contextFiles.test.ts @@ -95,6 +95,37 @@ describe('context file discovery', () => { ]); }); + it('discovers Copilot instruction files from configured VS Code locations', async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), 'contextforge-vscode-instructions-locations-')); + await mkdir(path.join(rootDir, '.vscode'), { recursive: true }); + await mkdir(path.join(rootDir, 'docs/copilot-rules/review'), { recursive: true }); + await writeFile( + path.join(rootDir, '.vscode/settings.json'), + JSON.stringify({ + 'chat.instructionsFilesLocations': { + 'docs/copilot-rules': true, + '.github/instructions': true, + '~/.config/copilot/instructions': false + } + }) + ); + await writeFile(path.join(rootDir, 'docs/copilot-rules/review.instructions.md'), 'Use the review checklist.\n'); + await writeFile(path.join(rootDir, 'docs/copilot-rules/review/security.instructions.md'), 'Check auth changes.\n'); + + const contextFiles = await listContextFiles(rootDir); + const securityFiles = await listSecurityContextFiles(rootDir); + + expect(contextFiles.map((file) => file.relativePath)).toEqual([ + 'docs/copilot-rules/review.instructions.md', + 'docs/copilot-rules/review/security.instructions.md' + ]); + expect(securityFiles.map((file) => file.relativePath)).toEqual([ + '.vscode/settings.json', + 'docs/copilot-rules/review.instructions.md', + 'docs/copilot-rules/review/security.instructions.md' + ]); + }); + it('discovers Claude Code subagents and custom slash commands', async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), 'contextforge-claude-artifacts-')); await mkdir(path.join(rootDir, '.claude/agents/review'), { recursive: true }); diff --git a/tests/contextHealth.test.ts b/tests/contextHealth.test.ts index 851f306..355e2b4 100644 --- a/tests/contextHealth.test.ts +++ b/tests/contextHealth.test.ts @@ -60,6 +60,24 @@ describe('context health audit', () => { ).toBe(false); }); + it('flags configured Copilot instruction files without applyTo frontmatter', async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), 'contextforge-vscode-applyto-')); + await mkdir(path.join(rootDir, '.vscode'), { recursive: true }); + await mkdir(path.join(rootDir, 'docs/copilot-rules'), { recursive: true }); + await writeFile(path.join(rootDir, '.vscode/settings.json'), '{"chat.instructionsFilesLocations":{"docs/copilot-rules":true}}\n'); + await writeFile(path.join(rootDir, 'docs/copilot-rules/review.instructions.md'), 'Use the review checklist.\n'); + + const audit = await auditContextFiles({ rootDir }); + + expect(audit.findings).toContainEqual( + expect.objectContaining({ + file: 'docs/copilot-rules/review.instructions.md', + type: 'copilot-missing-applyto', + severity: 'medium' + }) + ); + }); + it('audits Copilot prompts, custom agents, and project skills as repo context', async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), 'contextforge-copilot-artifact-health-')); await mkdir(path.join(rootDir, '.github/prompts'), { recursive: true }); diff --git a/tests/init.test.ts b/tests/init.test.ts index 27a9e4c..13ce7fe 100644 --- a/tests/init.test.ts +++ b/tests/init.test.ts @@ -88,7 +88,7 @@ describe('GitHub Action init scaffold', () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), 'contextforge-init-default-ref-')); const result = await scaffoldGithubActionWorkflow({ rootDir }); - expect(await readFile(result.path, 'utf8')).toContain('uses: grnbtqdbyx-create/contextforge@v0.72.0'); + expect(await readFile(result.path, 'utf8')).toContain('uses: grnbtqdbyx-create/contextforge@v0.73.0'); }); it('is available through the init CLI command', async () => {