Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/validate-readme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ jobs:
- name: Validate plugins
run: npm run plugin:validate

- name: Validate agents
run: npm run agent:validate

- name: Validate instructions
run: npm run instruction:validate

- name: Update README.md
run: npm start

Expand Down
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ npm run skill:validate

# Create a new skill
npm run skill:create -- --name <skill-name>

# Validate agent files (*.agent.md)
npm run agent:validate

# Validate instruction files (*.instructions.md)
npm run instruction:validate
```

## Development Workflow
Expand Down Expand Up @@ -202,6 +208,8 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
# Run all validation checks
npm run plugin:validate
npm run skill:validate
npm run agent:validate
npm run instruction:validate

# Build and verify README generation
npm run build
Expand Down
1 change: 1 addition & 0 deletions agents/declarative-agents-architect.agent.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
name: 'Declarative Agents Architect'
description: 'Expert Microsoft 365 Declarative Agent architect covering the v1.5 JSON schema, TypeSpec development, and Microsoft 365 Agents Toolkit integration.'
model: GPT-4.1
tools: ['codebase']
---
Expand Down
148 changes: 148 additions & 0 deletions eng/validate-agents.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env node

import fs from "fs";
import path from "path";
import { parseFrontmatter } from "./yaml-parser.mjs";
import { AGENTS_DIR } from "./constants.mjs";

const AGENT_FILENAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*\.agent\.md$/;
const AGENT_DESCRIPTION_MIN_LENGTH = 10;
const AGENT_DESCRIPTION_MAX_LENGTH = 1024;

// Validation functions
function validateAgentDescription(description) {
if (
!description ||
typeof description !== "string" ||
description.trim().length === 0
) {
return ["description is required and must be a non-empty string"];
}

const errors = [];
if (description.length < AGENT_DESCRIPTION_MIN_LENGTH) {
errors.push(
`description must be at least ${AGENT_DESCRIPTION_MIN_LENGTH} characters`
);
}
if (description.length > AGENT_DESCRIPTION_MAX_LENGTH) {
errors.push(
`description must not exceed ${AGENT_DESCRIPTION_MAX_LENGTH} characters`
);
}
return errors;
}

function validateAgentFile(filePath) {
const errors = [];
const warnings = [];
const fileName = path.basename(filePath);

if (!fs.existsSync(filePath)) {
errors.push("File not found");
return { errors, warnings };
}

if (!fileName.endsWith(".agent.md")) {
errors.push('File name must end with ".agent.md"');
} else if (!AGENT_FILENAME_PATTERN.test(fileName)) {
warnings.push(
'File name should be lower case with words separated by hyphens (e.g. "my-agent.agent.md")'
);
}

const frontmatter = parseFrontmatter(filePath);
if (!frontmatter) {
errors.push("Missing or unparsable YAML front matter");
return { errors, warnings };
}

errors.push(...validateAgentDescription(frontmatter.description));

if (
!frontmatter.name ||
typeof frontmatter.name !== "string" ||
frontmatter.name.trim().length === 0
) {
warnings.push(
'name field is recommended (a human-readable title, e.g. "Address Comments")'
);
}

if (frontmatter.model === undefined || frontmatter.model === null) {
warnings.push("model field is strongly recommended");
}

if (frontmatter.tools === undefined) {
warnings.push("tools field is recommended");
} else if (!Array.isArray(frontmatter.tools)) {
errors.push("tools must be an array when present");
}

return { errors, warnings };
}

function discoverAgentFiles() {
if (!fs.existsSync(AGENTS_DIR)) {
return [];
}
return fs
.readdirSync(AGENTS_DIR)
.filter((file) => file.endsWith(".agent.md"))
.map((file) => path.join(AGENTS_DIR, file));
}

// Main validation function
function validateAgents(files) {
if (files.length === 0) {
console.log("No agent files found - validation skipped");
return true;
}

console.log(`Validating ${files.length} agent file(s)...`);

let hasErrors = false;

for (const filePath of files) {
const relPath = path.relative(process.cwd(), filePath);
const { errors, warnings } = validateAgentFile(filePath);

if (errors.length > 0) {
console.error(`\n❌ ${relPath}:`);
errors.forEach((error) => console.error(` - ${error}`));
hasErrors = true;
} else {
console.log(`✅ ${relPath}`);
}

warnings.forEach((warning) =>
console.warn(` ⚠️ ${relPath}: ${warning}`)
);
}

if (!hasErrors) {
console.log(`\n✅ All ${files.length} agent file(s) passed validation`);
}

return !hasErrors;
}

// Run validation
try {
const cliArgs = process.argv.slice(2);
const files =
cliArgs.length > 0
? cliArgs.map((file) => path.resolve(file))
: discoverAgentFiles();

const isValid = validateAgents(files);
if (!isValid) {
console.error("\n❌ Agent validation failed");
process.exit(1);
}
console.log("\n🎉 Agent validation passed");
} catch (error) {
console.error(`Error during validation: ${error.message}`);
console.error(error.stack);
process.exit(1);
}
152 changes: 152 additions & 0 deletions eng/validate-instructions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env node

import fs from "fs";
import path from "path";
import { parseFrontmatter } from "./yaml-parser.mjs";
import { INSTRUCTIONS_DIR } from "./constants.mjs";

const INSTRUCTION_FILENAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*\.instructions\.md$/;
const INSTRUCTION_DESCRIPTION_MIN_LENGTH = 10;
const INSTRUCTION_DESCRIPTION_MAX_LENGTH = 1024;

// Validation functions
function validateInstructionDescription(description) {
if (
!description ||
typeof description !== "string" ||
description.trim().length === 0
) {
return ["description is required and must be a non-empty string"];
}

const errors = [];
if (description.length < INSTRUCTION_DESCRIPTION_MIN_LENGTH) {
errors.push(
`description must be at least ${INSTRUCTION_DESCRIPTION_MIN_LENGTH} characters`
);
}
if (description.length > INSTRUCTION_DESCRIPTION_MAX_LENGTH) {
errors.push(
`description must not exceed ${INSTRUCTION_DESCRIPTION_MAX_LENGTH} characters`
);
}
return errors;
}

function validateApplyTo(applyTo) {
if (applyTo === undefined || applyTo === null) {
return ["applyTo is required and must specify one or more file glob patterns"];
}

const patterns = Array.isArray(applyTo) ? applyTo : String(applyTo).split(",");

if (Array.isArray(applyTo) && applyTo.length === 0) {
return ["applyTo must not be an empty array"];
}

const errors = [];
for (const pattern of patterns) {
if (typeof pattern !== "string" || pattern.trim().length === 0) {
errors.push("applyTo patterns must be non-empty strings");
break;
}
}
return errors;
}

function validateInstructionFile(filePath) {
const errors = [];
const warnings = [];
const fileName = path.basename(filePath);

if (!fs.existsSync(filePath)) {
errors.push("File not found");
return { errors, warnings };
}

if (!fileName.endsWith(".instructions.md")) {
errors.push('File name must end with ".instructions.md"');
} else if (!INSTRUCTION_FILENAME_PATTERN.test(fileName)) {
warnings.push(
'File name should be lower case with words separated by hyphens (e.g. "my-rules.instructions.md")'
);
}

const frontmatter = parseFrontmatter(filePath);
if (!frontmatter) {
errors.push("Missing or unparsable YAML front matter");
return { errors, warnings };
}

errors.push(...validateInstructionDescription(frontmatter.description));
errors.push(...validateApplyTo(frontmatter.applyTo));

return { errors, warnings };
}

function discoverInstructionFiles() {
if (!fs.existsSync(INSTRUCTIONS_DIR)) {
return [];
}
return fs
.readdirSync(INSTRUCTIONS_DIR)
.filter((file) => file.endsWith(".instructions.md"))
.map((file) => path.join(INSTRUCTIONS_DIR, file));
}

// Main validation function
function validateInstructions(files) {
if (files.length === 0) {
console.log("No instruction files found - validation skipped");
return true;
}

console.log(`Validating ${files.length} instruction file(s)...`);

let hasErrors = false;

for (const filePath of files) {
const relPath = path.relative(process.cwd(), filePath);
const { errors, warnings } = validateInstructionFile(filePath);

if (errors.length > 0) {
console.error(`\n❌ ${relPath}:`);
errors.forEach((error) => console.error(` - ${error}`));
hasErrors = true;
} else {
console.log(`✅ ${relPath}`);
}

warnings.forEach((warning) =>
console.warn(` ⚠️ ${relPath}: ${warning}`)
);
}

if (!hasErrors) {
console.log(
`\n✅ All ${files.length} instruction file(s) passed validation`
);
}

return !hasErrors;
}

// Run validation
try {
const cliArgs = process.argv.slice(2);
const files =
cliArgs.length > 0
? cliArgs.map((file) => path.resolve(file))
: discoverInstructionFiles();

const isValid = validateInstructions(files);
if (!isValid) {
console.error("\n❌ Instruction validation failed");
process.exit(1);
}
console.log("\n🎉 Instruction validation passed");
} catch (error) {
console.error(`Error during validation: ${error.message}`);
console.error(error.stack);
process.exit(1);
}
1 change: 1 addition & 0 deletions instructions/codexer.instructions.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
applyTo: '**/*.py'
description: 'Advanced Python research assistant with Context 7 MCP integration, focusing on speed, reliability, and 10+ years of software development expertise'
---

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
applyTo: '**'
description: 'Advanced Dataverse SDK for Python features including enums, complex filtering, SQL queries, metadata operations, and production patterns.'
---

# Dataverse SDK for Python - Advanced Features Guide

## Overview
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
applyTo: '**'
description: 'Using the Dataverse SDK for Python (public preview) to build agentic workflows.'
---

# Dataverse SDK for Python - Agentic Workflows Guide

## ⚠️ PREVIEW FEATURE NOTICE
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
applyTo: '**'
description: 'API reference for the DataverseClient class and other core classes in the Dataverse SDK for Python.'
---
# Dataverse SDK for Python — API Reference Guide

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
applyTo: '**'
description: 'Authentication and security patterns for the Dataverse SDK for Python, including OAuth setup and credential handling.'
---

# Dataverse SDK for Python — Authentication & Security Patterns
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
applyTo: '**'
description: 'Production-ready patterns and best practices for the Dataverse SDK for Python, extracted from Microsoft official examples and recommended workflows.'
---

# Dataverse SDK for Python - Best Practices Guide

## Overview
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
applyTo: '**'
description: 'Error handling and troubleshooting strategies for the Dataverse SDK for Python.'
---

# Dataverse SDK for Python — Error Handling & Troubleshooting Guide
Expand Down
Loading
Loading