feat: add hooks functionality with automated workflows

- Introduced hooks to enable automated workflows triggered by specific events during GitHub Copilot sessions.
- Added documentation for hooks in AGENTS.md and README.md.
- Created a new directory structure for hooks, including README.md and hooks.json files.
- Implemented two example hooks: Session Auto-Commit and Session Logger.
- Developed scripts for logging session events and auto-committing changes.
- Enhanced validation and parsing for hook metadata.
- Updated build and validation scripts to accommodate new hooks functionality.
This commit is contained in:
Aaron Powell
2026-02-09 16:44:53 +11:00
parent d99ba71986
commit acb5ad4ce8
17 changed files with 783 additions and 66 deletions

View File

@@ -8,6 +8,7 @@ The Awesome GitHub Copilot repository is a community-driven collection of custom
- **Prompts** - Task-specific prompts for code generation and problem-solving - **Prompts** - Task-specific prompts for code generation and problem-solving
- **Instructions** - Coding standards and best practices applied to specific file patterns - **Instructions** - Coding standards and best practices applied to specific file patterns
- **Skills** - Self-contained folders with instructions and bundled resources for specialized tasks - **Skills** - Self-contained folders with instructions and bundled resources for specialized tasks
- **Hooks** - Automated workflows triggered by specific events during development
- **Collections** - Curated collections organized around specific themes and workflows - **Collections** - Curated collections organized around specific themes and workflows
## Repository Structure ## Repository Structure
@@ -18,6 +19,7 @@ The Awesome GitHub Copilot repository is a community-driven collection of custom
├── prompts/ # Task-specific prompts (.prompt.md files) ├── prompts/ # Task-specific prompts (.prompt.md files)
├── instructions/ # Coding standards and guidelines (.instructions.md files) ├── instructions/ # Coding standards and guidelines (.instructions.md files)
├── skills/ # Agent Skills folders (each with SKILL.md and optional bundled assets) ├── skills/ # Agent Skills folders (each with SKILL.md and optional bundled assets)
├── hooks/ # Automated workflow hooks (.hook.md files)
├── collections/ # Curated collections of resources (.md files) ├── collections/ # Curated collections of resources (.md files)
├── docs/ # Documentation for different resource types ├── docs/ # Documentation for different resource types
├── eng/ # Build and automation scripts ├── eng/ # Build and automation scripts
@@ -48,9 +50,9 @@ npm run skill:create -- --name <skill-name>
## Development Workflow ## Development Workflow
### Working with Agents, Prompts, Instructions, and Skills ### Working with Agents, Prompts, Instructions, Skills, and Hooks
All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction files (`*.instructions.md`) must include proper markdown front matter. Agent Skills are folders containing a `SKILL.md` file with frontmatter and optional bundled assets: All agent files (`*.agent.md`), prompt files (`*.prompt.md`), instruction files (`*.instructions.md`), and hook files (`*.hook.md`) must include proper markdown front matter. Agent Skills are folders containing a `SKILL.md` file with frontmatter and optional bundled assets:
#### Agent Files (*.agent.md) #### Agent Files (*.agent.md)
- Must have `description` field (wrapped in single quotes) - Must have `description` field (wrapped in single quotes)
@@ -80,9 +82,20 @@ All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction fi
- Asset files should be reasonably sized (under 5MB per file) - Asset files should be reasonably sized (under 5MB per file)
- Skills follow the [Agent Skills specification](https://agentskills.io/specification) - Skills follow the [Agent Skills specification](https://agentskills.io/specification)
#### Hook Folders (hooks/*/README.md)
- Each hook is a folder containing a `README.md` file with frontmatter
- README.md must have `name` field (human-readable name)
- README.md must have `description` field (wrapped in single quotes, not empty)
- Must include a `hooks.json` file with hook configuration (hook events extracted from this file)
- Folder names should be lower case with words separated by hyphens
- Can include bundled assets (scripts, utilities, configuration files)
- Bundled scripts should be referenced in the README.md and hooks.json
- Follow the [GitHub Copilot hooks specification](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks)
- Optionally includes `tags` field for categorization
### Adding New Resources ### Adding New Resources
When adding a new agent, prompt, instruction, or skill: When adding a new agent, prompt, instruction, skill, or hook:
**For Agents, Prompts, and Instructions:** **For Agents, Prompts, and Instructions:**
1. Create the file with proper front matter 1. Create the file with proper front matter
@@ -90,6 +103,16 @@ When adding a new agent, prompt, instruction, or skill:
3. Update the README.md by running: `npm run build` 3. Update the README.md by running: `npm run build`
4. Verify the resource appears in the generated README 4. Verify the resource appears in the generated README
**For Hooks:**
1. Create a new folder in `hooks/` with a descriptive name
2. Create `README.md` with proper frontmatter (name, description, hooks, tags)
3. Create `hooks.json` with hook configuration following GitHub Copilot hooks spec
4. Add any bundled scripts or assets to the folder
5. Make scripts executable: `chmod +x script.sh`
6. Update the README.md by running: `npm run build`
7. Verify the hook appears in the generated README
**For Skills:** **For Skills:**
1. Run `npm run skill:create` to scaffold a new skill folder 1. Run `npm run skill:create` to scaffold a new skill folder
2. Edit the generated SKILL.md file with your instructions 2. Edit the generated SKILL.md file with your instructions
@@ -186,6 +209,16 @@ For skills (skills/*/):
- [ ] Any bundled assets are referenced in SKILL.md - [ ] Any bundled assets are referenced in SKILL.md
- [ ] Bundled assets are under 5MB per file - [ ] Bundled assets are under 5MB per file
For hook folders (hooks/*/):
- [ ] Folder contains a README.md file with markdown front matter
- [ ] Has `name` field with human-readable name
- [ ] Has non-empty `description` field wrapped in single quotes
- [ ] Has `hooks.json` file with valid hook configuration (hook events extracted from this file)
- [ ] Folder name is lower case with hyphens
- [ ] Any bundled scripts are executable and referenced in README.md
- [ ] Follows [GitHub Copilot hooks specification](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks)
- [ ] Optionally includes `tags` array field for categorization
## Contributing ## Contributing
This is a community-driven project. Contributions are welcome! Please see: This is a community-driven project. Contributions are welcome! Please see:

View File

@@ -11,6 +11,7 @@ This repository provides a comprehensive toolkit for enhancing GitHub Copilot wi
- **👉 [Awesome Agents](docs/README.agents.md)** - Specialized GitHub Copilot agents that integrate with MCP servers to provide enhanced capabilities for specific workflows and tools - **👉 [Awesome Agents](docs/README.agents.md)** - Specialized GitHub Copilot agents that integrate with MCP servers to provide enhanced capabilities for specific workflows and tools
- **👉 [Awesome Prompts](docs/README.prompts.md)** - Focused, task-specific prompts for generating code, documentation, and solving specific problems - **👉 [Awesome Prompts](docs/README.prompts.md)** - Focused, task-specific prompts for generating code, documentation, and solving specific problems
- **👉 [Awesome Instructions](docs/README.instructions.md)** - Comprehensive coding standards and best practices that apply to specific file patterns or entire projects - **👉 [Awesome Instructions](docs/README.instructions.md)** - Comprehensive coding standards and best practices that apply to specific file patterns or entire projects
- **👉 [Awesome Hooks](docs/README.hooks.md)** - Automated workflows triggered by specific events during development, testing, and deployment
- **👉 [Awesome Skills](docs/README.skills.md)** - Self-contained folders with instructions and bundled resources that enhance AI capabilities for specialized tasks - **👉 [Awesome Skills](docs/README.skills.md)** - Self-contained folders with instructions and bundled resources that enhance AI capabilities for specialized tasks
- **👉 [Awesome Collections](docs/README.collections.md)** - Curated collections of related prompts, instructions, agents, and skills organized around specific themes and workflows - **👉 [Awesome Collections](docs/README.collections.md)** - Curated collections of related prompts, instructions, agents, and skills organized around specific themes and workflows
- **👉 [Awesome Cookbook Recipes](cookbook/README.md)** - Practical, copy-paste-ready code snippets and real-world examples for working with GitHub Copilot tools and features - **👉 [Awesome Cookbook Recipes](cookbook/README.md)** - Practical, copy-paste-ready code snippets and real-world examples for working with GitHub Copilot tools and features
@@ -96,6 +97,10 @@ Use the `/` command in GitHub Copilot Chat to access prompts:
Instructions automatically apply to files based on their patterns and provide contextual guidance for coding standards, frameworks, and best practices. Instructions automatically apply to files based on their patterns and provide contextual guidance for coding standards, frameworks, and best practices.
### 🪝 Hooks
Hooks enable automated workflows triggered by specific events during GitHub Copilot coding agent sessions (like sessionStart, sessionEnd, userPromptSubmitted). They can automate tasks like logging, auto-committing changes, or integrating with external services.
## 🎯 Why Use Awesome GitHub Copilot? ## 🎯 Why Use Awesome GitHub Copilot?
- **Productivity**: Pre-built agents, prompts and instructions save time and provide consistent results. - **Productivity**: Pre-built agents, prompts and instructions save time and provide consistent results.
@@ -107,7 +112,7 @@ Instructions automatically apply to files based on their patterns and provide co
We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details on how to: We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details on how to:
- Add new prompts, instructions, agents, or skills - Add new prompts, instructions, hooks, agents, or skills
- Improve existing content - Improve existing content
- Report issues or suggest enhancements - Report issues or suggest enhancements
@@ -148,7 +153,7 @@ The customizations in this repository are sourced from and created by third-part
--- ---
**Ready to supercharge your coding experience?** Start exploring our [prompts](docs/README.prompts.md), [instructions](docs/README.instructions.md), and [custom agents](docs/README.agents.md)! **Ready to supercharge your coding experience?** Start exploring our [prompts](docs/README.prompts.md), [instructions](docs/README.instructions.md), [hooks](docs/README.hooks.md), and [custom agents](docs/README.agents.md)!
## Contributors ✨ ## Contributors ✨

31
docs/README.hooks.md Normal file
View File

@@ -0,0 +1,31 @@
# 🪝 Hooks
Hooks enable automated workflows triggered by specific events during GitHub Copilot coding agent sessions, such as session start, session end, user prompts, and tool usage.
### How to Use Hooks
**What's Included:**
- Each hook is a folder containing a `README.md` file and a `hooks.json` configuration
- Hooks may include helper scripts, utilities, or other bundled assets
- Hooks follow the [GitHub Copilot hooks specification](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks)
**To Install:**
- Copy the hook folder to your repository's `.github/hooks/` directory
- Ensure any bundled scripts are executable (`chmod +x script.sh`)
- Commit the hook to your repository's default branch
**To Activate/Use:**
- Hooks automatically execute during Copilot coding agent sessions
- Configure hook events in the `hooks.json` file
- Available events: `sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`, `postToolUse`, `errorOccurred`
**When to Use:**
- Automate session logging and audit trails
- Auto-commit changes at session end
- Track usage analytics
- Integrate with external tools and services
- Custom session workflows
| Name | Description | Events | Bundled Assets |
| ---- | ----------- | ------ | -------------- |
| [Session Auto-Commit](../hooks/session-auto-commit/README.md) | Automatically commits and pushes changes when a Copilot coding agent session ends | sessionEnd | `auto-commit.sh`<br />`hooks.json` |
| [Session Logger](../hooks/session-logger/README.md) | Logs all Copilot coding agent session activity for audit and analysis | sessionStart, sessionEnd, userPromptSubmitted | `hooks.json`<br />`log-prompt.sh`<br />`log-session-end.sh`<br />`log-session-start.sh` |

View File

@@ -158,6 +158,8 @@ function getDisplayName(filePath, kind) {
return basename.replace(".agent.md", ""); return basename.replace(".agent.md", "");
} else if (kind === "instruction") { } else if (kind === "instruction") {
return basename.replace(".instructions.md", ""); return basename.replace(".instructions.md", "");
} else if (kind === "hook") {
return basename.replace(".hook.md", "");
} else if (kind === "skill") { } else if (kind === "skill") {
return path.basename(filePath); return path.basename(filePath);
} }
@@ -221,6 +223,23 @@ function generateReadme(collection, items) {
lines.push(""); lines.push("");
} }
// Hooks
const hooks = items.filter((item) => item.kind === "hook");
if (hooks.length > 0) {
lines.push("### Hooks");
lines.push("");
lines.push("| Hook | Description | Event |");
lines.push("|------|-------------|-------|");
for (const item of hooks) {
const name = getDisplayName(item.path, "hook");
const description =
item.frontmatter?.description || item.frontmatter?.name || name;
const event = item.frontmatter?.event || "N/A";
lines.push(`| \`${name}\` | ${description} | ${event} |`);
}
lines.push("");
}
// Skills // Skills
const skills = items.filter((item) => item.kind === "skill"); const skills = items.filter((item) => item.kind === "skill");
if (skills.length > 0) { if (skills.length > 0) {

View File

@@ -1,6 +1,5 @@
import path from "path"; import path, { dirname } from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
@@ -100,6 +99,34 @@ Skills differ from other primitives by supporting bundled assets (scripts, code
- Browse the skills table below to find relevant capabilities - Browse the skills table below to find relevant capabilities
- Copy the skill folder to your local skills directory - Copy the skill folder to your local skills directory
- Reference skills in your prompts or let the agent discover them automatically`, - Reference skills in your prompts or let the agent discover them automatically`,
hooksSection: `## 🪝 Hooks
Hooks enable automated workflows triggered by specific events during GitHub Copilot coding agent sessions, such as session start, session end, user prompts, and tool usage.`,
hooksUsage: `### How to Use Hooks
**What's Included:**
- Each hook is a folder containing a \`README.md\` file and a \`hooks.json\` configuration
- Hooks may include helper scripts, utilities, or other bundled assets
- Hooks follow the [GitHub Copilot hooks specification](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks)
**To Install:**
- Copy the hook folder to your repository's \`.github/hooks/\` directory
- Ensure any bundled scripts are executable (\`chmod +x script.sh\`)
- Commit the hook to your repository's default branch
**To Activate/Use:**
- Hooks automatically execute during Copilot coding agent sessions
- Configure hook events in the \`hooks.json\` file
- Available events: \`sessionStart\`, \`sessionEnd\`, \`userPromptSubmitted\`, \`preToolUse\`, \`postToolUse\`, \`errorOccurred\`
**When to Use:**
- Automate session logging and audit trails
- Auto-commit changes at session end
- Track usage analytics
- Integrate with external tools and services
- Custom session workflows`,
}; };
const vscodeInstallImage = const vscodeInstallImage =
@@ -115,6 +142,7 @@ const AKA_INSTALL_URLS = {
instructions: "https://aka.ms/awesome-copilot/install/instructions", instructions: "https://aka.ms/awesome-copilot/install/instructions",
prompt: "https://aka.ms/awesome-copilot/install/prompt", prompt: "https://aka.ms/awesome-copilot/install/prompt",
agent: "https://aka.ms/awesome-copilot/install/agent", agent: "https://aka.ms/awesome-copilot/install/agent",
hook: "https://aka.ms/awesome-copilot/install/hook",
}; };
const ROOT_FOLDER = path.join(__dirname, ".."); const ROOT_FOLDER = path.join(__dirname, "..");
@@ -122,6 +150,7 @@ const INSTRUCTIONS_DIR = path.join(ROOT_FOLDER, "instructions");
const PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts"); const PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts");
const AGENTS_DIR = path.join(ROOT_FOLDER, "agents"); const AGENTS_DIR = path.join(ROOT_FOLDER, "agents");
const SKILLS_DIR = path.join(ROOT_FOLDER, "skills"); const SKILLS_DIR = path.join(ROOT_FOLDER, "skills");
const HOOKS_DIR = path.join(ROOT_FOLDER, "hooks");
const COLLECTIONS_DIR = path.join(ROOT_FOLDER, "collections"); const COLLECTIONS_DIR = path.join(ROOT_FOLDER, "collections");
const COOKBOOK_DIR = path.join(ROOT_FOLDER, "cookbook"); const COOKBOOK_DIR = path.join(ROOT_FOLDER, "cookbook");
const MAX_COLLECTION_ITEMS = 50; const MAX_COLLECTION_ITEMS = 50;
@@ -135,23 +164,7 @@ const SKILL_DESCRIPTION_MAX_LENGTH = 1024;
const DOCS_DIR = path.join(ROOT_FOLDER, "docs"); const DOCS_DIR = path.join(ROOT_FOLDER, "docs");
export { export {
TEMPLATES, AGENTS_DIR, AKA_INSTALL_URLS, COLLECTIONS_DIR,
vscodeInstallImage, COOKBOOK_DIR, DOCS_DIR, HOOKS_DIR, INSTRUCTIONS_DIR, MAX_COLLECTION_ITEMS, PROMPTS_DIR, repoBaseUrl, ROOT_FOLDER, SKILL_DESCRIPTION_MAX_LENGTH, SKILL_DESCRIPTION_MIN_LENGTH, SKILL_NAME_MAX_LENGTH, SKILL_NAME_MIN_LENGTH, SKILLS_DIR, TEMPLATES, vscodeInsidersInstallImage, vscodeInstallImage
vscodeInsidersInstallImage,
repoBaseUrl,
AKA_INSTALL_URLS,
ROOT_FOLDER,
INSTRUCTIONS_DIR,
PROMPTS_DIR,
AGENTS_DIR,
SKILLS_DIR,
COLLECTIONS_DIR,
COOKBOOK_DIR,
MAX_COLLECTION_ITEMS,
SKILL_NAME_MIN_LENGTH,
SKILL_NAME_MAX_LENGTH,
SKILL_DESCRIPTION_MIN_LENGTH,
SKILL_DESCRIPTION_MAX_LENGTH,
DOCS_DIR,
}; };

