From feda41500ec53fcd4e3131de7b0441bce08fd3e9 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Fri, 1 May 2026 10:39:00 -0500 Subject: [PATCH] github-copilot-cli: add context, agents, and skills Add support for managing Copilot CLI global instructions via programs.github-copilot-cli.context, written to copilot-instructions.md under COPILOT_HOME. Also manage custom agents and skills from inline definitions, file paths, and directories, with focused tests and a news entry covering the new options. --- .../misc/news/2026/05/2026-05-01_16-15-00.nix | 13 ++ modules/programs/github-copilot-cli.nix | 174 +++++++++++++++++- .../agents/code-reviewer.agent.md | 6 + .../agents/documentation.agent.md | 6 + .../programs/github-copilot-cli/config.nix | 88 +++++++++ .../programs/github-copilot-cli/context.md | 2 + .../programs/github-copilot-cli/default.nix | 4 + .../github-copilot-cli/directories.nix | 37 ++++ .../github-copilot-cli/path-not-directory.nix | 12 ++ .../skills/data-analysis/SKILL.md | 6 + .../skills/data-analysis/notes.txt | 1 + .../skills/release-notes/SKILL.md | 6 + .../skills/release-notes/checklist.md | 5 + .../github-copilot-cli/store-path-dir.nix | 39 ++++ .../store-path-not-directory.nix | 28 +++ .../programs/github-copilot-cli/test-skill.md | 6 + .../github-copilot-cli/xdg-config-dir.nix | 6 + 17 files changed, 437 insertions(+), 2 deletions(-) create mode 100644 modules/misc/news/2026/05/2026-05-01_16-15-00.nix create mode 100644 tests/modules/programs/github-copilot-cli/agents/code-reviewer.agent.md create mode 100644 tests/modules/programs/github-copilot-cli/agents/documentation.agent.md create mode 100644 tests/modules/programs/github-copilot-cli/context.md create mode 100644 tests/modules/programs/github-copilot-cli/directories.nix create mode 100644 tests/modules/programs/github-copilot-cli/path-not-directory.nix create mode 100644 tests/modules/programs/github-copilot-cli/skills/data-analysis/SKILL.md create mode 100644 tests/modules/programs/github-copilot-cli/skills/data-analysis/notes.txt create mode 100644 tests/modules/programs/github-copilot-cli/skills/release-notes/SKILL.md create mode 100644 tests/modules/programs/github-copilot-cli/skills/release-notes/checklist.md create mode 100644 tests/modules/programs/github-copilot-cli/store-path-dir.nix create mode 100644 tests/modules/programs/github-copilot-cli/store-path-not-directory.nix create mode 100644 tests/modules/programs/github-copilot-cli/test-skill.md diff --git a/modules/misc/news/2026/05/2026-05-01_16-15-00.nix b/modules/misc/news/2026/05/2026-05-01_16-15-00.nix new file mode 100644 index 0000000000..10eec48480 --- /dev/null +++ b/modules/misc/news/2026/05/2026-05-01_16-15-00.nix @@ -0,0 +1,13 @@ +{ config, ... }: +{ + time = "2026-05-01T16:15:00+00:00"; + condition = config.programs.github-copilot-cli.enable; + message = '' + The `programs.github-copilot-cli.context`, `programs.github-copilot-cli.agents`, + and `programs.github-copilot-cli.skills` options have been added. + + `context` manages `copilot-instructions.md` under `COPILOT_HOME`, while + `agents` and `skills` let you define custom Copilot CLI agents and skills + from inline definitions or managed directories. + ''; +} diff --git a/modules/programs/github-copilot-cli.nix b/modules/programs/github-copilot-cli.nix index 6ae2e52c33..296eac59fd 100644 --- a/modules/programs/github-copilot-cli.nix +++ b/modules/programs/github-copilot-cli.nix @@ -19,6 +19,11 @@ let upstreamConfigDir = "${config.home.homeDirectory}/.copilot"; + isStorePathString = + content: builtins.isString content && lib.hasPrefix "${builtins.storeDir}/" content; + + isPathLikeContent = content: lib.isPath content || isStorePathString content; + transformSingleServer = _name: server: let @@ -69,7 +74,8 @@ in example = literalExpression ''"''${config.xdg.configHome}/copilot"''; description = '' Directory holding Copilot CLI configuration files such as - {file}`config.json` and {file}`mcp-config.json`. + {file}`config.json`, {file}`mcp-config.json`, and + {file}`copilot-instructions.md`. Defaults to `''${config.xdg.configHome}/copilot` when {option}`home.preferXdgDirectories` is enabled and to `~/.copilot` @@ -136,6 +142,28 @@ in ''; }; + context = mkOption { + type = lib.types.either lib.types.lines lib.types.path; + default = ""; + example = literalExpression '' + ''' + Review the current workspace before making edits. + Prefer actionable findings over general commentary. + ''' + ''; + description = '' + Global instructions for GitHub Copilot CLI. + + The value is either: + - Inline content as a string + - A path to a file containing the content + + The configured content is written to + {file}`copilot-instructions.md` inside + {option}`programs.github-copilot-cli.configDir`. + ''; + }; + mcpServers = mkOption { type = lib.types.attrsOf jsonFormat.type; default = { }; @@ -172,9 +200,111 @@ in for the documentation. ''; }; + + agents = mkOption { + type = lib.types.either (lib.types.attrsOf ( + lib.types.oneOf [ + lib.types.lines + lib.types.path + lib.types.str + ] + )) lib.types.path; + default = { }; + example = literalExpression '' + { + code-reviewer = ''' + --- + description: High signal code review for logic, security, and test gaps. + tools: ["*"] + --- + + Review the current changes and report only actionable findings. + '''; + documentation = ./agents/documentation.agent.md; + } + ''; + description = '' + Custom agents for GitHub Copilot CLI. + + This option can either be: + - An attribute set defining agents + - A path to a directory containing multiple agent files + + If an attribute set is used, the attribute name becomes the agent + filename, and the value is either: + - Inline content as a string (creates + {file}`''${configDir}/agents/.agent.md`) + - A path to a file (creates + {file}`''${configDir}/agents/.agent.md`) + + If a path is used, it is expected to contain agent files. The directory + is symlinked to {file}`''${configDir}/agents/`. + + See + for the documentation. + ''; + }; + + skills = mkOption { + type = lib.types.either (lib.types.attrsOf ( + lib.types.oneOf [ + lib.types.lines + lib.types.path + lib.types.str + ] + )) lib.types.path; + default = { }; + example = literalExpression '' + { + data-analysis = ./skills/data-analysis; + release-notes = ''' + --- + name: release-notes + description: Draft release notes from commits and pull requests. + --- + + Summarize user-visible changes and call out migrations. + '''; + } + ''; + description = '' + Custom skills for GitHub Copilot CLI. + + This option can be either: + - An attribute set defining skills + - A path to a directory containing skill folders + + If an attribute set is used, the attribute name becomes the skill + directory name, and the value is either: + - Inline content as a string (creates + {file}`''${configDir}/skills//SKILL.md`) + - A path to a file (creates + {file}`''${configDir}/skills//SKILL.md`) + - A path to a directory (symlinks + {file}`''${configDir}/skills//` to that directory) + + If a path is used, it is expected to contain one folder per skill name, + each containing a {file}`SKILL.md`. The directory is symlinked to + {file}`''${configDir}/skills/`. + + See + for the documentation. + ''; + }; }; config = mkIf cfg.enable { + assertions = [ + { + assertion = !isPathLikeContent cfg.agents || lib.pathIsDirectory cfg.agents; + message = "`programs.github-copilot-cli.agents` must be a directory when set to a path"; + } + { + assertion = !isPathLikeContent cfg.skills || lib.pathIsDirectory cfg.skills; + message = "`programs.github-copilot-cli.skills` must be a directory when set to a path"; + } + ]; + home.packages = mkIf (cfg.package != null) [ cfg.package ]; home.file = { @@ -193,7 +323,47 @@ in mcpServers = mergedMcpServers; }; }; - }; + + "${cfg.configDir}/copilot-instructions.md" = + if isPathLikeContent cfg.context then + { source = cfg.context; } + else + mkIf (cfg.context != "") { + text = cfg.context; + }; + + "${cfg.configDir}/agents" = mkIf (isPathLikeContent cfg.agents) { + source = cfg.agents; + recursive = true; + }; + + "${cfg.configDir}/skills" = mkIf (isPathLikeContent cfg.skills) { + source = cfg.skills; + recursive = true; + }; + } + // lib.optionalAttrs (builtins.isAttrs cfg.agents) ( + lib.mapAttrs' ( + name: content: + lib.nameValuePair "${cfg.configDir}/agents/${name}.agent.md" ( + if isPathLikeContent content then { source = content; } else { text = content; } + ) + ) cfg.agents + ) + // lib.optionalAttrs (builtins.isAttrs cfg.skills) ( + lib.mapAttrs' ( + name: content: + if isPathLikeContent content && lib.pathIsDirectory content then + lib.nameValuePair "${cfg.configDir}/skills/${name}" { + source = content; + recursive = true; + } + else + lib.nameValuePair "${cfg.configDir}/skills/${name}/SKILL.md" ( + if isPathLikeContent content then { source = content; } else { text = content; } + ) + ) cfg.skills + ); home.sessionVariables = mkIf (cfg.configDir != upstreamConfigDir) { COPILOT_HOME = cfg.configDir; diff --git a/tests/modules/programs/github-copilot-cli/agents/code-reviewer.agent.md b/tests/modules/programs/github-copilot-cli/agents/code-reviewer.agent.md new file mode 100644 index 0000000000..ec50c0f4bc --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/agents/code-reviewer.agent.md @@ -0,0 +1,6 @@ +--- +description: Review changes for correctness and missing tests. +tools: ["*"] +--- + +Prioritize concrete, reproducible findings. diff --git a/tests/modules/programs/github-copilot-cli/agents/documentation.agent.md b/tests/modules/programs/github-copilot-cli/agents/documentation.agent.md new file mode 100644 index 0000000000..68d98e638f --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/agents/documentation.agent.md @@ -0,0 +1,6 @@ +--- +description: Write concise, accurate user-facing documentation updates. +tools: ["*"] +--- + +Focus on examples, migrations, and operator-facing impact. diff --git a/tests/modules/programs/github-copilot-cli/config.nix b/tests/modules/programs/github-copilot-cli/config.nix index 8d725e0d10..3469f36549 100644 --- a/tests/modules/programs/github-copilot-cli/config.nix +++ b/tests/modules/programs/github-copilot-cli/config.nix @@ -1,3 +1,37 @@ +{ pkgs, ... }: +let + inlineAgent = '' + --- + description: Review staged changes for bugs and test gaps. + tools: ["*"] + --- + + Report only actionable findings. + ''; + storeAgentSrc = pkgs.writeText "code-reviewer.agent.md" '' + --- + description: Review changes for bugs and missing tests. + tools: ["*"] + --- + + Report actionable findings only. + ''; + inlineSkill = '' + --- + name: inline-skill + description: Inline skill fixture for Copilot CLI tests. + --- + + Use this skill when validating inline skill materialization. + ''; + inlineContext = '' + Review the repository before making changes. + Report only actionable findings. + ''; + storeSkillSrc = pkgs.writeTextDir "skills/external-skill/SKILL.md" '' + # External Skill + ''; +in { programs.github-copilot-cli = { enable = true; @@ -6,11 +40,65 @@ theme = "dark"; trusted_folders = [ "/home/user/projects" ]; }; + context = inlineContext; + agents = { + inline-reviewer = inlineAgent; + path-reviewer = ./agents/documentation.agent.md; + store-reviewer = "${storeAgentSrc}"; + }; + skills = { + inline-skill = inlineSkill; + path-skill = ./test-skill.md; + dir-skill = ./skills/data-analysis; + store-skill = "${storeSkillSrc}/skills/external-skill"; + }; }; nmt.script = '' assertFileExists home-files/.copilot/config.json assertFileContent home-files/.copilot/config.json ${./expected-config.json} + + assertFileExists home-files/.copilot/copilot-instructions.md + assertFileContent home-files/.copilot/copilot-instructions.md \ + ${builtins.toFile "expected-copilot-instructions.md" inlineContext} + + assertFileExists home-files/.copilot/agents/inline-reviewer.agent.md + assertFileContent home-files/.copilot/agents/inline-reviewer.agent.md \ + ${builtins.toFile "expected-inline-reviewer.agent.md" inlineAgent} + + assertFileExists home-files/.copilot/agents/path-reviewer.agent.md + assertLinkExists home-files/.copilot/agents/path-reviewer.agent.md + assertFileContent home-files/.copilot/agents/path-reviewer.agent.md \ + ${./agents/documentation.agent.md} + + assertFileExists home-files/.copilot/agents/store-reviewer.agent.md + assertLinkExists home-files/.copilot/agents/store-reviewer.agent.md + assertFileContent home-files/.copilot/agents/store-reviewer.agent.md \ + ${storeAgentSrc} + + assertFileExists home-files/.copilot/skills/inline-skill/SKILL.md + assertFileContent home-files/.copilot/skills/inline-skill/SKILL.md \ + ${builtins.toFile "expected-inline-skill.md" inlineSkill} + + assertFileExists home-files/.copilot/skills/path-skill/SKILL.md + assertLinkExists home-files/.copilot/skills/path-skill/SKILL.md + assertFileContent home-files/.copilot/skills/path-skill/SKILL.md \ + ${./test-skill.md} + + assertFileExists home-files/.copilot/skills/dir-skill/SKILL.md + assertFileExists home-files/.copilot/skills/dir-skill/notes.txt + assertLinkExists home-files/.copilot/skills/dir-skill/SKILL.md + assertLinkExists home-files/.copilot/skills/dir-skill/notes.txt + assertFileContent home-files/.copilot/skills/dir-skill/SKILL.md \ + ${./skills/data-analysis/SKILL.md} + assertFileContent home-files/.copilot/skills/dir-skill/notes.txt \ + ${./skills/data-analysis/notes.txt} + + assertFileExists home-files/.copilot/skills/store-skill/SKILL.md + assertLinkExists home-files/.copilot/skills/store-skill/SKILL.md + assertFileContent home-files/.copilot/skills/store-skill/SKILL.md \ + "${storeSkillSrc}/skills/external-skill/SKILL.md" + assertPathNotExists home-files/.copilot/mcp-config.json assertFileNotRegex home-path/etc/profile.d/hm-session-vars.sh 'COPILOT_HOME' ''; diff --git a/tests/modules/programs/github-copilot-cli/context.md b/tests/modules/programs/github-copilot-cli/context.md new file mode 100644 index 0000000000..70ff7bc93b --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/context.md @@ -0,0 +1,2 @@ +Review the current workspace before editing files. +Prefer concrete recommendations and minimal churn. diff --git a/tests/modules/programs/github-copilot-cli/default.nix b/tests/modules/programs/github-copilot-cli/default.nix index 64349b224e..ec9338f3d1 100644 --- a/tests/modules/programs/github-copilot-cli/default.nix +++ b/tests/modules/programs/github-copilot-cli/default.nix @@ -1,6 +1,10 @@ { github-copilot-cli-config = ./config.nix; + github-copilot-cli-directories = ./directories.nix; github-copilot-cli-mcp = ./mcp.nix; github-copilot-cli-mcp-integration = ./mcp-integration.nix; + github-copilot-cli-path-not-directory = ./path-not-directory.nix; + github-copilot-cli-store-path-dir = ./store-path-dir.nix; + github-copilot-cli-store-path-not-directory = ./store-path-not-directory.nix; github-copilot-cli-xdg-config-dir = ./xdg-config-dir.nix; } diff --git a/tests/modules/programs/github-copilot-cli/directories.nix b/tests/modules/programs/github-copilot-cli/directories.nix new file mode 100644 index 0000000000..21d5799eaf --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/directories.nix @@ -0,0 +1,37 @@ +{ + programs.github-copilot-cli = { + enable = true; + agents = ./agents; + skills = ./skills; + }; + + nmt.script = '' + assertFileExists home-files/.copilot/agents/code-reviewer.agent.md + assertLinkExists home-files/.copilot/agents/code-reviewer.agent.md + assertFileContent home-files/.copilot/agents/code-reviewer.agent.md \ + ${./agents/code-reviewer.agent.md} + + assertFileExists home-files/.copilot/agents/documentation.agent.md + assertLinkExists home-files/.copilot/agents/documentation.agent.md + assertFileContent home-files/.copilot/agents/documentation.agent.md \ + ${./agents/documentation.agent.md} + + assertFileExists home-files/.copilot/skills/data-analysis/SKILL.md + assertFileExists home-files/.copilot/skills/data-analysis/notes.txt + assertLinkExists home-files/.copilot/skills/data-analysis/SKILL.md + assertLinkExists home-files/.copilot/skills/data-analysis/notes.txt + assertFileContent home-files/.copilot/skills/data-analysis/SKILL.md \ + ${./skills/data-analysis/SKILL.md} + assertFileContent home-files/.copilot/skills/data-analysis/notes.txt \ + ${./skills/data-analysis/notes.txt} + + assertFileExists home-files/.copilot/skills/release-notes/SKILL.md + assertFileExists home-files/.copilot/skills/release-notes/checklist.md + assertLinkExists home-files/.copilot/skills/release-notes/SKILL.md + assertLinkExists home-files/.copilot/skills/release-notes/checklist.md + assertFileContent home-files/.copilot/skills/release-notes/SKILL.md \ + ${./skills/release-notes/SKILL.md} + assertFileContent home-files/.copilot/skills/release-notes/checklist.md \ + ${./skills/release-notes/checklist.md} + ''; +} diff --git a/tests/modules/programs/github-copilot-cli/path-not-directory.nix b/tests/modules/programs/github-copilot-cli/path-not-directory.nix new file mode 100644 index 0000000000..c45609efea --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/path-not-directory.nix @@ -0,0 +1,12 @@ +{ + programs.github-copilot-cli = { + enable = true; + agents = ./agents/code-reviewer.agent.md; + skills = ./skills/data-analysis/SKILL.md; + }; + + test.asserts.assertions.expected = [ + "`programs.github-copilot-cli.agents` must be a directory when set to a path" + "`programs.github-copilot-cli.skills` must be a directory when set to a path" + ]; +} diff --git a/tests/modules/programs/github-copilot-cli/skills/data-analysis/SKILL.md b/tests/modules/programs/github-copilot-cli/skills/data-analysis/SKILL.md new file mode 100644 index 0000000000..093e06de46 --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/skills/data-analysis/SKILL.md @@ -0,0 +1,6 @@ +--- +name: data-analysis +description: Summarize structured outputs and highlight regressions. +--- + +Use this skill when a task depends on reading tabular or machine-generated data. diff --git a/tests/modules/programs/github-copilot-cli/skills/data-analysis/notes.txt b/tests/modules/programs/github-copilot-cli/skills/data-analysis/notes.txt new file mode 100644 index 0000000000..2123d8c0f1 --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/skills/data-analysis/notes.txt @@ -0,0 +1 @@ +Supplementary notes for the data-analysis skill fixture. diff --git a/tests/modules/programs/github-copilot-cli/skills/release-notes/SKILL.md b/tests/modules/programs/github-copilot-cli/skills/release-notes/SKILL.md new file mode 100644 index 0000000000..f90d79c4bb --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/skills/release-notes/SKILL.md @@ -0,0 +1,6 @@ +--- +name: release-notes +description: Draft release notes from commits and merged pull requests. +--- + +Summarize user-visible changes and call out upgrades or migration notes. diff --git a/tests/modules/programs/github-copilot-cli/skills/release-notes/checklist.md b/tests/modules/programs/github-copilot-cli/skills/release-notes/checklist.md new file mode 100644 index 0000000000..4f2d472a37 --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/skills/release-notes/checklist.md @@ -0,0 +1,5 @@ +# Release Notes Checklist + +- Group changes by user impact. +- Call out migrations. +- Link follow-up issues when relevant. diff --git a/tests/modules/programs/github-copilot-cli/store-path-dir.nix b/tests/modules/programs/github-copilot-cli/store-path-dir.nix new file mode 100644 index 0000000000..2af79f2187 --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/store-path-dir.nix @@ -0,0 +1,39 @@ +{ pkgs, ... }: +let + src = pkgs.writeTextDir "agents/code-reviewer.agent.md" '' + --- + description: Review changes from a store-path directory fixture. + tools: ["*"] + --- + + Focus on correctness and missing coverage. + ''; + + skillsSrc = pkgs.writeTextDir "skills/external-skill/SKILL.md" '' + --- + name: external-skill + description: Store-path directory fixture. + --- + + Exercise top-level store-path directory handling. + ''; +in +{ + programs.github-copilot-cli = { + enable = true; + agents = "${src}/agents"; + skills = "${skillsSrc}/skills"; + }; + + nmt.script = '' + assertFileExists home-files/.copilot/agents/code-reviewer.agent.md + assertLinkExists home-files/.copilot/agents/code-reviewer.agent.md + assertFileContent home-files/.copilot/agents/code-reviewer.agent.md \ + "${src}/agents/code-reviewer.agent.md" + + assertFileExists home-files/.copilot/skills/external-skill/SKILL.md + assertLinkExists home-files/.copilot/skills/external-skill/SKILL.md + assertFileContent home-files/.copilot/skills/external-skill/SKILL.md \ + "${skillsSrc}/skills/external-skill/SKILL.md" + ''; +} diff --git a/tests/modules/programs/github-copilot-cli/store-path-not-directory.nix b/tests/modules/programs/github-copilot-cli/store-path-not-directory.nix new file mode 100644 index 0000000000..83ed01e333 --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/store-path-not-directory.nix @@ -0,0 +1,28 @@ +{ pkgs, ... }: +let + agentFile = pkgs.writeText "code-reviewer.agent.md" '' + --- + description: Invalid top-level agent file fixture. + tools: ["*"] + --- + ''; + + skillFile = pkgs.writeText "SKILL.md" '' + --- + name: invalid-top-level-skill + description: Invalid top-level skill file fixture. + --- + ''; +in +{ + programs.github-copilot-cli = { + enable = true; + agents = "${agentFile}"; + skills = "${skillFile}"; + }; + + test.asserts.assertions.expected = [ + "`programs.github-copilot-cli.agents` must be a directory when set to a path" + "`programs.github-copilot-cli.skills` must be a directory when set to a path" + ]; +} diff --git a/tests/modules/programs/github-copilot-cli/test-skill.md b/tests/modules/programs/github-copilot-cli/test-skill.md new file mode 100644 index 0000000000..2763583f81 --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/test-skill.md @@ -0,0 +1,6 @@ +--- +name: test-skill +description: File-backed skill fixture. +--- + +This skill is defined by a standalone markdown file. diff --git a/tests/modules/programs/github-copilot-cli/xdg-config-dir.nix b/tests/modules/programs/github-copilot-cli/xdg-config-dir.nix index b7c1ddcfdd..3dde2a190a 100644 --- a/tests/modules/programs/github-copilot-cli/xdg-config-dir.nix +++ b/tests/modules/programs/github-copilot-cli/xdg-config-dir.nix @@ -2,6 +2,7 @@ home.preferXdgDirectories = true; programs.github-copilot-cli = { enable = true; + context = ./context.md; settings = { model = "claude-sonnet-4-5"; theme = "dark"; @@ -12,7 +13,12 @@ nmt.script = '' assertFileExists home-files/.config/copilot/config.json assertFileContent home-files/.config/copilot/config.json ${./expected-config.json} + assertFileExists home-files/.config/copilot/copilot-instructions.md + assertLinkExists home-files/.config/copilot/copilot-instructions.md + assertFileContent home-files/.config/copilot/copilot-instructions.md \ + ${./context.md} assertPathNotExists home-files/.copilot/config.json + assertPathNotExists home-files/.copilot/copilot-instructions.md assertFileContains home-path/etc/profile.d/hm-session-vars.sh \ 'export COPILOT_HOME="/home/hm-user/.config/copilot"' '';