Merge branch 'main' into add-symfony-instructions

This commit is contained in:
Matt Soucoup
2026-01-09 12:43:29 -10:00
committed by GitHub
36 changed files with 7536 additions and 23 deletions

View File

@@ -0,0 +1,791 @@
---
description: 'Guidelines for creating custom agent files for GitHub Copilot'
applyTo: '**/*.agent.md'
---
# Custom Agent File Guidelines
Instructions for creating effective and maintainable custom agent files that provide specialized expertise for specific development tasks in GitHub Copilot.
## Project Context
- Target audience: Developers creating custom agents for GitHub Copilot
- File format: Markdown with YAML frontmatter
- File naming convention: lowercase with hyphens (e.g., `test-specialist.agent.md`)
- Location: `.github/agents/` directory (repository-level) or `agents/` directory (organization/enterprise-level)
- Purpose: Define specialized agents with tailored expertise, tools, and instructions for specific tasks
- Official documentation: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-custom-agents
## Required Frontmatter
Every agent file must include YAML frontmatter with the following fields:
```yaml
---
description: 'Brief description of the agent purpose and capabilities'
name: 'Agent Display Name'
tools: ['read', 'edit', 'search']
model: 'Claude Sonnet 4.5'
target: 'vscode'
infer: true
---
```
### Core Frontmatter Properties
#### **description** (REQUIRED)
- Single-quoted string, clearly stating the agent's purpose and domain expertise
- Should be concise (50-150 characters) and actionable
- Example: `'Focuses on test coverage, quality, and testing best practices'`
#### **name** (OPTIONAL)
- Display name for the agent in the UI
- If omitted, defaults to filename (without `.md` or `.agent.md`)
- Use title case and be descriptive
- Example: `'Testing Specialist'`
#### **tools** (OPTIONAL)
- List of tool names or aliases the agent can use
- Supports comma-separated string or YAML array format
- If omitted, agent has access to all available tools
- See "Tool Configuration" section below for details
#### **model** (STRONGLY RECOMMENDED)
- Specifies which AI model the agent should use
- Supported in VS Code, JetBrains IDEs, Eclipse, and Xcode
- Example: `'Claude Sonnet 4.5'`, `'gpt-4'`, `'gpt-4o'`
- Choose based on agent complexity and required capabilities
#### **target** (OPTIONAL)
- Specifies target environment: `'vscode'` or `'github-copilot'`
- If omitted, agent is available in both environments
- Use when agent has environment-specific features
#### **infer** (OPTIONAL)
- Boolean controlling whether Copilot can automatically use this agent based on context
- Default: `true` if omitted
- Set to `false` to require manual agent selection
#### **metadata** (OPTIONAL, GitHub.com only)
- Object with name-value pairs for agent annotation
- Example: `metadata: { category: 'testing', version: '1.0' }`
- Not supported in VS Code
#### **mcp-servers** (OPTIONAL, Organization/Enterprise only)
- Configure MCP servers available only to this agent
- Only supported for organization/enterprise level agents
- See "MCP Server Configuration" section below
## Tool Configuration
### Tool Specification Strategies
**Enable all tools** (default):
```yaml
# Omit tools property entirely, or use:
tools: ['*']
```
**Enable specific tools**:
```yaml
tools: ['read', 'edit', 'search', 'execute']
```
**Enable MCP server tools**:
```yaml
tools: ['read', 'edit', 'github/*', 'playwright/navigate']
```
**Disable all tools**:
```yaml
tools: []
```
### Standard Tool Aliases
All aliases are case-insensitive:
| Alias | Alternative Names | Category | Description |
|-------|------------------|----------|-------------|
| `execute` | shell, Bash, powershell | Shell execution | Execute commands in appropriate shell |
| `read` | Read, NotebookRead, view | File reading | Read file contents |
| `edit` | Edit, MultiEdit, Write, NotebookEdit | File editing | Edit and modify files |
| `search` | Grep, Glob, search | Code search | Search for files or text in files |
| `agent` | custom-agent, Task | Agent invocation | Invoke other custom agents |
| `web` | WebSearch, WebFetch | Web access | Fetch web content and search |
| `todo` | TodoWrite | Task management | Create and manage task lists (VS Code only) |
### Built-in MCP Server Tools
**GitHub MCP Server**:
```yaml
tools: ['github/*'] # All GitHub tools
tools: ['github/get_file_contents', 'github/search_repositories'] # Specific tools
```
- All read-only tools available by default
- Token scoped to source repository
**Playwright MCP Server**:
```yaml
tools: ['playwright/*'] # All Playwright tools
tools: ['playwright/navigate', 'playwright/screenshot'] # Specific tools
```
- Configured to access localhost only
- Useful for browser automation and testing
### Tool Selection Best Practices
- **Principle of Least Privilege**: Only enable tools necessary for the agent's purpose
- **Security**: Limit `execute` access unless explicitly required
- **Focus**: Fewer tools = clearer agent purpose and better performance
- **Documentation**: Comment why specific tools are required for complex configurations
## Sub-Agent Invocation (Agent Orchestration)
Agents can invoke other agents using `runSubagent` to orchestrate multi-step workflows.
### How It Works
Include `agent` in tools list to enable sub-agent invocation:
```yaml
tools: ['read', 'edit', 'search', 'agent']
```
Then invoke other agents with `runSubagent`:
```javascript
const result = await runSubagent({
description: 'What this step does',
prompt: `You are the [Specialist] specialist.
Context:
- Parameter: ${parameterValue}
- Input: ${inputPath}
- Output: ${outputPath}
Task:
1. Do the specific work
2. Write results to output location
3. Return summary of completion`
});
```
### Basic Pattern
Structure each sub-agent call with:
1. **description**: Clear one-line purpose of the sub-agent invocation
2. **prompt**: Detailed instructions with substituted variables
The prompt should include:
- Who the sub-agent is (specialist role)
- What context it needs (parameters, paths)
- What to do (concrete tasks)
- Where to write output
- What to return (summary)
### Example: Multi-Step Processing
```javascript
// Step 1: Process data
const processing = await runSubagent({
description: 'Transform raw input data',
prompt: `You are the Data Processor specialist.
Project: ${projectName}
Input: ${basePath}/raw/
Output: ${basePath}/processed/
Task:
1. Read all files from input directory
2. Apply transformations
3. Write processed files to output
4. Create summary: ${basePath}/processed/summary.md
Return: Number of files processed and any issues found`
});
// Step 2: Analyze (depends on Step 1)
const analysis = await runSubagent({
description: 'Analyze processed data',
prompt: `You are the Data Analyst specialist.
Project: ${projectName}
Input: ${basePath}/processed/
Output: ${basePath}/analysis/
Task:
1. Read processed files from input
2. Generate analysis report
3. Write to: ${basePath}/analysis/report.md
Return: Key findings and identified patterns`
});
```
### Key Points
- **Pass variables in prompts**: Use `${variableName}` for all dynamic values
- **Keep prompts focused**: Clear, specific tasks for each sub-agent
- **Return summaries**: Each sub-agent should report what it accomplished
- **Sequential execution**: Use `await` to maintain order when steps depend on each other
- **Error handling**: Check results before proceeding to dependent steps
### ⚠️ Tool Availability Requirement
**Critical**: If a sub-agent requires specific tools (e.g., `edit`, `execute`, `search`), the orchestrator must include those tools in its own `tools` list. Sub-agents cannot access tools that aren't available to their parent orchestrator.
**Example**:
```yaml
# If your sub-agents need to edit files, execute commands, or search code
tools: ['read', 'edit', 'search', 'execute', 'agent']
```
The orchestrator's tool permissions act as a ceiling for all invoked sub-agents. Plan your tool list carefully to ensure all sub-agents have the tools they need.
### ⚠️ Important Limitation
**Sub-agent orchestration is NOT suitable for large-scale data processing.** Avoid using `runSubagent` when:
- Processing hundreds or thousands of files
- Handling large datasets
- Performing bulk transformations on big codebases
- Orchestrating more than 5-10 sequential steps
Each sub-agent call adds latency and context overhead. For high-volume processing, implement logic directly in a single agent instead. Use orchestration only for coordinating specialized tasks on focused, manageable datasets.
## Agent Prompt Structure
The markdown content below the frontmatter defines the agent's behavior, expertise, and instructions. Well-structured prompts typically include:
1. **Agent Identity and Role**: Who the agent is and its primary role
2. **Core Responsibilities**: What specific tasks the agent performs
3. **Approach and Methodology**: How the agent works to accomplish tasks
4. **Guidelines and Constraints**: What to do/avoid and quality standards
5. **Output Expectations**: Expected output format and quality
### Prompt Writing Best Practices
- **Be Specific and Direct**: Use imperative mood ("Analyze", "Generate"); avoid vague terms
- **Define Boundaries**: Clearly state scope limits and constraints
- **Include Context**: Explain domain expertise and reference relevant frameworks
- **Focus on Behavior**: Describe how the agent should think and work
- **Use Structured Format**: Headers, bullets, and lists make prompts scannable
## Variable Definition and Extraction
Agents can define dynamic parameters to extract values from user input and use them throughout the agent's behavior and sub-agent communications. This enables flexible, context-aware agents that adapt to user-provided data.
### When to Use Variables
**Use variables when**:
- Agent behavior depends on user input
- Need to pass dynamic values to sub-agents
- Want to make agents reusable across different contexts
- Require parameterized workflows
- Need to track or reference user-provided context
**Examples**:
- Extract project name from user prompt
- Capture certification name for pipeline processing
- Identify file paths or directories
- Extract configuration options
- Parse feature names or module identifiers
### Variable Declaration Pattern
Define variables section early in the agent prompt to document expected parameters:
```markdown
# Agent Name
## Dynamic Parameters
- **Parameter Name**: Description and usage
- **Another Parameter**: How it's extracted and used
## Your Mission
Process [PARAMETER_NAME] to accomplish [task].
```
### Variable Extraction Methods
#### 1. **Explicit User Input**
Ask the user to provide the variable if not detected in the prompt:
```markdown
## Your Mission
Process the project by analyzing your codebase.
### Step 1: Identify Project
If no project name is provided, **ASK THE USER** for:
- Project name or identifier
- Base path or directory location
- Configuration type (if applicable)
Use this information to contextualize all subsequent tasks.
```
#### 2. **Implicit Extraction from Prompt**
Automatically extract variables from the user's natural language input:
```javascript
// Example: Extract certification name from user input
const userInput = "Process My Certification";
// Extract key information
const certificationName = extractCertificationName(userInput);
// Result: "My Certification"
const basePath = `certifications/${certificationName}`;
// Result: "certifications/My Certification"
```
#### 3. **Contextual Variable Resolution**
Use file context or workspace information to derive variables:
```markdown
## Variable Resolution Strategy
1. **From User Prompt**: First, look for explicit mentions in user input
2. **From File Context**: Check current file name or path
3. **From Workspace**: Use workspace folder or active project
4. **From Settings**: Reference configuration files
5. **Ask User**: If all else fails, request missing information
```
### Using Variables in Agent Prompts
#### Variable Substitution in Instructions
Use template variables in agent prompts to make them dynamic:
```markdown
# Agent Name
## Dynamic Parameters
- **Project Name**: ${projectName}
- **Base Path**: ${basePath}
- **Output Directory**: ${outputDir}
## Your Mission
Process the **${projectName}** project located at `${basePath}`.
## Process Steps
1. Read input from: `${basePath}/input/`
2. Process files according to project configuration
3. Write results to: `${outputDir}/`
4. Generate summary report
## Quality Standards
- Maintain project-specific coding standards for **${projectName}**
- Follow directory structure: `${basePath}/[structure]`
```
#### Passing Variables to Sub-Agents
When invoking a sub-agent, pass all context through template variables in the prompt:
```javascript
// Extract and prepare variables
const basePath = `projects/${projectName}`;
const inputPath = `${basePath}/src/`;
const outputPath = `${basePath}/docs/`;
// Pass to sub-agent with all variables substituted
const result = await runSubagent({
description: 'Generate project documentation',
prompt: `You are the Documentation specialist.
Project: ${projectName}
Input: ${inputPath}
Output: ${outputPath}
Task:
1. Read source files from ${inputPath}
2. Generate comprehensive documentation
3. Write to ${outputPath}/index.md
4. Include code examples and usage guides
Return: Summary of documentation generated (file count, word count)`
});
```
The sub-agent receives all necessary context embedded in the prompt. Variables are resolved before sending the prompt, so the sub-agent works with concrete paths and values, not variable placeholders.
### Real-World Example: Code Review Orchestrator
Example of a simple orchestrator that validates code through multiple specialized agents:
```javascript
async function reviewCodePipeline(repositoryName, prNumber) {
const basePath = `projects/${repositoryName}/pr-${prNumber}`;
// Step 1: Security Review
const security = await runSubagent({
description: 'Scan for security vulnerabilities',
prompt: `You are the Security Reviewer specialist.
Repository: ${repositoryName}
PR: ${prNumber}
Code: ${basePath}/changes/
Task:
1. Scan code for OWASP Top 10 vulnerabilities
2. Check for injection attacks, auth flaws
3. Write findings to ${basePath}/security-review.md
Return: List of critical, high, and medium issues found`
});
// Step 2: Test Coverage Check
const coverage = await runSubagent({
description: 'Verify test coverage for changes',
prompt: `You are the Test Coverage specialist.
Repository: ${repositoryName}
PR: ${prNumber}
Changes: ${basePath}/changes/
Task:
1. Analyze code coverage for modified files
2. Identify untested critical paths
3. Write report to ${basePath}/coverage-report.md
Return: Current coverage percentage and gaps`
});
// Step 3: Aggregate Results
const finalReport = await runSubagent({
description: 'Compile all review findings',
prompt: `You are the Review Aggregator specialist.
Repository: ${repositoryName}
Reports: ${basePath}/*.md
Task:
1. Read all review reports from ${basePath}/
2. Synthesize findings into single report
3. Determine overall verdict (APPROVE/NEEDS_FIXES/BLOCK)
4. Write to ${basePath}/final-review.md
Return: Final verdict and executive summary`
});
return finalReport;
}
```
This pattern applies to any orchestration scenario: extract variables, call sub-agents with clear context, await results.
### Variable Best Practices
#### 1. **Clear Documentation**
Always document what variables are expected:
```markdown
## Required Variables
- **projectName**: The name of the project (string, required)
- **basePath**: Root directory for project files (path, required)
## Optional Variables
- **mode**: Processing mode - quick/standard/detailed (enum, default: standard)
- **outputFormat**: Output format - markdown/json/html (enum, default: markdown)
## Derived Variables
- **outputDir**: Automatically set to ${basePath}/output
- **logFile**: Automatically set to ${basePath}/.log.md
```
#### 2. **Consistent Naming**
Use consistent variable naming conventions:
```javascript
// Good: Clear, descriptive naming
const variables = {
projectName, // What project to work on
basePath, // Where project files are located
outputDirectory, // Where to save results
processingMode, // How to process (detail level)
configurationPath // Where config files are
};
// Avoid: Ambiguous or inconsistent
const bad_variables = {
name, // Too generic
path, // Unclear which path
mode, // Too short
config // Too vague
};
```
#### 3. **Validation and Constraints**
Document valid values and constraints:
```markdown
## Variable Constraints
**projectName**:
- Type: string (alphanumeric, hyphens, underscores allowed)
- Length: 1-100 characters
- Required: yes
- Pattern: `/^[a-zA-Z0-9_-]+$/`
**processingMode**:
- Type: enum
- Valid values: "quick" (< 5min), "standard" (5-15min), "detailed" (15+ min)
- Default: "standard"
- Required: no
```
## MCP Server Configuration (Organization/Enterprise Only)
MCP servers extend agent capabilities with additional tools. Only supported for organization and enterprise-level agents.
### Configuration Format
```yaml
---
name: my-custom-agent
description: 'Agent with MCP integration'
tools: ['read', 'edit', 'custom-mcp/tool-1']
mcp-servers:
custom-mcp:
type: 'local'
command: 'some-command'
args: ['--arg1', '--arg2']
tools: ["*"]
env:
ENV_VAR_NAME: ${{ secrets.API_KEY }}
---
```
### MCP Server Properties
- **type**: Server type (`'local'` or `'stdio'`)
- **command**: Command to start the MCP server
- **args**: Array of command arguments
- **tools**: Tools to enable from this server (`["*"]` for all)
- **env**: Environment variables (supports secrets)
### Environment Variables and Secrets
Secrets must be configured in repository settings under "copilot" environment.
**Supported syntax**:
```yaml
env:
# Environment variable only
VAR_NAME: COPILOT_MCP_ENV_VAR_VALUE
# Variable with header
VAR_NAME: $COPILOT_MCP_ENV_VAR_VALUE
VAR_NAME: ${COPILOT_MCP_ENV_VAR_VALUE}
# GitHub Actions-style (YAML only)
VAR_NAME: ${{ secrets.COPILOT_MCP_ENV_VAR_VALUE }}
VAR_NAME: ${{ var.COPILOT_MCP_ENV_VAR_VALUE }}
```
## File Organization and Naming
### Repository-Level Agents
- Location: `.github/agents/`
- Scope: Available only in the specific repository
- Access: Uses repository-configured MCP servers
### Organization/Enterprise-Level Agents
- Location: `.github-private/agents/` (then move to `agents/` root)
- Scope: Available across all repositories in org/enterprise
- Access: Can configure dedicated MCP servers
### Naming Conventions
- Use lowercase with hyphens: `test-specialist.agent.md`
- Name should reflect agent purpose
- Filename becomes default agent name (if `name` not specified)
- Allowed characters: `.`, `-`, `_`, `a-z`, `A-Z`, `0-9`
## Agent Processing and Behavior
### Versioning
- Based on Git commit SHAs for the agent file
- Create branches/tags for different agent versions
- Instantiated using latest version for repository/branch
- PR interactions use same agent version for consistency
### Name Conflicts
Priority (highest to lowest):
1. Repository-level agent
2. Organization-level agent
3. Enterprise-level agent
Lower-level configurations override higher-level ones with the same name.
### Tool Processing
- `tools` list filters available tools (built-in and MCP)
- No tools specified = all tools enabled
- Empty list (`[]`) = all tools disabled
- Specific list = only those tools enabled
- Unrecognized tool names are ignored (allows environment-specific tools)
### MCP Server Processing Order
1. Out-of-the-box MCP servers (e.g., GitHub MCP)
2. Custom agent MCP configuration (org/enterprise only)
3. Repository-level MCP configurations
Each level can override settings from previous levels.
## Agent Creation Checklist
### Frontmatter
- [ ] `description` field present and descriptive (50-150 chars)
- [ ] `description` wrapped in single quotes
- [ ] `name` specified (optional but recommended)
- [ ] `tools` configured appropriately (or intentionally omitted)
- [ ] `model` specified for optimal performance
- [ ] `target` set if environment-specific
- [ ] `infer` set to `false` if manual selection required
### Prompt Content
- [ ] Clear agent identity and role defined
- [ ] Core responsibilities listed explicitly
- [ ] Approach and methodology explained
- [ ] Guidelines and constraints specified
- [ ] Output expectations documented
- [ ] Examples provided where helpful
- [ ] Instructions are specific and actionable
- [ ] Scope and boundaries clearly defined
- [ ] Total content under 30,000 characters
### File Structure
- [ ] Filename follows lowercase-with-hyphens convention
- [ ] File placed in correct directory (`.github/agents/` or `agents/`)
- [ ] Filename uses only allowed characters
- [ ] File extension is `.agent.md`
### Quality Assurance
- [ ] Agent purpose is unique and not duplicative
- [ ] Tools are minimal and necessary
- [ ] Instructions are clear and unambiguous
- [ ] Agent has been tested with representative tasks
- [ ] Documentation references are current
- [ ] Security considerations addressed (if applicable)
## Common Agent Patterns
### Testing Specialist
**Purpose**: Focus on test coverage and quality
**Tools**: All tools (for comprehensive test creation)
**Approach**: Analyze, identify gaps, write tests, avoid production code changes
### Implementation Planner
**Purpose**: Create detailed technical plans and specifications
**Tools**: Limited to `['read', 'search', 'edit']`
**Approach**: Analyze requirements, create documentation, avoid implementation
### Code Reviewer
**Purpose**: Review code quality and provide feedback
**Tools**: `['read', 'search']` only
**Approach**: Analyze, suggest improvements, no direct modifications
### Refactoring Specialist
**Purpose**: Improve code structure and maintainability
**Tools**: `['read', 'search', 'edit']`
**Approach**: Analyze patterns, propose refactorings, implement safely
### Security Auditor
**Purpose**: Identify security issues and vulnerabilities
**Tools**: `['read', 'search', 'web']`
**Approach**: Scan code, check against OWASP, report findings
## Common Mistakes to Avoid
### Frontmatter Errors
- ❌ Missing `description` field
- ❌ Description not wrapped in quotes
- ❌ Invalid tool names without checking documentation
- ❌ Incorrect YAML syntax (indentation, quotes)
### Tool Configuration Issues
- ❌ Granting excessive tool access unnecessarily
- ❌ Missing required tools for agent's purpose
- ❌ Not using tool aliases consistently
- ❌ Forgetting MCP server namespace (`server-name/tool`)
### Prompt Content Problems
- ❌ Vague, ambiguous instructions
- ❌ Conflicting or contradictory guidelines
- ❌ Lack of clear scope definition
- ❌ Missing output expectations
- ❌ Overly verbose instructions (exceeding character limits)
- ❌ No examples or context for complex tasks
### Organizational Issues
- ❌ Filename doesn't reflect agent purpose
- ❌ Wrong directory (confusing repo vs org level)
- ❌ Using spaces or special characters in filename
- ❌ Duplicate agent names causing conflicts
## Testing and Validation
### Manual Testing
1. Create the agent file with proper frontmatter
2. Reload VS Code or refresh GitHub.com
3. Select the agent from the dropdown in Copilot Chat
4. Test with representative user queries
5. Verify tool access works as expected
6. Confirm output meets expectations
### Integration Testing
- Test agent with different file types in scope
- Verify MCP server connectivity (if configured)
- Check agent behavior with missing context
- Test error handling and edge cases
- Validate agent switching and handoffs
### Quality Checks
- Run through agent creation checklist
- Review against common mistakes list
- Compare with example agents in repository
- Get peer review for complex agents
- Document any special configuration needs
## Additional Resources
### Official Documentation
- [Creating Custom Agents](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-custom-agents)
- [Custom Agents Configuration](https://docs.github.com/en/copilot/reference/custom-agents-configuration)
- [Custom Agents in VS Code](https://code.visualstudio.com/docs/copilot/customization/custom-agents)
- [MCP Integration](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/extend-coding-agent-with-mcp)
### Community Resources
- [Awesome Copilot Agents Collection](https://github.com/github/awesome-copilot/tree/main/agents)
- [Customization Library Examples](https://docs.github.com/en/copilot/tutorials/customization-library/custom-agents)
- [Your First Custom Agent Tutorial](https://docs.github.com/en/copilot/tutorials/customization-library/custom-agents/your-first-custom-agent)
### Related Files
- [Prompt Files Guidelines](./prompt.instructions.md) - For creating prompt files
- [Instructions Guidelines](./instructions.instructions.md) - For creating instruction files
## Version Compatibility Notes
### GitHub.com (Coding Agent)
- ✅ Fully supports all standard frontmatter properties
- ✅ Repository and org/enterprise level agents
- ✅ MCP server configuration (org/enterprise)
- ❌ Does not support `model`, `argument-hint`, `handoffs` properties
### VS Code / JetBrains / Eclipse / Xcode
- ✅ Supports `model` property for AI model selection
- ✅ Supports `argument-hint` and `handoffs` properties
- ✅ User profile and workspace-level agents
- ❌ Cannot configure MCP servers at repository level
- ⚠️ Some properties may behave differently
When creating agents for multiple environments, focus on common properties and test in all target environments. Use `target` property to create environment-specific agents when necessary.

View File

@@ -0,0 +1,136 @@
---
applyTo: 'k8s/**/*.yaml,k8s/**/*.yml,manifests/**/*.yaml,manifests/**/*.yml,deploy/**/*.yaml,deploy/**/*.yml,charts/**/templates/**/*.yaml,charts/**/templates/**/*.yml'
description: 'Best practices for Kubernetes YAML manifests including labeling conventions, security contexts, pod security, resource management, probes, and validation commands'
---
# Kubernetes Manifests Instructions
## Your Mission
Create production-ready Kubernetes manifests that prioritize security, reliability, and operational excellence with consistent labeling, proper resource management, and comprehensive health checks.
## Labeling Conventions
**Required Labels** (Kubernetes recommended):
- `app.kubernetes.io/name`: Application name
- `app.kubernetes.io/instance`: Instance identifier
- `app.kubernetes.io/version`: Version
- `app.kubernetes.io/component`: Component role
- `app.kubernetes.io/part-of`: Application group
- `app.kubernetes.io/managed-by`: Management tool
**Additional Labels**:
- `environment`: Environment name
- `team`: Owning team
- `cost-center`: For billing
**Useful Annotations**:
- Documentation and ownership
- Monitoring: `prometheus.io/scrape`, `prometheus.io/port`, `prometheus.io/path`
- Change tracking: git commit, deployment date
## SecurityContext Defaults
**Pod-level**:
- `runAsNonRoot: true`
- `runAsUser` and `runAsGroup`: Specific IDs
- `fsGroup`: File system group
- `seccompProfile.type: RuntimeDefault`
**Container-level**:
- `allowPrivilegeEscalation: false`
- `readOnlyRootFilesystem: true` (with tmpfs mounts for writable dirs)
- `capabilities.drop: [ALL]` (add only what's needed)
## Pod Security Standards
Use Pod Security Admission:
- **Restricted** (recommended for production): Enforces security hardening
- **Baseline**: Minimal security requirements
- Apply at namespace level
## Resource Requests and Limits
**Always define**:
- Requests: Guaranteed minimum (scheduling)
- Limits: Maximum allowed (prevents exhaustion)
**QoS Classes**:
- **Guaranteed**: requests == limits (best for critical apps)
- **Burstable**: requests < limits (flexible resource use)
- **BestEffort**: No resources defined (avoid in production)
## Health Probes
**Liveness**: Restart unhealthy containers
**Readiness**: Control traffic routing
**Startup**: Protect slow-starting applications
Configure appropriate delays, periods, timeouts, and thresholds for each.
## Rollout Strategies
**Deployment Strategy**:
- `RollingUpdate` with `maxSurge` and `maxUnavailable`
- Set `maxUnavailable: 0` for zero-downtime
**High Availability**:
- Minimum 2-3 replicas
- Pod Disruption Budget (PDB)
- Anti-affinity rules (spread across nodes/zones)
- Horizontal Pod Autoscaler (HPA) for variable load
## Validation Commands
**Pre-deployment**:
- `kubectl apply --dry-run=client -f manifest.yaml`
- `kubectl apply --dry-run=server -f manifest.yaml`
- `kubeconform -strict manifest.yaml` (schema validation)
- `helm template ./chart | kubeconform -strict` (for Helm)
**Policy Validation**:
- OPA Conftest, Kyverno, or Datree
## Rollout & Rollback
**Deploy**:
- `kubectl apply -f manifest.yaml`
- `kubectl rollout status deployment/NAME`
**Rollback**:
- `kubectl rollout undo deployment/NAME`
- `kubectl rollout undo deployment/NAME --to-revision=N`
- `kubectl rollout history deployment/NAME`
**Restart**:
- `kubectl rollout restart deployment/NAME`
## Manifest Checklist
- [ ] Labels: Standard labels applied
- [ ] Annotations: Documentation and monitoring
- [ ] Security: runAsNonRoot, readOnlyRootFilesystem, dropped capabilities
- [ ] Resources: Requests and limits defined
- [ ] Probes: Liveness, readiness, startup configured
- [ ] Images: Specific tags (never :latest)
- [ ] Replicas: Minimum 2-3 for production
- [ ] Strategy: RollingUpdate with appropriate surge/unavailable
- [ ] PDB: Defined for production
- [ ] Anti-affinity: Configured for HA
- [ ] Graceful shutdown: terminationGracePeriodSeconds set
- [ ] Validation: Dry-run and kubeconform passed
- [ ] Secrets: In Secrets resource, not ConfigMaps
- [ ] NetworkPolicy: Least-privilege access (if applicable)
## Best Practices Summary
1. Use standard labels and annotations
2. Always run as non-root with dropped capabilities
3. Define resource requests and limits
4. Implement all three probe types
5. Pin image tags to specific versions
6. Configure anti-affinity for HA
7. Set Pod Disruption Budgets
8. Use rolling updates with zero unavailability
9. Validate manifests before applying
10. Enable read-only root filesystem when possible

View File

@@ -0,0 +1,426 @@
---
description: 'Guidelines and best practices for developing Lightning Web Components (LWC) on Salesforce Platform.'
applyTo: 'force-app/main/default/lwc/**'
---
# LWC Development
## General Instructions
- Each LWC should reside in its own folder under `force-app/main/default/lwc/`.
- The folder name should match the component name (e.g., `myComponent` folder for the `myComponent` component).
- Each component folder should contain the following files:
- `myComponent.html`: The HTML template file.
- `myComponent.js`: The JavaScript controller file.
- `myComponent.js-meta.xml`: The metadata configuration file.
- Optional: `myComponent.css` for component-specific styles.
- Optional: `myComponent.test.js` for Jest unit tests.
## Core Principles
### 1. Use Lightning Components Over HTML Tags
Always prefer Lightning Web Component library components over plain HTML elements for consistency, accessibility, and future-proofing.
#### Recommended Approach
```html
<!-- Use Lightning components -->
<lightning-button label="Save" variant="brand" onclick={handleSave}></lightning-button>
<lightning-input type="text" label="Name" value={name} onchange={handleNameChange}></lightning-input>
<lightning-combobox label="Type" options={typeOptions} value={selectedType}></lightning-combobox>
<lightning-radio-group name="duration" label="Duration" options={durationOptions} value={duration} type="radio"></lightning-radio-group>
```
#### Avoid Plain HTML
```html
<!-- Avoid these -->
<button onclick={handleSave}>Save</button>
<input type="text" onchange={handleNameChange} />
<select onchange={handleTypeChange}>
<option value="option1">Option 1</option>
</select>
```
### 2. Lightning Component Mapping Guide
| HTML Element | Lightning Component | Key Attributes |
|--------------|-------------------|----------------|
| `<button>` | `<lightning-button>` | `variant`, `label`, `icon-name` |
| `<input>` | `<lightning-input>` | `type`, `label`, `variant` |
| `<select>` | `<lightning-combobox>` | `options`, `value`, `placeholder` |
| `<textarea>` | `<lightning-textarea>` | `label`, `max-length` |
| `<input type="checkbox">` | `<lightning-input type="checkbox">` | `checked`, `label` |
| `<input type="radio">` | `<lightning-radio-group>` | `options`, `type`, `name` |
| `<input type="toggle">` | `<lightning-input type="toggle">` | `checked`, `variant` |
| Custom pills | `<lightning-pill>` | `label`, `name`, `onremove` |
| Icons | `<lightning-icon>` | `icon-name`, `size`, `variant` |
### 3. Lightning Design System Compliance
#### Use SLDS Utility Classes
Always use Salesforce Lightning Design System utility classes with the `slds-var-` prefix for modern implementations:
```html
<!-- Spacing -->
<div class="slds-var-m-around_medium slds-var-p-top_large">
<div class="slds-var-m-bottom_small">Content</div>
</div>
<!-- Layout -->
<div class="slds-grid slds-wrap slds-gutters_small">
<div class="slds-col slds-size_1-of-2 slds-medium-size_1-of-3">
<!-- Content -->
</div>
</div>
<!-- Typography -->
<h2 class="slds-text-heading_medium slds-var-m-bottom_small">Section Title</h2>
<p class="slds-text-body_regular">Description text</p>
```
#### SLDS Component Patterns
```html
<!-- Card Layout -->
<article class="slds-card slds-var-m-around_medium">
<header class="slds-card__header">
<h2 class="slds-text-heading_small">Card Title</h2>
</header>
<div class="slds-card__body slds-card__body_inner">
<!-- Card content -->
</div>
<footer class="slds-card__footer">
<!-- Card actions -->
</footer>
</article>
<!-- Form Layout -->
<div class="slds-form slds-form_stacked">
<div class="slds-form-element">
<lightning-input label="Field Label" value={fieldValue}></lightning-input>
</div>
</div>
```
### 4. Avoid Custom CSS
#### Use SLDS Classes
```html
<!-- Color and theming -->
<div class="slds-theme_success slds-text-color_inverse slds-var-p-around_small">
Success message
</div>
<div class="slds-theme_error slds-text-color_inverse slds-var-p-around_small">
Error message
</div>
<div class="slds-theme_warning slds-text-color_inverse slds-var-p-around_small">
Warning message
</div>
```
#### Avoid Custom CSS (Anti-Pattern)
```css
/* Don't create custom styles that override SLDS */
.custom-button {
background-color: red;
padding: 10px;
}
.my-special-layout {
display: flex;
justify-content: center;
}
```
#### When Custom CSS is Necessary
If you must use custom CSS, follow these guidelines:
- Use CSS custom properties (design tokens) when possible
- Prefix custom classes to avoid conflicts
- Never override SLDS base classes
```css
/* Custom CSS example */
.my-component-special {
border-radius: var(--lwc-borderRadiusMedium);
box-shadow: var(--lwc-shadowButton);
}
```
### 5. Component Architecture Best Practices
#### Reactive Properties
```javascript
import { LightningElement, track, api } from 'lwc';
export default class MyComponent extends LightningElement {
// Use @api for public properties
@api recordId;
@api title;
// Primitive properties (string, number, boolean) are automatically reactive
// No decorator needed - reassignment triggers re-render
simpleValue = 'initial';
count = 0;
// Computed properties
get displayName() {
return this.name ? `Hello, ${this.name}` : 'Hello, Guest';
}
// @track is NOT needed for simple property reassignment
// This will trigger reactivity automatically:
handleUpdate() {
this.simpleValue = 'updated'; // Reactive without @track
this.count++; // Reactive without @track
}
// @track IS needed when mutating nested properties without reassignment
@track complexData = {
user: {
name: 'John',
preferences: {
theme: 'dark'
}
}
};
handleDeepUpdate() {
// Requires @track because we're mutating a nested property
this.complexData.user.preferences.theme = 'light';
}
// BETTER: Avoid @track by using immutable patterns
regularData = {
user: {
name: 'John',
preferences: {
theme: 'dark'
}
}
};
handleImmutableUpdate() {
// No @track needed - we're creating a new object reference
this.regularData = {
...this.regularData,
user: {
...this.regularData.user,
preferences: {
...this.regularData.user.preferences,
theme: 'light'
}
}
};
}
// Arrays: @track is needed only for mutating methods
@track items = ['a', 'b', 'c'];
handleArrayMutation() {
// Requires @track
this.items.push('d');
this.items[0] = 'z';
}
// BETTER: Use immutable array operations
regularItems = ['a', 'b', 'c'];
handleImmutableArray() {
// No @track needed
this.regularItems = [...this.regularItems, 'd'];
this.regularItems = this.regularItems.map((item, idx) =>
idx === 0 ? 'z' : item
);
}
// Use @track only for complex objects/arrays when you mutate nested properties.
// For example, updating complexObject.details.status without reassigning complexObject.
@track complexObject = {
details: {
status: 'new'
}
};
}
```
#### Event Handling Patterns
```javascript
// Custom event dispatch
handleSave() {
const saveEvent = new CustomEvent('save', {
detail: {
recordData: this.recordData,
timestamp: new Date()
}
});
this.dispatchEvent(saveEvent);
}
// Lightning component event handling
handleInputChange(event) {
const fieldName = event.target.name;
const fieldValue = event.target.value;
// For lightning-input, lightning-combobox, etc.
this[fieldName] = fieldValue;
}
handleRadioChange(event) {
// For lightning-radio-group
this.selectedValue = event.detail.value;
}
handleToggleChange(event) {
// For lightning-input type="toggle"
this.isToggled = event.detail.checked;
}
```
### 6. Data Handling and Wire Services
#### Use @wire for Data Access
```javascript
import { getRecord } from 'lightning/uiRecordApi';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';
const FIELDS = ['Account.Name', 'Account.Industry', 'Account.AnnualRevenue'];
export default class MyComponent extends LightningElement {
@api recordId;
@wire(getRecord, { recordId: '$recordId', fields: FIELDS })
record;
@wire(getObjectInfo, { objectApiName: 'Account' })
objectInfo;
get recordData() {
return this.record.data ? this.record.data.fields : {};
}
}
```
### 7. Error Handling and User Experience
#### Implement Proper Error Boundaries
```javascript
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class MyComponent extends LightningElement {
isLoading = false;
error = null;
async handleAsyncOperation() {
this.isLoading = true;
this.error = null;
try {
const result = await this.performOperation();
this.showSuccessToast();
} catch (error) {
this.error = error;
this.showErrorToast(error.body?.message || 'An error occurred');
} finally {
this.isLoading = false;
}
}
performOperation() {
// Developer-defined async operation
}
showSuccessToast() {
const event = new ShowToastEvent({
title: 'Success',
message: 'Operation completed successfully',
variant: 'success'
});
this.dispatchEvent(event);
}
showErrorToast(message) {
const event = new ShowToastEvent({
title: 'Error',
message: message,
variant: 'error',
mode: 'sticky'
});
this.dispatchEvent(event);
}
}
```
### 8. Performance Optimization
#### Conditional Rendering
Prefer `lwc:if`, `lwc:elseif` and `lwc:else` for conditional rendering (API v58.0+). Legacy `if:true` / `if:false` are still supported but should be avoided in new components.
```html
<!-- Use template directives for conditional rendering -->
<template lwc:if={isLoading}>
<lightning-spinner alternative-text="Loading..."></lightning-spinner>
</template>
<template lwc:elseif={error}>
<div class="slds-theme_error slds-text-color_inverse slds-var-p-around_small">
{error.message}
</div>
</template>
<template lwc:else>
<template for:each={items} for:item="item">
<div key={item.id} class="slds-var-m-bottom_small">
{item.name}
</div>
</template>
</template>
```
```html
<!-- Legacy approach (avoid in new components) -->
<template if:true={isLoading}>
<lightning-spinner alternative-text="Loading..."></lightning-spinner>
</template>
<template if:true={error}>
<div class="slds-theme_error slds-text-color_inverse slds-var-p-around_small">
{error.message}
</div>
</template>
<template if:false={isLoading}>
<template if:false={error}>
<template for:each={items} for:item="item">
<div key={item.id} class="slds-var-m-bottom_small">
{item.name}
</div>
</template>
</template>
</template>
```
### 9. Accessibility Best Practices
#### Use Proper ARIA Labels and Semantic HTML
```html
<!-- Use semantic structure -->
<section aria-label="Product Selection">
<h2 class="slds-text-heading_medium">Products</h2>
<lightning-input
type="search"
label="Search Products"
placeholder="Enter product name..."
aria-describedby="search-help">
</lightning-input>
<div id="search-help" class="slds-assistive-text">
Type to filter the product list
</div>
</section>
```
## Common Anti-Patterns to Avoid
- **Direct DOM Manipulation**: Never use `document.querySelector()` or similar
- **jQuery or External Libraries**: Avoid non-Lightning compatible libraries
- **Inline Styles**: Use SLDS classes instead of `style` attributes
- **Global CSS**: All styles should be scoped to the component
- **Hardcoded Values**: Use custom labels, custom metadata, or constants
- **Imperative API Calls**: Prefer `@wire` over imperative `import` calls when possible
- **Memory Leaks**: Always clean up event listeners in `disconnectedCallback()`

View File

@@ -0,0 +1,357 @@
---
description: 'Best practices for building MCP-based declarative agents and API plugins for Microsoft 365 Copilot with Model Context Protocol integration'
applyTo: '**/{*mcp*,*agent*,*plugin*,declarativeAgent.json,ai-plugin.json,mcp.json,manifest.json}'
---
# MCP-based M365 Copilot Development Guidelines
## Core Principles
### Model Context Protocol First
- Leverage MCP servers for external system integration
- Import tools from server endpoints, not manual definitions
- Let MCP handle schema discovery and function generation
- Use point-and-click tool selection in Agents Toolkit
### Declarative Over Imperative
- Define agent behavior through configuration, not code
- Use declarativeAgent.json for instructions and capabilities
- Specify tools and actions in ai-plugin.json
- Configure MCP servers in mcp.json
### Security and Governance
- Always use OAuth 2.0 or SSO for authentication
- Follow principle of least privilege for tool selection
- Validate MCP server endpoints are secure
- Review compliance requirements before deployment
### User-Centric Design
- Create adaptive cards for rich visual responses
- Provide clear conversation starters
- Design for responsive experience across hubs
- Test thoroughly before organizational deployment
## MCP Server Design
### Server Selection
Choose MCP servers that:
- Expose relevant tools for user tasks
- Support secure authentication (OAuth 2.0, SSO)
- Provide reliable uptime and performance
- Follow MCP specification standards
- Return well-structured response data
### Tool Import Strategy
- Import only necessary tools (avoid over-scoping)
- Group related tools from same server
- Test each tool individually before combining
- Consider token limits when selecting multiple tools
### Authentication Configuration
**OAuth 2.0 Static Registration:**
```json
{
"type": "OAuthPluginVault",
"reference_id": "YOUR_AUTH_ID",
"client_id": "github_client_id",
"client_secret": "github_client_secret",
"authorization_url": "https://github.com/login/oauth/authorize",
"token_url": "https://github.com/login/oauth/access_token",
"scope": "repo read:user"
}
```
**SSO (Microsoft Entra ID):**
```json
{
"type": "OAuthPluginVault",
"reference_id": "sso_auth",
"authorization_url": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"token_url": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"scope": "User.Read"
}
```
## File Organization
### Project Structure
```
project-root/
├── appPackage/
│ ├── manifest.json # Teams app manifest
│ ├── declarativeAgent.json # Agent config (instructions, capabilities)
│ ├── ai-plugin.json # API plugin definition
│ ├── color.png # App icon color
│ └── outline.png # App icon outline
├── .vscode/
│ └── mcp.json # MCP server configuration
├── .env.local # Credentials (NEVER commit)
└── teamsapp.yml # Teams Toolkit config
```
### Critical Files
**declarativeAgent.json:**
- Agent name and description
- Instructions for behavior
- Conversation starters
- Capabilities (actions from plugins)
**ai-plugin.json:**
- MCP server tools import
- Response semantics (data_path, properties)
- Static adaptive card templates
- Function definitions (auto-generated)
**mcp.json:**
- MCP server URL
- Server metadata endpoint
- Authentication reference
**.env.local:**
- OAuth client credentials
- API keys and secrets
- Environment-specific config
- **CRITICAL**: Add to .gitignore
## Response Semantics Best Practices
### Data Path Configuration
Use JSONPath to extract relevant data:
```json
{
"data_path": "$.items[*]",
"properties": {
"title": "$.name",
"subtitle": "$.description",
"url": "$.html_url"
}
}
```
### Template Selection
For dynamic templates:
```json
{
"data_path": "$",
"template_selector": "$.templateType",
"properties": {
"title": "$.title",
"url": "$.url"
}
}
```
### Static Templates
Define in ai-plugin.json for consistent formatting:
- Use when all responses follow same structure
- Better performance than dynamic templates
- Easier to maintain and version control
## Adaptive Card Guidelines
### Design Principles
- **Single-column layout**: Stack elements vertically
- **Flexible widths**: Use "stretch" or "auto", not fixed pixels
- **Responsive design**: Test in Chat, Teams, Outlook
- **Minimal complexity**: Keep cards simple and scannable
### Template Language Patterns
**Conditionals:**
```json
{
"type": "TextBlock",
"text": "${if(status == 'active', '✅ Active', '❌ Inactive')}"
}
```
**Data Binding:**
```json
{
"type": "TextBlock",
"text": "${title}",
"weight": "bolder"
}
```
**Number Formatting:**
```json
{
"type": "TextBlock",
"text": "Score: ${formatNumber(score, 0)}"
}
```
**Conditional Rendering:**
```json
{
"type": "Container",
"$when": "${count(items) > 0}",
"items": [ ... ]
}
```
### Card Elements Usage
- **TextBlock**: Titles, descriptions, metadata
- **FactSet**: Key-value pairs (status, dates, IDs)
- **Image**: Icons, thumbnails (use size: "small")
- **Container**: Grouping related content
- **ActionSet**: Buttons for follow-up actions
## Testing and Deployment
### Local Testing Workflow
1. **Provision**: Teams Toolkit → Provision
2. **Deploy**: Teams Toolkit → Deploy
3. **Sideload**: App uploaded to Teams
4. **Test**: Visit [m365.cloud.microsoft/chat](https://m365.cloud.microsoft/chat)
5. **Iterate**: Fix issues and re-deploy
### Pre-Deployment Checklist
- [ ] All MCP server tools tested individually
- [ ] Authentication flow works end-to-end
- [ ] Adaptive cards render correctly across hubs
- [ ] Response semantics extract expected data
- [ ] Error handling provides clear messages
- [ ] Conversation starters are relevant and clear
- [ ] Agent instructions guide proper behavior
- [ ] Compliance and security reviewed
### Deployment Options
**Organization Deployment:**
- IT admin deploys to all or selected users
- Requires approval in Microsoft 365 admin center
- Best for internal business agents
**Agent Store:**
- Submit to Partner Center for validation
- Public availability to all Copilot users
- Requires rigorous security review
## Common Patterns
### Multi-Tool Agent
Import tools from multiple MCP servers:
```json
{
"mcpServers": {
"github": {
"url": "https://github-mcp.example.com"
},
"jira": {
"url": "https://jira-mcp.example.com"
}
}
}
```
### Search and Display
1. Tool retrieves data from MCP server
2. Response semantics extract relevant fields
3. Adaptive card displays formatted results
4. User can take action from card buttons
### Authenticated Actions
1. User triggers tool requiring auth
2. OAuth flow redirects for consent
3. Access token stored in plugin vault
4. Subsequent requests use stored token
## Error Handling
### MCP Server Errors
- Provide clear error messages in agent responses
- Fall back to alternative tools if available
- Log errors for debugging
- Guide user to retry or alternative approach
### Authentication Failures
- Check OAuth credentials in .env.local
- Verify scopes match required permissions
- Test auth flow outside Copilot first
- Ensure token refresh logic works
### Response Parsing Failures
- Validate JSONPath expressions in response semantics
- Handle missing or null data gracefully
- Provide default values where appropriate
- Test with varied API responses
## Performance Optimization
### Tool Selection
- Import only necessary tools (reduces token usage)
- Avoid redundant tools from multiple servers
- Test impact of each tool on response time
### Response Size
- Use data_path to filter unnecessary data
- Limit result sets where possible
- Consider pagination for large datasets
- Keep adaptive cards lightweight
### Caching Strategy
- MCP servers should cache where appropriate
- Agent responses may be cached by M365
- Consider cache invalidation for time-sensitive data
## Security Best Practices
### Credential Management
- **NEVER** commit .env.local to source control
- Use environment variables for all secrets
- Rotate OAuth credentials regularly
- Use separate credentials for dev/prod
### Data Privacy
- Only request minimum necessary scopes
- Avoid logging sensitive user data
- Review data residency requirements
- Follow compliance policies (GDPR, etc.)
### Server Validation
- Verify MCP server is trusted and secure
- Check HTTPS endpoints only
- Review server's privacy policy
- Test for injection vulnerabilities
## Governance and Compliance
### Admin Controls
Agents can be:
- **Blocked**: Prevented from use
- **Deployed**: Assigned to specific users/groups
- **Published**: Made available organization-wide
### Monitoring
Track:
- Agent usage and adoption
- Error rates and performance
- User feedback and satisfaction
- Security incidents
### Audit Requirements
Maintain:
- Change history for agent configurations
- Access logs for sensitive operations
- Approval records for deployments
- Compliance attestations
## Resources and References
### Official Documentation
- [Build Declarative Agents with MCP (DevBlogs)](https://devblogs.microsoft.com/microsoft365dev/build-declarative-agents-for-microsoft-365-copilot-with-mcp/)
- [Build MCP Plugins (Learn)](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/build-mcp-plugins)
- [API Plugin Adaptive Cards (Learn)](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/api-plugin-adaptive-cards)
- [Manage Copilot Agents (Learn)](https://learn.microsoft.com/en-us/microsoft-365/admin/manage/manage-copilot-agents-integrated-apps)
### Tools and SDKs
- Microsoft 365 Agents Toolkit (VS Code extension v6.3.x+)
- Teams Toolkit for agent packaging
- Adaptive Cards Designer
- MCP specification documentation
### Partner Examples
- monday.com: Task management integration
- Canva: Design automation
- Sitecore: Content management

View File

@@ -0,0 +1,834 @@
---
description: 'Scala 2.12/2.13 programming language coding conventions and best practices following Databricks style guide for functional programming, type safety, and production code quality.'
applyTo: '**.scala, **/build.sbt, **/build.sc'
---
# Scala Best Practices
Based on the [Databricks Scala Style Guide](https://github.com/databricks/scala-style-guide)
## Core Principles
### Write Simple Code
Code is written once but read and modified multiple times. Optimize for long-term readability and maintainability by writing simple code.
### Immutability by Default
- Always prefer `val` over `var`
- Use immutable collections from `scala.collection.immutable`
- Case class constructor parameters should NOT be mutable
- Use copy constructor to create modified instances
```scala
// Good - Immutable case class
case class Person(name: String, age: Int)
// Bad - Mutable case class
case class Person(name: String, var age: Int)
// To change values, use copy constructor
val p1 = Person("Peter", 15)
val p2 = p1.copy(age = 16)
// Good - Immutable collections
val users = List(User("Alice", 30), User("Bob", 25))
val updatedUsers = users.map(u => u.copy(age = u.age + 1))
```
### Pure Functions
- Functions should be deterministic and side-effect free
- Separate pure logic from effects
- Use explicit types for methods with effects
```scala
// Good - Pure function
def calculateTotal(items: List[Item]): BigDecimal =
items.map(_.price).sum
// Bad - Impure function with side effects
def calculateTotal(items: List[Item]): BigDecimal = {
println(s"Calculating total for ${items.size} items") // Side effect
val total = items.map(_.price).sum
saveToDatabase(total) // Side effect
total
}
```
## Naming Conventions
### Classes and Objects
```scala
// Classes, traits, objects - PascalCase
class ClusterManager
trait Expression
object Configuration
// Packages - all lowercase ASCII
package com.databricks.resourcemanager
// Methods/functions - camelCase
def getUserById(id: Long): Option[User]
def processData(input: String): Result
// Constants - uppercase in companion object
object Configuration {
val DEFAULT_PORT = 10000
val MAX_RETRIES = 3
val TIMEOUT_MS = 5000L
}
```
### Variables and Parameters
```scala
// Variables - camelCase, self-evident names
val serverPort = 1000
val clientPort = 2000
val maxRetryAttempts = 3
// One-character names OK in small, localized scope
for (i <- 0 until 10) {
// ...
}
// Do NOT use "l" (Larry) - looks like "1", "|", "I"
```
### Enumerations
```scala
// Enumeration object - PascalCase
// Values - UPPER_CASE with underscores
private object ParseState extends Enumeration {
type ParseState = Value
val PREFIX,
TRIM_BEFORE_SIGN,
SIGN,
VALUE,
UNIT_BEGIN,
UNIT_END = Value
}
```
## Syntactic Style
### Line Length and Spacing
```scala
// Limit lines to 100 characters
// One space before and after operators
def add(int1: Int, int2: Int): Int = int1 + int2
// One space after commas
val list = List("a", "b", "c")
// One space after colons
def getConf(key: String, defaultValue: String): String = {
// code
}
// Use 2-space indentation
if (true) {
println("Wow!")
}
// 4-space indentation for long parameter lists
def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
path: String,
fClass: Class[F],
kClass: Class[K],
vClass: Class[V],
conf: Configuration = hadoopConfiguration): RDD[(K, V)] = {
// method body
}
// Class with long parameters
class Foo(
val param1: String, // 4 space indent
val param2: String,
val param3: Array[Byte])
extends FooInterface // 2 space indent
with Logging {
def firstMethod(): Unit = { ... } // blank line above
}
```
### Rule of 30
- A method should contain less than 30 lines of code
- A class should contain less than 30 methods
### Curly Braces
```scala
// Always use curly braces for multi-line blocks
if (true) {
println("Wow!")
}
// Exception: one-line ternary (side-effect free)
val result = if (condition) value1 else value2
// Always use braces for try-catch
try {
foo()
} catch {
case e: Exception => handle(e)
}
```
### Long Literals
```scala
// Use uppercase L for long literals
val longValue = 5432L // Do this
val badValue = 5432l // Don't do this - hard to see
```
### Parentheses
```scala
// Methods with side-effects - use parentheses
class Job {
def killJob(): Unit = { ... } // Correct - changes state
def getStatus: JobStatus = { ... } // Correct - no side-effect
}
// Callsite should match declaration
new Job().killJob() // Correct
new Job().getStatus // Correct
```
### Imports
```scala
// Avoid wildcard imports unless importing 6+ entities
import scala.collection.mutable.{Map, HashMap, ArrayBuffer}
// OK to use wildcard for implicits or 6+ items
import scala.collection.JavaConverters._
import java.util.{Map, HashMap, List, ArrayList, Set, HashSet}
// Always use absolute paths
import scala.util.Random // Good
// import util.Random // Don't use relative
// Import order (with blank lines):
import java.io.File
import javax.servlet.http.HttpServlet
import scala.collection.mutable.HashMap
import scala.util.Random
import org.apache.spark.SparkContext
import org.apache.spark.rdd.RDD
import com.databricks.MyClass
```
### Pattern Matching
```scala
// Put match on same line if method is entirely pattern match
def test(msg: Message): Unit = msg match {
case TextMessage(text) => handleText(text)
case ImageMessage(url) => handleImage(url)
}
// Single case closures - same line
list.zipWithIndex.map { case (elem, i) =>
// process
}
// Multiple cases - indent and wrap
list.map {
case a: Foo => processFoo(a)
case b: Bar => processBar(b)
case _ => handleDefault()
}
// Match on type only - don't expand all args
case class Pokemon(name: String, weight: Int, hp: Int, attack: Int, defense: Int)
// Bad - brittle when fields change
targets.foreach {
case Pokemon(_, _, hp, _, defense) =>
// error prone
}
// Good - match on type
targets.foreach {
case p: Pokemon =>
val loss = math.min(0, myAttack - p.defense)
p.copy(hp = p.hp - loss)
}
```
### Anonymous Functions
```scala
// Avoid excessive parentheses
// Correct
list.map { item =>
transform(item)
}
// Correct
list.map(item => transform(item))
// Wrong - unnecessary braces
list.map(item => {
transform(item)
})
// Wrong - excessive nesting
list.map({ item => ... })
```
### Infix Methods
```scala
// Avoid infix for non-symbolic methods
list.map(func) // Correct
list map func // Wrong
// OK for operators
arrayBuffer += elem
```
## Language Features
### Avoid apply() on Classes
```scala
// Avoid apply on classes - hard to trace
class TreeNode {
def apply(name: String): TreeNode = { ... } // Don't do this
}
// OK on companion objects as factory
object TreeNode {
def apply(name: String): TreeNode = new TreeNode(name) // OK
}
```
### override Modifier
```scala
// Always use override - even for abstract methods
trait Parent {
def hello(data: Map[String, String]): Unit
}
class Child extends Parent {
// Without override, this might not actually override!
override def hello(data: Map[String, String]): Unit = {
println(data)
}
}
```
### Avoid Destructuring in Constructors
```scala
// Don't use destructuring binds in constructors
class MyClass {
// Bad - creates non-transient Tuple2
@transient private val (a, b) = someFuncThatReturnsTuple2()
// Good
@transient private val tuple = someFuncThatReturnsTuple2()
@transient private val a = tuple._1
@transient private val b = tuple._2
}
```
### Avoid Call-by-Name
```scala
// Avoid call-by-name parameters
// Bad - caller can't tell if executed once or many times
def print(value: => Int): Unit = {
println(value)
println(value + 1)
}
// Good - explicit function type
def print(value: () => Int): Unit = {
println(value())
println(value() + 1)
}
```
### Avoid Multiple Parameter Lists
```scala
// Avoid multiple parameter lists (except for implicits)
// Bad
case class Person(name: String, age: Int)(secret: String)
// Good
case class Person(name: String, age: Int, secret: String)
// Exception: separate list for implicits (but avoid implicits!)
def foo(x: Int)(implicit ec: ExecutionContext): Future[Int]
```
### Symbolic Methods
```scala
// Only use for arithmetic operators
class Vector {
def +(other: Vector): Vector = { ... } // OK
def -(other: Vector): Vector = { ... } // OK
}
// Don't use for other methods
// Bad
channel ! msg
stream1 >>= stream2
// Good
channel.send(msg)
stream1.join(stream2)
```
### Type Inference
```scala
// Always type public methods
def getUserById(id: Long): Option[User] = { ... }
// Always type implicit methods
implicit def stringToInt(s: String): Int = s.toInt
// Type variables when not obvious (3 second rule)
val user: User = complexComputation()
// OK to omit when obvious
val count = 5
val name = "Alice"
```
### Return Statements
```scala
// Avoid return in closures - uses exceptions under the hood
def receive(rpc: WebSocketRPC): Option[Response] = {
tableFut.onComplete { table =>
if (table.isFailure) {
return None // Don't do this - wrong thread!
}
}
}
// Use return as guard to simplify control flow
def doSomething(obj: Any): Any = {
if (obj eq null) {
return null
}
// do something
}
// Use return to break loops early
while (true) {
if (cond) {
return
}
}
```
### Recursion and Tail Recursion
```scala
// Avoid recursion unless naturally recursive (trees, graphs)
// Use @tailrec for tail-recursive methods
@scala.annotation.tailrec
def max0(data: Array[Int], pos: Int, max: Int): Int = {
if (pos == data.length) {
max
} else {
max0(data, pos + 1, if (data(pos) > max) data(pos) else max)
}
}
// Prefer explicit loops for clarity
def max(data: Array[Int]): Int = {
var max = Int.MinValue
for (v <- data) {
if (v > max) {
max = v
}
}
max
}
```
### Implicits
```scala
// Avoid implicits unless:
// 1. Building a DSL
// 2. Implicit type parameters (ClassTag, TypeTag)
// 3. Private type conversions within your class
// If you must use them, don't overload
object ImplicitHolder {
// Bad - can't selectively import
def toRdd(seq: Seq[Int]): RDD[Int] = { ... }
def toRdd(seq: Seq[Long]): RDD[Long] = { ... }
}
// Good - distinct names
object ImplicitHolder {
def intSeqToRdd(seq: Seq[Int]): RDD[Int] = { ... }
def longSeqToRdd(seq: Seq[Long]): RDD[Long] = { ... }
}
```
## Type Safety
### Algebraic Data Types
```scala
// Sum types - sealed traits with case classes
sealed trait PaymentMethod
case class CreditCard(number: String, cvv: String) extends PaymentMethod
case class PayPal(email: String) extends PaymentMethod
case class BankTransfer(account: String, routing: String) extends PaymentMethod
def processPayment(payment: PaymentMethod): Either[Error, Receipt] = payment match {
case CreditCard(number, cvv) => chargeCreditCard(number, cvv)
case PayPal(email) => chargePayPal(email)
case BankTransfer(account, routing) => chargeBankAccount(account, routing)
}
// Product types - case classes
case class User(id: Long, name: String, email: String, age: Int)
case class Order(id: Long, userId: Long, items: List[Item], total: BigDecimal)
```
### Option over null
```scala
// Use Option instead of null
def findUserById(id: Long): Option[User] = {
database.query(id)
}
// Use Option() to guard against nulls
def myMethod1(input: String): Option[String] = Option(transform(input))
// Don't use Some() - it won't protect against null
def myMethod2(input: String): Option[String] = Some(transform(input)) // Bad
// Pattern matching on Option
def processUser(id: Long): String = findUserById(id) match {
case Some(user) => s"Found: ${user.name}"
case None => "User not found"
}
// Don't call get() unless absolutely sure
val user = findUserById(123).get // Dangerous!
// Use getOrElse, map, flatMap, fold instead
val name = findUserById(123).map(_.name).getOrElse("Unknown")
```
### Error Handling with Either
```scala
sealed trait ValidationError
case class InvalidEmail(email: String) extends ValidationError
case class InvalidAge(age: Int) extends ValidationError
case class MissingField(field: String) extends ValidationError
def validateUser(data: Map[String, String]): Either[ValidationError, User] = {
for {
name <- data.get("name").toRight(MissingField("name"))
email <- data.get("email").toRight(MissingField("email"))
validEmail <- validateEmail(email)
ageStr <- data.get("age").toRight(MissingField("age"))
age <- ageStr.toIntOption.toRight(InvalidAge(-1))
} yield User(name, validEmail, age)
}
```
### Try vs Exceptions
```scala
// Don't return Try from APIs
// Bad
def getUser(id: Long): Try[User]
// Good - explicit throws
@throws(classOf[DatabaseConnectionException])
def getUser(id: Long): Option[User]
// Use NonFatal for catching exceptions
import scala.util.control.NonFatal
try {
dangerousOperation()
} catch {
case NonFatal(e) =>
logger.error("Operation failed", e)
case e: InterruptedException =>
// handle interruption
}
```
## Collections
### Prefer Immutable Collections
```scala
import scala.collection.immutable._
// Good
val numbers = List(1, 2, 3, 4, 5)
val doubled = numbers.map(_ * 2)
val evens = numbers.filter(_ % 2 == 0)
val userMap = Map(
1L -> "Alice",
2L -> "Bob"
)
val updated = userMap + (3L -> "Charlie")
// Use Stream (Scala 2.12) or LazyList (Scala 2.13) for lazy sequences
val fibonacci: LazyList[BigInt] =
BigInt(0) #:: BigInt(1) #:: fibonacci.zip(fibonacci.tail).map { case (a, b) => a + b }
val first10 = fibonacci.take(10).toList
```
### Monadic Chaining
```scala
// Avoid chaining more than 3 operations
// Break after flatMap
// Don't chain with if-else blocks
// Bad - too complex
database.get(name).flatMap { elem =>
elem.data.get("address").flatMap(Option.apply)
}
// Good - more readable
def getAddress(name: String): Option[String] = {
if (!database.contains(name)) {
return None
}
database(name).data.get("address") match {
case Some(null) => None
case Some(addr) => Option(addr)
case None => None
}
}
// Don't chain with if-else
// Bad
if (condition) {
Seq(1, 2, 3)
} else {
Seq(1, 2, 3)
}.map(_ + 1)
// Good
val seq = if (condition) Seq(1, 2, 3) else Seq(4, 5, 6)
seq.map(_ + 1)
```
## Performance
### Use while Loops
```scala
// For performance-critical code, use while instead of for/map
val arr = Array.fill(1000)(Random.nextInt())
// Slow
val newArr = arr.zipWithIndex.map { case (elem, i) =>
if (i % 2 == 0) 0 else elem
}
// Fast
val newArr = new Array[Int](arr.length)
var i = 0
while (i < arr.length) {
newArr(i) = if (i % 2 == 0) 0 else arr(i)
i += 1
}
```
### Option vs null
```scala
// For performance-critical code, prefer null over Option
class Foo {
@javax.annotation.Nullable
private[this] var nullableField: Bar = _
}
```
### Use private[this]
```scala
// private[this] generates fields, not accessor methods
class MyClass {
private val field1 = ... // Might use accessor
private[this] val field2 = ... // Direct field access
def perfSensitiveMethod(): Unit = {
var i = 0
while (i < 1000000) {
field2 // Guaranteed field access
i += 1
}
}
}
```
### Java Collections
```scala
// For performance, prefer Java collections
import java.util.{ArrayList, HashMap}
val list = new ArrayList[String]()
val map = new HashMap[String, Int]()
```
## Concurrency
### Prefer ConcurrentHashMap
```scala
// Use java.util.concurrent.ConcurrentHashMap
private[this] val map = new java.util.concurrent.ConcurrentHashMap[String, String]
// Or synchronized map for low contention
private[this] val map = java.util.Collections.synchronizedMap(
new java.util.HashMap[String, String]
)
```
### Explicit Synchronization
```scala
class Manager {
private[this] var count = 0
private[this] val map = new java.util.HashMap[String, String]
def update(key: String, value: String): Unit = synchronized {
map.put(key, value)
count += 1
}
def getCount: Int = synchronized { count }
}
```
### Atomic Variables
```scala
import java.util.concurrent.atomic._
// Prefer Atomic over @volatile
val initialized = new AtomicBoolean(false)
// Clearly express only-once execution
if (!initialized.getAndSet(true)) {
initialize()
}
```
## Testing
### Intercept Specific Exceptions
```scala
import org.scalatest._
// Bad - too broad
intercept[Exception] {
thingThatThrows()
}
// Good - specific type
intercept[IllegalArgumentException] {
thingThatThrows()
}
```
## SBT Configuration
```scala
// build.sbt
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.13.12"
ThisBuild / organization := "com.example"
lazy val root = (project in file("."))
.settings(
name := "my-application",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "2.10.0",
"org.typelevel" %% "cats-effect" % "3.5.2",
// Testing
"org.scalatest" %% "scalatest" % "3.2.17" % Test,
"org.scalatestplus" %% "scalacheck-1-17" % "3.2.17.0" % Test
),
scalacOptions ++= Seq(
"-encoding", "UTF-8",
"-feature",
"-unchecked",
"-deprecation",
"-Xfatal-warnings"
)
)
```
## Miscellaneous
### Use nanoTime
```scala
// Use nanoTime for durations, not currentTimeMillis
val start = System.nanoTime()
doWork()
val elapsed = System.nanoTime() - start
import java.util.concurrent.TimeUnit
val elapsedMs = TimeUnit.NANOSECONDS.toMillis(elapsed)
```
### URI over URL
```scala
// Use URI instead of URL (URL.equals does DNS lookup!)
val uri = new java.net.URI("http://example.com")
// Not: val url = new java.net.URL("http://example.com")
```
## Summary
1. **Write simple code** - Optimize for readability and maintainability
2. **Use immutable data** - val, immutable collections, case classes
3. **Avoid language features** - Limit implicits, avoid symbolic methods
4. **Type public APIs** - Explicit types for methods and fields
5. **Prefer explicit over implicit** - Clear is better than concise
6. **Use standard libraries** - Don't reinvent the wheel
7. **Follow naming conventions** - PascalCase, camelCase, UPPER_CASE
8. **Keep methods small** - Rule of 30
9. **Handle errors explicitly** - Option, Either, exceptions with @throws
10. **Profile before optimizing** - Measure, don't guess
For complete details, see the [Databricks Scala Style Guide](https://github.com/databricks/scala-style-guide).

View File

@@ -0,0 +1,440 @@
---
description: 'Guidelines and best practices for building TypeSpec-based declarative agents and API plugins for Microsoft 365 Copilot'
applyTo: '**/*.tsp'
---
# TypeSpec for Microsoft 365 Copilot Development Guidelines
## Core Principles
When working with TypeSpec for Microsoft 365 Copilot:
1. **Type Safety First**: Leverage TypeSpec's strong typing for all models and operations
2. **Declarative Approach**: Use decorators to describe intent, not implementation
3. **Scoped Capabilities**: Always scope capabilities to specific resources when possible
4. **Clear Instructions**: Write explicit, detailed agent instructions
5. **User-Centric**: Design for the end-user experience in Microsoft 365 Copilot
## File Organization
### Standard Structure
```
project/
├── appPackage/
│ ├── cards/ # Adaptive Card templates
│ │ └── *.json
│ ├── .generated/ # Generated manifests (auto-generated)
│ └── manifest.json # Teams app manifest
├── src/
│ ├── main.tsp # Agent definition
│ └── actions.tsp # API operations (for plugins)
├── m365agents.yml # Agents Toolkit configuration
└── package.json
```
### Import Statements
Always include required imports at the top of TypeSpec files:
```typescript
import "@typespec/http";
import "@typespec/openapi3";
import "@microsoft/typespec-m365-copilot";
using TypeSpec.Http;
using TypeSpec.M365.Copilot.Agents; // For agents
using TypeSpec.M365.Copilot.Actions; // For API plugins
```
## Agent Development Best Practices
### Agent Declaration
```typescript
@agent({
name: "Role-Based Name", // e.g., "Customer Support Assistant"
description: "Clear, concise description under 1,000 characters"
})
```
- Use role-based names that describe what the agent does
- Make descriptions informative but concise
- Avoid generic names like "Helper" or "Bot"
### Instructions
```typescript
@instructions("""
You are a [specific role] specialized in [domain].
Your responsibilities include:
- [Key responsibility 1]
- [Key responsibility 2]
When helping users:
- [Behavioral guideline 1]
- [Behavioral guideline 2]
You should NOT:
- [Constraint 1]
- [Constraint 2]
""")
```
- Write in second person ("You are...")
- Be specific about the agent's role and expertise
- Define both what to do AND what not to do
- Keep under 8,000 characters
- Use clear, structured formatting
### Conversation Starters
```typescript
@conversationStarter(#{
title: "Action-Oriented Title", // e.g., "Check Status"
text: "Specific example query" // e.g., "What's the status of my ticket?"
})
```
- Provide 2-4 diverse starters
- Make each showcase a different capability
- Use action-oriented titles
- Write realistic example queries
### Capabilities - Knowledge Sources
**Web Search** - Scope to specific sites when possible:
```typescript
op webSearch is AgentCapabilities.WebSearch<Sites = [
{ url: "https://learn.microsoft.com" },
{ url: "https://docs.microsoft.com" }
]>;
```
**OneDrive and SharePoint** - Use URLs or IDs:
```typescript
op oneDriveAndSharePoint is AgentCapabilities.OneDriveAndSharePoint<
ItemsByUrl = [
{ url: "https://contoso.sharepoint.com/sites/Engineering" }
]
>;
```
**Teams Messages** - Specify channels/chats:
```typescript
op teamsMessages is AgentCapabilities.TeamsMessages<Urls = [
{ url: "https://teams.microsoft.com/l/channel/..." }
]>;
```
**Email** - Scope to specific folders:
```typescript
op email is AgentCapabilities.Email<
Folders = [
{ folderId: "Inbox" },
{ folderId: "SentItems" }
],
SharedMailbox = "support@contoso.com" // Optional
>;
```
**People** - No scoping needed:
```typescript
op people is AgentCapabilities.People;
```
**Copilot Connectors** - Specify connection IDs:
```typescript
op copilotConnectors is AgentCapabilities.GraphConnectors<
Connections = [
{ connectionId: "your-connector-id" }
]
>;
```
**Dataverse** - Scope to specific tables:
```typescript
op dataverse is AgentCapabilities.Dataverse<
KnowledgeSources = [
{
hostName: "contoso.crm.dynamics.com";
tables: [
{ tableName: "account" },
{ tableName: "contact" }
];
}
]
>;
```
### Capabilities - Productivity Tools
```typescript
// Python code execution
op codeInterpreter is AgentCapabilities.CodeInterpreter;
// Image generation
op graphicArt is AgentCapabilities.GraphicArt;
// Meeting content access
op meetings is AgentCapabilities.Meetings;
// Specialized AI models
op scenarioModels is AgentCapabilities.ScenarioModels<
ModelsById = [
{ id: "model-id" }
]
>;
```
## API Plugin Development Best Practices
### Service Definition
```typescript
@service
@actions(#{
nameForHuman: "User-Friendly API Name",
descriptionForHuman: "What users will understand",
descriptionForModel: "What the model needs to know",
contactEmail: "support@company.com",
privacyPolicyUrl: "https://company.com/privacy",
legalInfoUrl: "https://company.com/terms"
})
@server("https://api.example.com", "API Name")
@useAuth([AuthType]) // If authentication needed
namespace APINamespace {
// Operations here
}
```
### Operation Definition
```typescript
@route("/resource/{id}")
@get
@action
@card(#{
dataPath: "$.items",
title: "$.title",
file: "cards/card.json"
})
@capabilities(#{
confirmation: #{
type: "AdaptiveCard",
title: "Confirm Action",
body: "Confirm with {{ function.parameters.param }}"
}
})
@reasoning("Consider X when Y")
@responding("Present results as Z")
op getResource(
@path id: string,
@query filter?: string
): ResourceResponse;
```
### Models
```typescript
model Resource {
id: string;
name: string;
description?: string; // Optional fields
status: "active" | "inactive"; // Union types for enums
@format("date-time")
createdAt: utcDateTime;
@format("uri")
url?: string;
}
model ResourceList {
items: Resource[];
totalCount: int32;
nextPage?: string;
}
```
### Authentication
**API Key**
```typescript
@useAuth(ApiKeyAuth<ApiKeyLocation.header, "X-API-Key">)
// Or with reference ID
@useAuth(Auth)
@authReferenceId("${{ENV_VAR_REFERENCE_ID}}")
model Auth is ApiKeyAuth<ApiKeyLocation.header, "X-API-Key">;
```
**OAuth2**
```typescript
@useAuth(OAuth2Auth<[{
type: OAuth2FlowType.authorizationCode;
authorizationUrl: "https://auth.example.com/authorize";
tokenUrl: "https://auth.example.com/token";
refreshUrl: "https://auth.example.com/refresh";
scopes: ["read", "write"];
}]>)
// Or with reference ID
@useAuth(Auth)
@authReferenceId("${{OAUTH_REFERENCE_ID}}")
model Auth is OAuth2Auth<[...]>;
```
## Naming Conventions
### Files
- `main.tsp` - Agent definition
- `actions.tsp` - API operations
- `[feature].tsp` - Additional feature files
- `cards/*.json` - Adaptive Card templates
### TypeSpec Elements
- **Namespaces**: PascalCase (e.g., `CustomerSupportAgent`)
- **Operations**: camelCase (e.g., `listProjects`, `createTicket`)
- **Models**: PascalCase (e.g., `Project`, `TicketResponse`)
- **Model Properties**: camelCase (e.g., `projectId`, `createdDate`)
## Common Patterns
### Multi-Capability Agent
```typescript
@agent("Knowledge Worker", "Description")
@instructions("...")
namespace KnowledgeWorker {
op webSearch is AgentCapabilities.WebSearch;
op files is AgentCapabilities.OneDriveAndSharePoint;
op people is AgentCapabilities.People;
}
```
### CRUD API Plugin
```typescript
namespace ProjectAPI {
@route("/projects") @get @action
op list(): Project[];
@route("/projects/{id}") @get @action
op get(@path id: string): Project;
@route("/projects") @post @action
@capabilities(#{confirmation: ...})
op create(@body project: CreateProject): Project;
@route("/projects/{id}") @patch @action
@capabilities(#{confirmation: ...})
op update(@path id: string, @body project: UpdateProject): Project;
@route("/projects/{id}") @delete @action
@capabilities(#{confirmation: ...})
op delete(@path id: string): void;
}
```
### Adaptive Card Data Binding
```json
{
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5",
"body": [
{
"type": "Container",
"$data": "${$root}",
"items": [
{
"type": "TextBlock",
"text": "Title: ${if(title, title, 'N/A')}",
"wrap": true
}
]
}
]
}
```
## Validation and Testing
### Before Provisioning
1. Run TypeSpec validation: `npm run build` or use Agents Toolkit
2. Check all file paths in `@card` decorators exist
3. Verify authentication references match configuration
4. Ensure capability scoping is appropriate
5. Review instructions for clarity and length
### Testing Strategy
1. **Provision**: Deploy to development environment
2. **Test**: Use Microsoft 365 Copilot at https://m365.cloud.microsoft/chat
3. **Debug**: Enable Copilot developer mode for orchestrator insights
4. **Iterate**: Refine based on actual behavior
5. **Validate**: Test all conversation starters and capabilities
## Performance Optimization
1. **Scope Capabilities**: Don't grant access to all data if only subset needed
2. **Limit Operations**: Only expose API operations the agent actually uses
3. **Efficient Models**: Keep response models focused on necessary data
4. **Card Optimization**: Use conditional rendering (`$when`) in Adaptive Cards
5. **Caching**: Design APIs with appropriate caching headers
## Security Best Practices
1. **Authentication**: Always use authentication for non-public APIs
2. **Scoping**: Limit capability access to minimum required resources
3. **Validation**: Validate all inputs in API operations
4. **Secrets**: Use environment variables for sensitive data
5. **References**: Use `@authReferenceId` for production credentials
6. **Permissions**: Request minimum necessary OAuth scopes
## Error Handling
```typescript
model ErrorResponse {
error: {
code: string;
message: string;
details?: ErrorDetail[];
};
}
model ErrorDetail {
field?: string;
message: string;
}
```
## Documentation
Include comments in TypeSpec for complex operations:
```typescript
/**
* Retrieves project details with associated tasks and team members.
*
* @param id - Unique project identifier
* @param includeArchived - Whether to include archived tasks
* @returns Complete project information
*/
@route("/projects/{id}")
@get
@action
op getProjectDetails(
@path id: string,
@query includeArchived?: boolean
): ProjectDetails;
```
## Common Pitfalls to Avoid
1. ❌ Generic agent names ("Helper Bot")
2. ❌ Vague instructions ("Help users with things")
3. ❌ No capability scoping (accessing all data)
4. ❌ Missing confirmations on destructive operations
5. ❌ Overly complex Adaptive Cards
6. ❌ Hard-coded credentials in TypeSpec files
7. ❌ Missing error response models
8. ❌ Inconsistent naming conventions
9. ❌ Too many capabilities (use only what's needed)
10. ❌ Instructions over 8,000 characters
## Resources
- [TypeSpec Official Docs](https://typespec.io/)
- [Microsoft 365 Copilot Extensibility](https://learn.microsoft.com/microsoft-365-copilot/extensibility/)
- [Agents Toolkit](https://aka.ms/M365AgentsToolkit)
- [Adaptive Cards Designer](https://adaptivecards.io/designer/)

View File

@@ -0,0 +1,647 @@
---
description: 'Guidelines for Visual Studio extension (VSIX) development using Community.VisualStudio.Toolkit'
applyTo: '**/*.cs, **/*.vsct, **/*.xaml, **/source.extension.vsixmanifest'
---
# Visual Studio Extension Development with Community.VisualStudio.Toolkit
## Scope
**These instructions apply ONLY to Visual Studio extensions using `Community.VisualStudio.Toolkit`.**
Verify the project uses the toolkit by checking for:
- `Community.VisualStudio.Toolkit.*` NuGet package reference
- `ToolkitPackage` base class (not raw `AsyncPackage`)
- `BaseCommand<T>` pattern for commands
**If the project uses raw VSSDK (`AsyncPackage` directly) or the new `VisualStudio.Extensibility` model, do not apply these instructions.**
## Goals
- Generate async-first, thread-safe extension code
- Use toolkit abstractions (`VS.*` helpers, `BaseCommand<T>`, `BaseOptionModel<T>`)
- Ensure all UI respects Visual Studio themes
- Follow VSSDK and VSTHRD analyzer rules
- Produce testable, maintainable extension code
## Example Prompt Behaviors
### ✅ Good Suggestions
- "Create a command that opens the current file's containing folder using `BaseCommand<T>`"
- "Add an options page with a boolean setting using `BaseOptionModel<T>`"
- "Write a tagger provider for C# files that highlights TODO comments"
- "Show a status bar progress indicator while processing files"
### ❌ Avoid
- Suggesting raw `AsyncPackage` instead of `ToolkitPackage`
- Using `OleMenuCommandService` directly instead of `BaseCommand<T>`
- Creating WPF elements without switching to UI thread first
- Using `.Result`, `.Wait()`, or `Task.Run` for UI work
- Hardcoding colors instead of using VS theme colors
## Project Structure
```
src/
├── Commands/ # Command handlers (menu items, toolbar buttons)
├── Options/ # Settings/options pages
├── Services/ # Business logic and services
├── Tagging/ # ITagger implementations (syntax highlighting, outlining)
├── Adornments/ # Editor adornments (IntraTextAdornment, margins)
├── QuickInfo/ # QuickInfo/tooltip providers
├── SuggestedActions/ # Light bulb actions
├── Handlers/ # Event handlers (format document, paste, etc.)
├── Resources/ # Images, icons, license files
├── source.extension.vsixmanifest # Extension manifest
├── VSCommandTable.vsct # Command definitions (menus, buttons)
├── VSCommandTable.cs # Auto-generated command IDs
└── *Package.cs # Main package class
```
## Community.VisualStudio.Toolkit Patterns
### Global Usings
Extensions using the toolkit should have these global usings in the Package file:
```csharp
global using System;
global using Community.VisualStudio.Toolkit;
global using Microsoft.VisualStudio.Shell;
global using Task = System.Threading.Tasks.Task;
```
### Package Class
```csharp
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[InstalledProductRegistration(Vsix.Name, Vsix.Description, Vsix.Version)]
[ProvideMenuResource("Menus.ctmenu", 1)]
[Guid(PackageGuids.YourExtensionString)]
[ProvideOptionPage(typeof(OptionsProvider.GeneralOptions), Vsix.Name, "General", 0, 0, true, SupportsProfiles = true)]
public sealed class YourPackage : ToolkitPackage
{
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
await this.RegisterCommandsAsync();
}
}
```
### Commands
Commands use the `[Command]` attribute and inherit from `BaseCommand<T>`:
```csharp
[Command(PackageIds.YourCommandId)]
internal sealed class YourCommand : BaseCommand<YourCommand>
{
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
// Command implementation
}
// Optional: Control command state (enabled, checked, visible)
protected override void BeforeQueryStatus(EventArgs e)
{
Command.Checked = someCondition;
Command.Enabled = anotherCondition;
}
}
```
### Options Pages
```csharp
internal partial class OptionsProvider
{
[ComVisible(true)]
public class GeneralOptions : BaseOptionPage<General> { }
}
public class General : BaseOptionModel<General>
{
[Category("Category Name")]
[DisplayName("Setting Name")]
[Description("Description of the setting.")]
[DefaultValue(true)]
public bool MySetting { get; set; } = true;
}
```
## MEF Components
### Tagger Providers
Use `[Export]` and appropriate `[ContentType]` attributes:
```csharp
[Export(typeof(IViewTaggerProvider))]
[ContentType("CSharp")]
[ContentType("Basic")]
[TagType(typeof(IntraTextAdornmentTag))]
[TextViewRole(PredefinedTextViewRoles.Document)]
internal sealed class YourTaggerProvider : IViewTaggerProvider
{
[Import]
internal IOutliningManagerService OutliningManagerService { get; set; }
public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag
{
if (textView == null || !(textView is IWpfTextView wpfTextView))
return null;
if (textView.TextBuffer != buffer)
return null;
return wpfTextView.Properties.GetOrCreateSingletonProperty(
() => new YourTagger(wpfTextView)) as ITagger<T>;
}
}
```
### QuickInfo Sources
```csharp
[Export(typeof(IAsyncQuickInfoSourceProvider))]
[Name("YourQuickInfo")]
[ContentType("code")]
[Order(Before = "Default Quick Info Presenter")]
internal sealed class YourQuickInfoSourceProvider : IAsyncQuickInfoSourceProvider
{
public IAsyncQuickInfoSource TryCreateQuickInfoSource(ITextBuffer textBuffer)
{
return textBuffer.Properties.GetOrCreateSingletonProperty(
() => new YourQuickInfoSource(textBuffer));
}
}
```
### Suggested Actions (Light Bulb)
```csharp
[Export(typeof(ISuggestedActionsSourceProvider))]
[Name("Your Suggested Actions")]
[ContentType("text")]
internal sealed class YourSuggestedActionsSourceProvider : ISuggestedActionsSourceProvider
{
public ISuggestedActionsSource CreateSuggestedActionsSource(ITextView textView, ITextBuffer textBuffer)
{
return new YourSuggestedActionsSource(textView, textBuffer);
}
}
```
## Threading Guidelines
### Always switch to UI thread for WPF operations
```csharp
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
// Now safe to create/modify WPF elements
```
### Background work
```csharp
ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
await VS.Commands.ExecuteAsync("View.TaskList");
});
```
## VSSDK & Threading Analyzer Rules
Extensions should enforce these analyzer rules. Add to `.editorconfig`:
```ini
dotnet_diagnostic.VSSDK*.severity = error
dotnet_diagnostic.VSTHRD*.severity = error
```
### Performance Rules
| ID | Rule | Fix |
|----|------|-----|
| **VSSDK001** | Derive from `AsyncPackage` | Use `ToolkitPackage` (derives from AsyncPackage) |
| **VSSDK002** | `AllowsBackgroundLoading = true` | Add to `[PackageRegistration]` |
### Threading Rules (VSTHRD)
| ID | Rule | Fix |
|----|------|-----|
| **VSTHRD001** | Avoid `.Wait()` | Use `await` |
| **VSTHRD002** | Avoid `JoinableTaskFactory.Run` | Use `RunAsync` or `await` |
| **VSTHRD010** | COM calls require UI thread | `await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync()` |
| **VSTHRD100** | No `async void` | Use `async Task` |
| **VSTHRD110** | Observe async results | `await task;` or suppress with pragma |
## Visual Studio Theming
**All UI must respect VS themes (Light, Dark, Blue, High Contrast)**
### WPF Theming with Environment Colors
```xml
<!-- MyControl.xaml -->
<UserControl x:Class="MyExt.MyControl"
xmlns:vsui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0">
<Grid Background="{DynamicResource {x:Static vsui:EnvironmentColors.ToolWindowBackgroundBrushKey}}">
<TextBlock Foreground="{DynamicResource {x:Static vsui:EnvironmentColors.ToolWindowTextBrushKey}}"
Text="Hello, themed world!" />
</Grid>
</UserControl>
```
### Toolkit Auto-Theming (Recommended)
The toolkit provides automatic theming for WPF UserControls:
```xml
<UserControl x:Class="MyExt.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:toolkit="clr-namespace:Community.VisualStudio.Toolkit;assembly=Community.VisualStudio.Toolkit"
toolkit:Themes.UseVsTheme="True">
<!-- Controls automatically get VS styling -->
</UserControl>
```
For dialog windows, use `DialogWindow`:
```xml
<platform:DialogWindow
x:Class="MyExt.MyDialog"
xmlns:platform="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0"
xmlns:toolkit="clr-namespace:Community.VisualStudio.Toolkit;assembly=Community.VisualStudio.Toolkit"
toolkit:Themes.UseVsTheme="True">
</platform:DialogWindow>
```
### Common Theme Color Tokens
| Category | Token | Usage |
|----------|-------|-------|
| **Background** | `EnvironmentColors.ToolWindowBackgroundBrushKey` | Window/panel background |
| **Foreground** | `EnvironmentColors.ToolWindowTextBrushKey` | Text |
| **Command Bar** | `EnvironmentColors.CommandBarTextActiveBrushKey` | Menu items |
| **Links** | `EnvironmentColors.ControlLinkTextBrushKey` | Hyperlinks |
### Theme-Aware Icons
Use `KnownMonikers` from the VS Image Catalog for theme-aware icons:
```csharp
public ImageMoniker IconMoniker => KnownMonikers.Settings;
```
In VSCT:
```xml
<Icon guid="ImageCatalogGuid" id="Settings"/>
<CommandFlag>IconIsMoniker</CommandFlag>
```
## Common VS SDK APIs
### VS Helper Methods (Community.VisualStudio.Toolkit)
```csharp
// Status bar
await VS.StatusBar.ShowMessageAsync("Message");
await VS.StatusBar.ShowProgressAsync("Working...", currentStep, totalSteps);
// Solution/Projects
Solution solution = await VS.Solutions.GetCurrentSolutionAsync();
IEnumerable<SolutionItem> items = await VS.Solutions.GetActiveItemsAsync();
bool isOpen = await VS.Solutions.IsOpenAsync();
// Documents
DocumentView docView = await VS.Documents.GetActiveDocumentViewAsync();
string text = docView?.TextBuffer?.CurrentSnapshot.GetText();
await VS.Documents.OpenAsync(fileName);
await VS.Documents.OpenInPreviewTabAsync(fileName);
// Commands
await VS.Commands.ExecuteAsync("View.TaskList");
// Settings
await VS.Settings.OpenAsync<OptionsProvider.GeneralOptions>();
// Messages
await VS.MessageBox.ShowAsync("Title", "Message");
await VS.MessageBox.ShowErrorAsync("Extension Name", ex.ToString());
// Events
VS.Events.SolutionEvents.OnAfterOpenProject += OnAfterOpenProject;
VS.Events.DocumentEvents.Saved += OnDocumentSaved;
```
### Working with Settings
```csharp
// Read settings synchronously
var value = General.Instance.MyOption;
// Read settings asynchronously
var general = await General.GetLiveInstanceAsync();
var value = general.MyOption;
// Write settings
General.Instance.MyOption = newValue;
General.Instance.Save();
// Or async
general.MyOption = newValue;
await general.SaveAsync();
// Listen for settings changes
General.Saved += OnSettingsSaved;
```
### Text Buffer Operations
```csharp
// Get snapshot
ITextSnapshot snapshot = textBuffer.CurrentSnapshot;
// Get line
ITextSnapshotLine line = snapshot.GetLineFromLineNumber(lineNumber);
string lineText = line.GetText();
// Create tracking span
ITrackingSpan trackingSpan = snapshot.CreateTrackingSpan(span, SpanTrackingMode.EdgeInclusive);
// Edit buffer
using (ITextEdit edit = textBuffer.CreateEdit())
{
edit.Replace(span, newText);
edit.Apply();
}
// Insert at caret position
DocumentView docView = await VS.Documents.GetActiveDocumentViewAsync();
if (docView?.TextView != null)
{
SnapshotPoint position = docView.TextView.Caret.Position.BufferPosition;
docView.TextBuffer?.Insert(position, "text to insert");
}
```
## VSCT Command Table
### Menu/Command Structure
```xml
<Commands package="YourPackage">
<Menus>
<Menu guid="YourPackage" id="SubMenu" type="Menu">
<Parent guid="YourPackage" id="MenuGroup"/>
<Strings>
<ButtonText>Menu Name</ButtonText>
<CommandName>Menu Name</CommandName>
<CanonicalName>.YourExtension.MenuName</CanonicalName>
</Strings>
</Menu>
</Menus>
<Groups>
<Group guid="YourPackage" id="MenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_CODEWIN"/>
</Group>
</Groups>
<Buttons>
<Button guid="YourPackage" id="CommandId" type="Button">
<Parent guid="YourPackage" id="MenuGroup"/>
<Icon guid="ImageCatalogGuid" id="Settings"/>
<CommandFlag>IconIsMoniker</CommandFlag>
<CommandFlag>DynamicVisibility</CommandFlag>
<Strings>
<ButtonText>Command Name</ButtonText>
<CanonicalName>.YourExtension.CommandName</CanonicalName>
</Strings>
</Button>
</Buttons>
</Commands>
<Symbols>
<GuidSymbol name="YourPackage" value="{guid-here}">
<IDSymbol name="MenuGroup" value="0x0001"/>
<IDSymbol name="CommandId" value="0x0100"/>
</GuidSymbol>
</Symbols>
```
## Best Practices
### 1. Performance
- Check file/buffer size before processing large documents
- Use `NormalizedSnapshotSpanCollection` for efficient span operations
- Cache parsed results when possible
- Use `ConfigureAwait(false)` in library code
```csharp
// Skip large files
if (buffer.CurrentSnapshot.Length > 150000)
return null;
```
### 2. Error Handling
- Wrap external operations in try-catch
- Log errors appropriately
- Never let exceptions crash VS
```csharp
try
{
// Operation
}
catch (Exception ex)
{
await ex.LogAsync();
}
```
### 3. Disposable Resources
- Implement `IDisposable` on taggers and other long-lived objects
- Unsubscribe from events in Dispose
```csharp
public void Dispose()
{
if (!_isDisposed)
{
_buffer.Changed -= OnBufferChanged;
_isDisposed = true;
}
}
```
### 4. Content Types
Common content types for `[ContentType]` attribute:
- `"text"` - All text files
- `"code"` - All code files
- `"CSharp"` - C# files
- `"Basic"` - VB.NET files
- `"CSS"`, `"LESS"`, `"SCSS"` - Style files
- `"TypeScript"`, `"JavaScript"` - Script files
- `"HTML"`, `"HTMLX"` - HTML files
- `"XML"` - XML files
- `"JSON"` - JSON files
### 5. Images and Icons
Use `KnownMonikers` from the VS Image Catalog:
```csharp
public ImageMoniker IconMoniker => KnownMonikers.Settings;
```
In VSCT:
```xml
<Icon guid="ImageCatalogGuid" id="Settings"/>
<CommandFlag>IconIsMoniker</CommandFlag>
```
## Testing
- Use `[VsTestMethod]` for tests requiring VS context
- Mock VS services when possible
- Test business logic separately from VS integration
## Common Pitfalls
| Pitfall | Solution |
|---------|----------|
| Blocking UI thread | Always use `async`/`await` |
| Creating WPF on background thread | Call `SwitchToMainThreadAsync()` first |
| Ignoring cancellation tokens | Pass them through async chains |
| VSCommandTable.cs mismatch | Regenerate after VSCT changes |
| Hardcoded GUIDs | Use `PackageGuids` and `PackageIds` constants |
| Swallowing exceptions | Log with `await ex.LogAsync()` |
| Missing DynamicVisibility | Required for `BeforeQueryStatus` to work |
| Using `.Result`, `.Wait()` | Causes deadlocks; always `await` |
| Hardcoded colors | Use VS theme colors (`EnvironmentColors`) |
| `async void` methods | Use `async Task` instead |
## Validation
Build and verify the extension:
```bash
msbuild /t:rebuild
```
Ensure analyzers are enabled in `.editorconfig`:
```ini
dotnet_diagnostic.VSSDK*.severity = error
dotnet_diagnostic.VSTHRD*.severity = error
```
Test in VS Experimental Instance before release.
## NuGet Packages
| Package | Purpose |
|---------|---------|
| `Community.VisualStudio.Toolkit.17` | Simplifies VS extension development |
| `Microsoft.VisualStudio.SDK` | Core VS SDK |
| `Microsoft.VSSDK.BuildTools` | Build tools for VSIX |
| `Microsoft.VisualStudio.Threading.Analyzers` | Threading analyzers |
| `Microsoft.VisualStudio.SDK.Analyzers` | VSSDK analyzers |
## Resources
- [Community.VisualStudio.Toolkit](https://github.com/VsixCommunity/Community.VisualStudio.Toolkit)
- [VS Extensibility Docs](https://learn.microsoft.com/en-us/visualstudio/extensibility/)
- [VSIX Community Samples](https://github.com/VsixCommunity/Samples)
## README and Marketplace Presentation
A good README works on both GitHub and the VS Marketplace. The Marketplace uses the README.md as the extension's description page.
### README Structure
```markdown
[marketplace]: https://marketplace.visualstudio.com/items?itemName=Publisher.ExtensionName
[repo]: https://github.com/user/repo
# Extension Name
[![Build](https://github.com/user/repo/actions/workflows/build.yaml/badge.svg)](...)
[![Visual Studio Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/Publisher.ExtensionName)][marketplace]
[![Visual Studio Marketplace Downloads](https://img.shields.io/visual-studio-marketplace/d/Publisher.ExtensionName)][marketplace]
Download this extension from the [Visual Studio Marketplace][marketplace]
or get the [CI build](http://vsixgallery.com/extension/ExtensionId/).
--------------------------------------
**Hook line that sells the extension in one sentence.**
![Screenshot](art/screenshot.png)
## Features
### Feature 1
Description with screenshot...
## How to Use
...
## License
[Apache 2.0](LICENSE)
```
### README Best Practices
| Element | Guideline |
|---------|-----------|
| **Title** | Use the same name as `DisplayName` in vsixmanifest |
| **Hook line** | Bold, one-sentence value proposition immediately after badges |
| **Screenshots** | Place in `/art` folder, use relative paths (`art/image.png`) |
| **Image sizes** | Keep under 1MB, 800-1200px wide for clarity |
| **Badges** | Version, downloads, rating, build status |
| **Feature sections** | Use H3 (`###`) with screenshots for each major feature |
| **Keyboard shortcuts** | Format as **Ctrl+M, Ctrl+C** (bold) |
| **Tables** | Great for comparing options or listing features |
| **Links** | Use reference-style links at top for cleaner markdown |
### VSIX Manifest (source.extension.vsixmanifest)
```xml
<Metadata>
<Identity Id="ExtensionName.guid-here" Version="1.0.0" Language="en-US" Publisher="Your Name" />
<DisplayName>Extension Name</DisplayName>
<Description xml:space="preserve">Short, compelling description under 200 chars. This appears in search results and the extension tile.</Description>
<MoreInfo>https://github.com/user/repo</MoreInfo>
<License>Resources\LICENSE.txt</License>
<Icon>Resources\Icon.png</Icon>
<PreviewImage>Resources\Preview.png</PreviewImage>
<Tags>keyword1, keyword2, keyword3</Tags>
</Metadata>
```
### Manifest Best Practices
| Element | Guideline |
|---------|-----------|
| **DisplayName** | 3-5 words, no "for Visual Studio" (implied) |
| **Description** | Under 200 chars, focus on value not features. Appears in search tiles |
| **Tags** | 5-10 relevant keywords, comma-separated, helps discoverability |
| **Icon** | 128x128 or 256x256 PNG, simple design visible at small sizes |
| **PreviewImage** | 200x200 PNG, can be same as Icon or a feature screenshot |
| **MoreInfo** | Link to GitHub repo for documentation and issues |
### Writing Tips
1. **Lead with benefits, not features** - "Stop wrestling with XML comments" beats "XML comment formatter"
2. **Show, don't tell** - Screenshots are more convincing than descriptions
3. **Use consistent terminology** - Match terms between README, manifest, and UI
4. **Keep the description scannable** - Short paragraphs, bullet points, tables
5. **Include keyboard shortcuts** - Users love productivity tips
6. **Add a "Why" section** - Explain the problem before the solution