View File

@@ -7,24 +7,26 @@
*/ */
import fs from "fs"; import fs from "fs";
import path, { dirname } from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { import {
AGENTS_DIR, AGENTS_DIR,
COLLECTIONS_DIR, COLLECTIONS_DIR,
COOKBOOK_DIR, COOKBOOK_DIR,
INSTRUCTIONS_DIR, HOOKS_DIR,
PROMPTS_DIR, INSTRUCTIONS_DIR,
ROOT_FOLDER, PROMPTS_DIR,
SKILLS_DIR, ROOT_FOLDER,
SKILLS_DIR
} from "./constants.mjs"; } from "./constants.mjs";
import {
parseCollectionYaml,
parseFrontmatter,
parseSkillMetadata,
parseYamlFile,
} from "./yaml-parser.mjs";
import { getGitFileDates } from "./utils/git-dates.mjs"; import { getGitFileDates } from "./utils/git-dates.mjs";
import {
parseCollectionYaml,
parseFrontmatter,
parseSkillMetadata,
parseHookMetadata,
parseYamlFile,
} from "./yaml-parser.mjs";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@@ -122,6 +124,75 @@ function generateAgentsData(gitDates) {
}; };
} }
/**
* Generate hooks metadata
*/
/**
* Generate hooks metadata (similar to skills - folder-based)
*/
function generateHooksData(gitDates) {
const hooks = [];
// Check if hooks directory exists
if (!fs.existsSync(HOOKS_DIR)) {
return {
items: hooks,
filters: {
hooks: [],
tags: [],
},
};
}
// Get all hook folders (directories)
const hookFolders = fs.readdirSync(HOOKS_DIR).filter((file) => {
const filePath = path.join(HOOKS_DIR, file);
return fs.statSync(filePath).isDirectory();
});
// Track all unique values for filters
const allHookTypes = new Set();
const allTags = new Set();
for (const folder of hookFolders) {
const hookPath = path.join(HOOKS_DIR, folder);
const metadata = parseHookMetadata(hookPath);
if (!metadata) continue;
const relativePath = path
.relative(ROOT_FOLDER, hookPath)
.replace(/\\/g, "/");
const readmeRelativePath = `${relativePath}/README.md`;
// Track unique values
(metadata.hooks || []).forEach((h) => allHookTypes.add(h));
(metadata.tags || []).forEach((t) => allTags.add(t));
hooks.push({
id: folder,
title: metadata.name,
description: metadata.description,
hooks: metadata.hooks || [],
tags: metadata.tags || [],
assets: metadata.assets || [],
path: relativePath,
readmeFile: readmeRelativePath,
lastUpdated: gitDates.get(readmeRelativePath) || null,
});
}
// Sort and return with filter metadata
const sortedHooks = hooks.sort((a, b) => a.title.localeCompare(b.title));
return {
items: sortedHooks,
filters: {
hooks: Array.from(allHookTypes).sort(),
tags: Array.from(allTags).sort(),
},
};
}
/** /**
* Generate prompts metadata * Generate prompts metadata
*/ */
@@ -539,6 +610,7 @@ function generateSearchIndex(
agents, agents,
prompts, prompts,
instructions, instructions,
hooks,
skills, skills,
collections collections
) { ) {
@@ -584,6 +656,20 @@ function generateSearchIndex(
}); });
} }
for (const hook of hooks) {
index.push({
type: "hook",
id: hook.id,
title: hook.title,
description: hook.description,
path: hook.readmeFile,
lastUpdated: hook.lastUpdated,
searchText: `${hook.title} ${hook.description} ${hook.hooks.join(
" "
)} ${hook.tags.join(" ")}`.toLowerCase(),
});
}
for (const skill of skills) { for (const skill of skills) {
index.push({ index.push({
type: "skill", type: "skill",
@@ -720,7 +806,7 @@ async function main() {
// Load git dates for all resource files (single efficient git command) // Load git dates for all resource files (single efficient git command)
console.log("Loading git history for last updated dates..."); console.log("Loading git history for last updated dates...");
const gitDates = getGitFileDates( const gitDates = getGitFileDates(
["agents/", "prompts/", "instructions/", "skills/", "collections/"], ["agents/", "prompts/", "instructions/", "hooks/", "skills/", "collections/"],
ROOT_FOLDER ROOT_FOLDER
); );
console.log(`✓ Loaded dates for ${gitDates.size} files\n`); console.log(`✓ Loaded dates for ${gitDates.size} files\n`);
@@ -732,6 +818,12 @@ async function main() {
`✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)` `✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)`
); );
const hooksData = generateHooksData(gitDates);
const hooks = hooksData.items;
console.log(
`✓ Generated ${hooks.length} hooks (${hooksData.filters.hooks.length} hook types, ${hooksData.filters.tags.length} tags)`
);
const promptsData = generatePromptsData(gitDates); const promptsData = generatePromptsData(gitDates);
const prompts = promptsData.items; const prompts = promptsData.items;
console.log( console.log(
@@ -771,6 +863,7 @@ async function main() {
agents, agents,
prompts, prompts,
instructions, instructions,
hooks,
skills, skills,
collections collections
); );
@@ -782,6 +875,11 @@ async function main() {
JSON.stringify(agentsData, null, 2) JSON.stringify(agentsData, null, 2)
); );
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "hooks.json"),
JSON.stringify(hooksData, null, 2)
);
fs.writeFileSync( fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "prompts.json"), path.join(WEBSITE_DATA_DIR, "prompts.json"),
JSON.stringify(promptsData, null, 2) JSON.stringify(promptsData, null, 2)

View File

@@ -4,24 +4,26 @@ import fs from "fs";
import path, { dirname } from "path"; import path, { dirname } from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { import {
AGENTS_DIR, AGENTS_DIR,
AKA_INSTALL_URLS, AKA_INSTALL_URLS,
COLLECTIONS_DIR, COLLECTIONS_DIR,
DOCS_DIR, DOCS_DIR,
INSTRUCTIONS_DIR, HOOKS_DIR,
PROMPTS_DIR, INSTRUCTIONS_DIR,
repoBaseUrl, PROMPTS_DIR,
ROOT_FOLDER, repoBaseUrl,
SKILLS_DIR, ROOT_FOLDER,
TEMPLATES, SKILLS_DIR,
vscodeInsidersInstallImage, TEMPLATES,
vscodeInstallImage, vscodeInsidersInstallImage,
vscodeInstallImage,
} from "./constants.mjs"; } from "./constants.mjs";
import { import {
extractMcpServerConfigs, extractMcpServerConfigs,
parseCollectionYaml, parseCollectionYaml,
parseFrontmatter, parseFrontmatter,
parseSkillMetadata, parseSkillMetadata,
parseHookMetadata,
} from "./yaml-parser.mjs"; } from "./yaml-parser.mjs";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@@ -515,6 +517,67 @@ function generateAgentsSection(agentsDir, registryNames = []) {
}); });
} }
/**
* Generate the hooks section with a table of all hooks
*/
function generateHooksSection(hooksDir) {
if (!fs.existsSync(hooksDir)) {
console.log(`Hooks directory does not exist: ${hooksDir}`);
return "";
}
// Get all hook folders (directories)
const hookFolders = fs.readdirSync(hooksDir).filter((file) => {
const filePath = path.join(hooksDir, file);
return fs.statSync(filePath).isDirectory();
});
// Parse each hook folder
const hookEntries = hookFolders
.map((folder) => {
const hookPath = path.join(hooksDir, folder);
const metadata = parseHookMetadata(hookPath);
if (!metadata) return null;
return {
folder,
name: metadata.name,
description: metadata.description,
hooks: metadata.hooks,
tags: metadata.tags,
assets: metadata.assets,
};
})
.filter((entry) => entry !== null)
.sort((a, b) => a.name.localeCompare(b.name));
console.log(`Found ${hookEntries.length} hook(s)`);
if (hookEntries.length === 0) {
return "";
}
// Create table header
let content =
"| Name | Description | Events | Bundled Assets |\n| ---- | ----------- | ------ | -------------- |\n";
// Generate table rows for each hook
for (const hook of hookEntries) {
const link = `../hooks/${hook.folder}/README.md`;
const events = hook.hooks.length > 0 ? hook.hooks.join(", ") : "N/A";
const assetsList =
hook.assets.length > 0
? hook.assets.map((a) => `\`${a}\``).join("<br />")
: "None";
content += `| [${hook.name}](${link}) | ${formatTableCell(
hook.description
)} | ${events} | ${assetsList} |\n`;
}
return `${TEMPLATES.hooksSection}\n${TEMPLATES.hooksUsage}\n\n${content}`;
}
/** /**
* Generate the skills section with a table of all skills * Generate the skills section with a table of all skills
*/ */
@@ -1002,6 +1065,7 @@ async function main() {
); );
const promptsHeader = TEMPLATES.promptsSection.replace(/^##\s/m, "# "); const promptsHeader = TEMPLATES.promptsSection.replace(/^##\s/m, "# ");
const agentsHeader = TEMPLATES.agentsSection.replace(/^##\s/m, "# "); const agentsHeader = TEMPLATES.agentsSection.replace(/^##\s/m, "# ");
const hooksHeader = TEMPLATES.hooksSection.replace(/^##\s/m, "# ");
const skillsHeader = TEMPLATES.skillsSection.replace(/^##\s/m, "# "); const skillsHeader = TEMPLATES.skillsSection.replace(/^##\s/m, "# ");
const collectionsHeader = TEMPLATES.collectionsSection.replace( const collectionsHeader = TEMPLATES.collectionsSection.replace(
/^##\s/m, /^##\s/m,
@@ -1031,6 +1095,15 @@ async function main() {
registryNames registryNames
); );
// Generate hooks README
const hooksReadme = buildCategoryReadme(
generateHooksSection,
HOOKS_DIR,
hooksHeader,
TEMPLATES.hooksUsage,
registryNames
);
// Generate skills README // Generate skills README
const skillsReadme = buildCategoryReadme( const skillsReadme = buildCategoryReadme(
generateSkillsSection, generateSkillsSection,
@@ -1061,6 +1134,7 @@ async function main() {
); );
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme); writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme); writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
writeFileIfChanged(path.join(DOCS_DIR, "README.hooks.md"), hooksReadme);
writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme); writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme);
writeFileIfChanged( writeFileIfChanged(
path.join(DOCS_DIR, "README.collections.md"), path.join(DOCS_DIR, "README.collections.md"),

View File

@@ -3,9 +3,9 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { import {
COLLECTIONS_DIR, COLLECTIONS_DIR,
MAX_COLLECTION_ITEMS, MAX_COLLECTION_ITEMS,
ROOT_FOLDER, ROOT_FOLDER,
} from "./constants.mjs"; } from "./constants.mjs";
import { parseCollectionYaml, parseFrontmatter } from "./yaml-parser.mjs"; import { parseCollectionYaml, parseFrontmatter } from "./yaml-parser.mjs";
@@ -155,6 +155,41 @@ function validateAgentFile(filePath) {
} }
} }
function validateHookFile(filePath) {
try {
const hook = parseFrontmatter(filePath);
if (!hook) {
return `Item ${filePath} hook file could not be parsed`;
}
// Validate name field
if (!hook.name || typeof hook.name !== "string") {
return `Item ${filePath} hook must have a 'name' field`;
}
if (hook.name.length < 1 || hook.name.length > 50) {
return `Item ${filePath} hook name must be between 1 and 50 characters`;
}
// Validate description field
if (!hook.description || typeof hook.description !== "string") {
return `Item ${filePath} hook must have a 'description' field`;
}
if (hook.description.length < 1 || hook.description.length > 500) {
return `Item ${filePath} hook description must be between 1 and 500 characters`;
}
// Validate event field (optional but recommended)
if (hook.event !== undefined && typeof hook.event !== "string") {
return `Item ${filePath} hook 'event' must be a string`;
}
return null; // All validations passed
} catch (error) {
return `Item ${filePath} hook file validation failed: ${error.message}`;
}
}
function validateCollectionItems(items) { function validateCollectionItems(items) {
if (!items || !Array.isArray(items)) { if (!items || !Array.isArray(items)) {
return "Items is required and must be an array"; return "Items is required and must be an array";
@@ -177,10 +212,10 @@ function validateCollectionItems(items) {
if (!item.kind || typeof item.kind !== "string") { if (!item.kind || typeof item.kind !== "string") {
return `Item ${i + 1} must have a kind string`; return `Item ${i + 1} must have a kind string`;
} }
if (!["prompt", "instruction", "agent", "skill"].includes(item.kind)) { if (!["prompt", "instruction", "agent", "skill", "hook"].includes(item.kind)) {
return `Item ${ return `Item ${
i + 1 i + 1
} kind must be one of: prompt, instruction, agent, skill`; } kind must be one of: prompt, instruction, agent, skill, hook`;
} }
// Validate file path exists // Validate file path exists
@@ -208,6 +243,11 @@ function validateCollectionItems(items) {
i + 1 i + 1
} kind is "agent" but path doesn't end with .agent.md`; } kind is "agent" but path doesn't end with .agent.md`;
} }
if (item.kind === "hook" && !item.path.endsWith(".hook.md")) {
return `Item ${
i + 1
} kind is "hook" but path doesn't end with .hook.md`;
}
// Validate agent-specific frontmatter // Validate agent-specific frontmatter
if (item.kind === "agent") { if (item.kind === "agent") {
@@ -216,6 +256,14 @@ function validateCollectionItems(items) {
return agentValidation; return agentValidation;
} }
} }
// Validate hook-specific frontmatter
if (item.kind === "hook") {
const hookValidation = validateHookFile(filePath, i + 1);
if (hookValidation) {
return hookValidation;
}
}
} }
return null; return null;
} }

View File

@@ -1,7 +1,7 @@
// YAML parser for collection files and frontmatter parsing using vfile-matter // YAML parser for collection files and frontmatter parsing using vfile-matter
import fs from "fs"; import fs from "fs";
import path from "path";
import yaml from "js-yaml"; import yaml from "js-yaml";
import path from "path";
import { VFile } from "vfile"; import { VFile } from "vfile";
import { matter } from "vfile-matter"; import { matter } from "vfile-matter";
@@ -173,7 +173,7 @@ function parseSkillMetadata(skillPath) {
const relativePath = path.relative(skillPath, filePath); const relativePath = path.relative(skillPath, filePath);
if (relativePath !== "SKILL.md") { if (relativePath !== "SKILL.md") {
// Normalize path separators to forward slashes for cross-platform consistency // Normalize path separators to forward slashes for cross-platform consistency
arrayOfFiles.push(relativePath.replace(/\\/g, '/')); arrayOfFiles.push(relativePath.replace(/\\/g, "/"));
} }
} }
}); });
@@ -195,6 +195,83 @@ function parseSkillMetadata(skillPath) {
); );
} }
/**
* Parse hook metadata from a hook folder (similar to skills)
* @param {string} hookPath - Path to the hook folder
* @returns {object|null} Hook metadata or null on error
*/
function parseHookMetadata(hookPath) {
return safeFileOperation(
() => {
const readmeFile = path.join(hookPath, "README.md");
if (!fs.existsSync(readmeFile)) {
return null;
}
const frontmatter = parseFrontmatter(readmeFile);
// Validate required fields
if (!frontmatter?.name || !frontmatter?.description) {
console.warn(
`Invalid hook at ${hookPath}: missing name or description in frontmatter`
);
return null;
}
// Extract hook events from hooks.json if it exists
let hookEvents = [];
const hooksJsonPath = path.join(hookPath, "hooks.json");
if (fs.existsSync(hooksJsonPath)) {
try {
const hooksJsonContent = fs.readFileSync(hooksJsonPath, "utf8");
const hooksConfig = JSON.parse(hooksJsonContent);
// Extract all hook event names from the hooks object
if (hooksConfig.hooks && typeof hooksConfig.hooks === "object") {
hookEvents = Object.keys(hooksConfig.hooks);
}
} catch (error) {
console.warn(
`Failed to parse hooks.json at ${hookPath}: ${error.message}`
);
}
}
// List bundled assets (all files except README.md), recursing through subdirectories
const getAllFiles = (dirPath, arrayOfFiles = []) => {
const files = fs.readdirSync(dirPath);
files.forEach((file) => {
const filePath = path.join(dirPath, file);
if (fs.statSync(filePath).isDirectory()) {
arrayOfFiles = getAllFiles(filePath, arrayOfFiles);
} else {
const relativePath = path.relative(hookPath, filePath);
if (relativePath !== "README.md") {
// Normalize path separators to forward slashes for cross-platform consistency
arrayOfFiles.push(relativePath.replace(/\\/g, "/"));
}
}
});
return arrayOfFiles;
};
const assets = getAllFiles(hookPath).sort();
return {
name: frontmatter.name,
description: frontmatter.description,
hooks: hookEvents,
tags: frontmatter.tags || [],
assets,
path: hookPath,
};
},
hookPath,
null
);
}
/** /**
* Parse a generic YAML file (used for tools.yml and other config files) * Parse a generic YAML file (used for tools.yml and other config files)
* @param {string} filePath - Path to the YAML file * @param {string} filePath - Path to the YAML file
@@ -212,12 +289,13 @@ function parseYamlFile(filePath) {
} }
export { export {
extractAgentMetadata,
extractMcpServerConfigs,
extractMcpServers,
parseCollectionYaml, parseCollectionYaml,
parseFrontmatter, parseFrontmatter,
extractAgentMetadata,
extractMcpServers,
extractMcpServerConfigs,
parseSkillMetadata, parseSkillMetadata,
parseHookMetadata,
parseYamlFile, parseYamlFile,
safeFileOperation, safeFileOperation,
}; };

View File

@@ -0,0 +1,90 @@
---
name: 'Session Auto-Commit'
description: 'Automatically commits and pushes changes when a Copilot coding agent session ends'
tags: ['automation', 'git', 'productivity']
---
# Session Auto-Commit Hook
Automatically commits and pushes changes when a GitHub Copilot coding agent session ends, ensuring your work is always saved and backed up.
## Overview
This hook runs at the end of each Copilot coding agent session and automatically:
- Detects if there are uncommitted changes
- Stages all changes
- Creates a timestamped commit
- Pushes to the remote repository
## Features
- **Automatic Backup**: Never lose work from a Copilot session
- **Timestamped Commits**: Each auto-commit includes the session end time
- **Safe Execution**: Only commits when there are actual changes
- **Error Handling**: Gracefully handles push failures
## Installation
1. Copy this hook folder to your repository's `.github/hooks/` directory:
```bash
cp -r hooks/session-auto-commit .github/hooks/
```
2. Ensure the script is executable:
```bash
chmod +x .github/hooks/session-auto-commit/auto-commit.sh
```
3. Commit the hook configuration to your repository's default branch
## Configuration
The hook is configured in `hooks.json` to run on the `sessionEnd` event:
```json
{
"version": 1,
"hooks": {
"sessionEnd": [
{
"type": "command",
"bash": ".github/hooks/session-auto-commit/auto-commit.sh",
"timeoutSec": 30
}
]
}
}
```
## How It Works
1. When a Copilot coding agent session ends, the hook executes
2. Checks if inside a Git repository
3. Detects uncommitted changes using `git status`
4. Stages all changes with `git add -A`
5. Creates a commit with format: `auto-commit: YYYY-MM-DD HH:MM:SS`
6. Attempts to push to remote
7. Reports success or failure
## Customization
You can customize the hook by modifying `auto-commit.sh`:
- **Commit Message Format**: Change the timestamp format or message prefix
- **Selective Staging**: Use specific git add patterns instead of `-A`
- **Branch Selection**: Push to specific branches only
- **Notifications**: Add desktop notifications or Slack messages
## Disabling
To temporarily disable auto-commits:
1. Remove or comment out the `sessionEnd` hook in `hooks.json`
2. Or set an environment variable: `export SKIP_AUTO_COMMIT=true`
## Notes
- The hook uses `--no-verify` to avoid triggering pre-commit hooks
- Failed pushes won't block session termination
- Requires appropriate git credentials configured
- Works with both Copilot coding agent and GitHub Copilot CLI

View File

@@ -0,0 +1,45 @@
#!/bin/bash
# Session Auto-Commit Hook
# Automatically commits and pushes changes when a Copilot session ends
set -euo pipefail
# Check if SKIP_AUTO_COMMIT is set
if [[ "${SKIP_AUTO_COMMIT:-}" == "true" ]]; then
echo "⏭️ Auto-commit skipped (SKIP_AUTO_COMMIT=true)"
exit 0
fi
# Check if we're in a git repository
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
echo "⚠️ Not in a git repository"
exit 0
fi
# Check for uncommitted changes
if [[ -z "$(git status --porcelain)" ]]; then
echo "✨ No changes to commit"
exit 0
fi
echo "📦 Auto-committing changes from Copilot session..."
# Stage all changes
git add -A
# Create timestamped commit
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
git commit -m "auto-commit: $TIMESTAMP" --no-verify 2>/dev/null || {
echo "⚠️ Commit failed"
exit 0
}
# Attempt to push
if git push 2>/dev/null; then
echo "✅ Changes committed and pushed successfully"
else
echo "⚠️ Push failed - changes committed locally"
fi
exit 0

View File

@@ -0,0 +1,12 @@
{
"version": 1,
"hooks": {
"sessionEnd": [
{
"type": "command",
"bash": ".github/hooks/session-auto-commit/auto-commit.sh",
"timeoutSec": 30
}
]
}
}

View File

@@ -0,0 +1,64 @@
---
name: 'Session Logger'
description: 'Logs all Copilot coding agent session activity for audit and analysis'
tags: ['logging', 'audit', 'analytics']
---
# Session Logger Hook
Comprehensive logging for GitHub Copilot coding agent sessions, tracking session starts, ends, and user prompts for audit trails and usage analytics.
## Overview
This hook provides detailed logging of Copilot coding agent activity:
- Session start/end times
- User prompts and questions
- Session duration
- Working directory context
## Features
- **Complete Audit Trail**: Track all Copilot interactions
- **Structured Logging**: JSON format for easy parsing
- **Searchable History**: Review past sessions and prompts
- **Analytics Ready**: Export data for usage analysis
- **Privacy Aware**: Configurable to exclude sensitive data
## Installation
1. Copy this hook folder to your repository's `.github/hooks/` directory:
```bash
cp -r hooks/session-logger .github/hooks/
```
2. Create the logs directory:
```bash
mkdir -p logs/copilot
```
3. Ensure scripts are executable:
```bash
chmod +x .github/hooks/session-logger/*.sh
```
4. Commit the hook configuration to your repository's default branch
## Log Format
Logs are written to `logs/copilot/session.log` in JSON format:
```json
{
"timestamp": "2024-01-15T10:30:00Z",
"event": "sessionStart",
"sessionId": "abc123",
"cwd": "/workspace/project"
}
```
## Privacy & Security
- Add `logs/` to `.gitignore` to avoid committing session data
- Use `LOG_LEVEL=ERROR` to only log errors
- Set `SKIP_LOGGING=true` environment variable to disable
- Logs are stored locally only

View File

@@ -0,0 +1,32 @@
{
"version": 1,
"hooks": {
"sessionStart": [
{
"type": "command",
"bash": ".github/hooks/session-logger/log-session-start.sh",
"cwd": ".",
"timeoutSec": 5
}
],
"sessionEnd": [
{
"type": "command",
"bash": ".github/hooks/session-logger/log-session-end.sh",
"cwd": ".",
"timeoutSec": 5
}
],
"userPromptSubmitted": [
{
"type": "command",
"bash": ".github/hooks/session-logger/log-prompt.sh",
"cwd": ".",
"env": {
"LOG_LEVEL": "INFO"
},
"timeoutSec": 5
}
]
}
}

View File

@@ -0,0 +1,24 @@
#!/bin/bash
# Log user prompt submission
set -euo pipefail
# Skip if logging disabled
if [[ "${SKIP_LOGGING:-}" == "true" ]]; then
exit 0
fi
# Read input from Copilot (contains prompt info)
INPUT=$(cat)
# Create logs directory if it doesn't exist
mkdir -p logs/copilot
# Extract timestamp
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Log prompt (you can parse INPUT for more details)
echo "{\"timestamp\":\"$TIMESTAMP\",\"event\":\"userPromptSubmitted\",\"level\":\"${LOG_LEVEL:-INFO}\"}" >> logs/copilot/prompts.log
exit 0

View File

@@ -0,0 +1,25 @@
#!/bin/bash
# Log session end event
set -euo pipefail
# Skip if logging disabled
if [[ "${SKIP_LOGGING:-}" == "true" ]]; then
exit 0
fi
# Read input from Copilot
INPUT=$(cat)
# Create logs directory if it doesn't exist
mkdir -p logs/copilot
# Extract timestamp
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Log session end
echo "{\"timestamp\":\"$TIMESTAMP\",\"event\":\"sessionEnd\"}" >> logs/copilot/session.log
echo "📝 Session end logged"
exit 0

View File

@@ -0,0 +1,26 @@
#!/bin/bash
# Log session start event
set -euo pipefail
# Skip if logging disabled
if [[ "${SKIP_LOGGING:-}" == "true" ]]; then
exit 0
fi
# Read input from Copilot
INPUT=$(cat)
# Create logs directory if it doesn't exist
mkdir -p logs/copilot
# Extract timestamp and session info
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
CWD=$(pwd)
# Log session start
echo "{\"timestamp\":\"$TIMESTAMP\",\"event\":\"sessionStart\",\"cwd\":\"$CWD\"}" >> logs/copilot/session.log
echo "📝 Session logged"
exit 0