mirror of
https://github.com/github/awesome-copilot.git
synced 2026-03-13 04:35:12 +00:00
Merge branch 'main' into add-gem-team
This commit is contained in:
42
agents/reepl-linkedin.agent.md
Normal file
42
agents/reepl-linkedin.agent.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: reepl-linkedin
|
||||
description: "AI-powered LinkedIn content creation, scheduling, and analytics agent. Create posts, carousels, and manage your LinkedIn presence with GitHub Copilot."
|
||||
---
|
||||
|
||||
# Reepl -- LinkedIn Content Agent
|
||||
|
||||
You are a LinkedIn content strategist and automation expert powered by [Reepl](https://reepl.io). You help developers, marketers, and professionals create, schedule, and analyze LinkedIn content directly from their editor.
|
||||
|
||||
**What is Reepl?** Reepl is an AI-powered LinkedIn content management platform that lets you create posts, design carousels, schedule content, and track analytics. Learn more at [reepl.io](https://reepl.io) or explore the skills repository at [github.com/reepl-io/skills](https://github.com/reepl-io/skills).
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
- **Post Creation:** Draft engaging LinkedIn posts with AI assistance, including text formatting, hashtag suggestions, and hook optimization.
|
||||
- **Carousel Design:** Generate multi-slide LinkedIn carousels with structured content and visual layouts.
|
||||
- **Content Scheduling:** Plan and schedule posts for optimal engagement times.
|
||||
- **Analytics:** Review post performance, engagement metrics, and audience insights.
|
||||
- **Voice Profiles:** Match content tone and style to a user's personal brand or voice profile.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Understand the Goal:** Ask what the user wants to achieve -- thought leadership, product launch, hiring, community engagement, etc.
|
||||
2. **Draft Content:** Create LinkedIn-optimized content following best practices (hooks, formatting, CTAs).
|
||||
3. **Refine:** Iterate on tone, length, and structure based on feedback.
|
||||
4. **Schedule or Publish:** Help the user schedule or publish the content through Reepl.
|
||||
|
||||
## LinkedIn Content Best Practices
|
||||
|
||||
- Start with a strong hook in the first two lines to earn the "see more" click.
|
||||
- Use short paragraphs and line breaks for readability on mobile.
|
||||
- Include a clear call-to-action (comment, share, visit link).
|
||||
- Keep hashtags relevant and limited to 3-5 per post.
|
||||
- Carousels should tell a story with a clear beginning, middle, and end.
|
||||
- Optimal post length is 1,200-1,500 characters for engagement.
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Always tailor content to the user's industry and audience.
|
||||
- Maintain a professional but authentic tone unless the user specifies otherwise.
|
||||
- Respect LinkedIn's content policies and community guidelines.
|
||||
- Never generate misleading, spammy, or engagement-bait content.
|
||||
- Prioritize value-driven content that educates, inspires, or informs.
|
||||
@@ -61,3 +61,11 @@ cookbooks:
|
||||
- sessions
|
||||
- persistence
|
||||
- state-management
|
||||
- id: accessibility-report
|
||||
name: Accessibility Report
|
||||
description: Generate WCAG accessibility reports using the Playwright MCP server
|
||||
tags:
|
||||
- accessibility
|
||||
- playwright
|
||||
- mcp
|
||||
- wcag
|
||||
|
||||
@@ -6,35 +6,43 @@ This cookbook collects small, focused recipes showing how to accomplish common t
|
||||
|
||||
### .NET (C#)
|
||||
|
||||
- [Ralph Loop](dotnet/ralph-loop.md): Build autonomous AI coding loops with fresh context per iteration, planning/building modes, and backpressure.
|
||||
- [Error Handling](dotnet/error-handling.md): Handle errors gracefully including connection failures, timeouts, and cleanup.
|
||||
- [Multiple Sessions](dotnet/multiple-sessions.md): Manage multiple independent conversations simultaneously.
|
||||
- [Managing Local Files](dotnet/managing-local-files.md): Organize files by metadata using AI-powered grouping strategies.
|
||||
- [PR Visualization](dotnet/pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server.
|
||||
- [Persisting Sessions](dotnet/persisting-sessions.md): Save and resume sessions across restarts.
|
||||
- [Accessibility Report](dotnet/accessibility-report.md): Generate WCAG accessibility reports using the Playwright MCP server.
|
||||
|
||||
### Node.js / TypeScript
|
||||
|
||||
- [Ralph Loop](nodejs/ralph-loop.md): Build autonomous AI coding loops with fresh context per iteration, planning/building modes, and backpressure.
|
||||
- [Error Handling](nodejs/error-handling.md): Handle errors gracefully including connection failures, timeouts, and cleanup.
|
||||
- [Multiple Sessions](nodejs/multiple-sessions.md): Manage multiple independent conversations simultaneously.
|
||||
- [Managing Local Files](nodejs/managing-local-files.md): Organize files by metadata using AI-powered grouping strategies.
|
||||
- [PR Visualization](nodejs/pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server.
|
||||
- [Persisting Sessions](nodejs/persisting-sessions.md): Save and resume sessions across restarts.
|
||||
- [Accessibility Report](nodejs/accessibility-report.md): Generate WCAG accessibility reports using the Playwright MCP server.
|
||||
|
||||
### Python
|
||||
|
||||
- [Ralph Loop](python/ralph-loop.md): Build autonomous AI coding loops with fresh context per iteration, planning/building modes, and backpressure.
|
||||
- [Error Handling](python/error-handling.md): Handle errors gracefully including connection failures, timeouts, and cleanup.
|
||||
- [Multiple Sessions](python/multiple-sessions.md): Manage multiple independent conversations simultaneously.
|
||||
- [Managing Local Files](python/managing-local-files.md): Organize files by metadata using AI-powered grouping strategies.
|
||||
- [PR Visualization](python/pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server.
|
||||
- [Persisting Sessions](python/persisting-sessions.md): Save and resume sessions across restarts.
|
||||
- [Accessibility Report](python/accessibility-report.md): Generate WCAG accessibility reports using the Playwright MCP server.
|
||||
|
||||
### Go
|
||||
|
||||
- [Ralph Loop](go/ralph-loop.md): Build autonomous AI coding loops with fresh context per iteration, planning/building modes, and backpressure.
|
||||
- [Error Handling](go/error-handling.md): Handle errors gracefully including connection failures, timeouts, and cleanup.
|
||||
- [Multiple Sessions](go/multiple-sessions.md): Manage multiple independent conversations simultaneously.
|
||||
- [Managing Local Files](go/managing-local-files.md): Organize files by metadata using AI-powered grouping strategies.
|
||||
- [PR Visualization](go/pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server.
|
||||
- [Persisting Sessions](go/persisting-sessions.md): Save and resume sessions across restarts.
|
||||
- [Accessibility Report](go/accessibility-report.md): Generate WCAG accessibility reports using the Playwright MCP server.
|
||||
|
||||
## How to Use
|
||||
|
||||
@@ -83,4 +91,4 @@ go run <filename>.go
|
||||
|
||||
## Status
|
||||
|
||||
Cookbook structure is complete with 4 recipes across all 4 supported languages. Each recipe includes both markdown documentation and runnable examples.
|
||||
Cookbook structure is complete with 7 recipes across all 4 supported languages. Each recipe includes both markdown documentation and runnable examples.
|
||||
|
||||
287
cookbook/copilot-sdk/dotnet/accessibility-report.md
Normal file
287
cookbook/copilot-sdk/dotnet/accessibility-report.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# Generating Accessibility Reports
|
||||
|
||||
Build a CLI tool that analyzes web page accessibility using the Playwright MCP server and generates detailed WCAG-compliant reports with optional test generation.
|
||||
|
||||
> **Runnable example:** [recipe/accessibility-report.cs](recipe/accessibility-report.cs)
|
||||
>
|
||||
> ```bash
|
||||
> dotnet run recipe/accessibility-report.cs
|
||||
> ```
|
||||
|
||||
## Example scenario
|
||||
|
||||
You want to audit a website's accessibility compliance. This tool navigates to a URL using Playwright, captures an accessibility snapshot, and produces a structured report covering WCAG criteria like landmarks, heading hierarchy, focus management, and touch targets. It can also generate Playwright test files to automate future accessibility checks.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
dotnet add package GitHub.Copilot.SDK
|
||||
```
|
||||
|
||||
You also need `npx` available (Node.js installed) for the Playwright MCP server.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
dotnet run recipe/accessibility-report.cs
|
||||
# Enter a URL when prompted
|
||||
```
|
||||
|
||||
## Full example: accessibility-report.cs
|
||||
|
||||
```csharp
|
||||
#:package GitHub.Copilot.SDK@*
|
||||
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
// Create and start client
|
||||
await using var client = new CopilotClient();
|
||||
await client.StartAsync();
|
||||
|
||||
Console.WriteLine("=== Accessibility Report Generator ===");
|
||||
Console.WriteLine();
|
||||
|
||||
Console.Write("Enter URL to analyze: ");
|
||||
var url = Console.ReadLine()?.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
Console.WriteLine("No URL provided. Exiting.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure URL has a scheme
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||
{
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
Console.WriteLine($"\nAnalyzing: {url}");
|
||||
Console.WriteLine("Please wait...\n");
|
||||
|
||||
// Create a session with Playwright MCP server
|
||||
await using var session = await client.CreateSessionAsync(new SessionConfig
|
||||
{
|
||||
Model = "claude-opus-4.6",
|
||||
Streaming = true,
|
||||
McpServers = new Dictionary<string, object>()
|
||||
{
|
||||
["playwright"] =
|
||||
new McpLocalServerConfig
|
||||
{
|
||||
Type = "local",
|
||||
Command = "npx",
|
||||
Args = ["@playwright/mcp@latest"],
|
||||
Tools = ["*"]
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for response using session.idle event
|
||||
var done = new TaskCompletionSource();
|
||||
|
||||
session.On(evt =>
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case AssistantMessageDeltaEvent delta:
|
||||
Console.Write(delta.Data.DeltaContent);
|
||||
break;
|
||||
case SessionIdleEvent:
|
||||
done.TrySetResult();
|
||||
break;
|
||||
case SessionErrorEvent error:
|
||||
Console.WriteLine($"\nError: {error.Data.Message}");
|
||||
done.TrySetResult();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
var prompt = $"""
|
||||
Use the Playwright MCP server to analyze the accessibility of this webpage: {url}
|
||||
|
||||
Please:
|
||||
1. Navigate to the URL using playwright-browser_navigate
|
||||
2. Take an accessibility snapshot using playwright-browser_snapshot
|
||||
3. Analyze the snapshot and provide a detailed accessibility report
|
||||
|
||||
Format the report EXACTLY like this structure with emoji indicators:
|
||||
|
||||
📊 Accessibility Report: [Page Title] (domain.com)
|
||||
|
||||
✅ What's Working Well
|
||||
| Category | Status | Details |
|
||||
|----------|--------|---------|
|
||||
| Language | ✅ Pass | lang="en-US" properly set |
|
||||
| Page Title | ✅ Pass | "[Title]" is descriptive |
|
||||
| Heading Hierarchy | ✅ Pass | Single H1, proper H2/H3 structure |
|
||||
| Images | ✅ Pass | All X images have alt text |
|
||||
|
||||
⚠️ Issues Found
|
||||
| Severity | Issue | WCAG Criterion | Recommendation |
|
||||
|----------|-------|----------------|----------------|
|
||||
| 🔴 High | No <main> landmark | 1.3.1, 2.4.1 | Wrap main content in <main> element |
|
||||
| 🟡 Medium | Focus outlines disabled | 2.4.7 | Ensure visible :focus styles exist |
|
||||
|
||||
📋 Stats Summary
|
||||
- Total Links: X
|
||||
- Total Headings: X
|
||||
- Focusable Elements: X
|
||||
- Landmarks Found: banner ✅, navigation ✅, main ❌, footer ✅
|
||||
|
||||
⚙️ Priority Recommendations
|
||||
...
|
||||
|
||||
Use ✅ for pass, 🔴 for high severity issues, 🟡 for medium severity, ❌ for missing items.
|
||||
Include actual findings from the page analysis - don't just copy the example.
|
||||
""";
|
||||
|
||||
await session.SendAsync(new MessageOptions { Prompt = prompt });
|
||||
await done.Task;
|
||||
|
||||
Console.WriteLine("\n\n=== Report Complete ===\n");
|
||||
|
||||
// Prompt user for test generation
|
||||
Console.Write("Would you like to generate Playwright accessibility tests? (y/n): ");
|
||||
var generateTests = Console.ReadLine()?.Trim().ToLowerInvariant();
|
||||
|
||||
if (generateTests == "y" || generateTests == "yes")
|
||||
{
|
||||
// Reset for next interaction
|
||||
done = new TaskCompletionSource();
|
||||
|
||||
var detectLanguagePrompt = $"""
|
||||
Analyze the current working directory to detect the primary programming language used in this project.
|
||||
Respond with ONLY the detected language name and a brief explanation.
|
||||
If no project is detected, suggest "TypeScript" as the default for Playwright tests.
|
||||
""";
|
||||
|
||||
Console.WriteLine("\nDetecting project language...\n");
|
||||
await session.SendAsync(new MessageOptions { Prompt = detectLanguagePrompt });
|
||||
await done.Task;
|
||||
|
||||
Console.Write("\n\nConfirm language for tests (or enter a different one): ");
|
||||
var language = Console.ReadLine()?.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(language))
|
||||
{
|
||||
language = "TypeScript";
|
||||
}
|
||||
|
||||
// Reset for test generation
|
||||
done = new TaskCompletionSource();
|
||||
|
||||
var testGenerationPrompt = $"""
|
||||
Based on the accessibility report you just generated for {url}, create Playwright accessibility tests in {language}.
|
||||
|
||||
The tests should:
|
||||
1. Verify all the accessibility checks from the report
|
||||
2. Test for the issues that were found (to ensure they get fixed)
|
||||
3. Include tests for landmarks, heading hierarchy, alt text, focus indicators, and more
|
||||
4. Use Playwright's accessibility testing features
|
||||
5. Include helpful comments explaining each test
|
||||
|
||||
Output the complete test file that can be saved and run.
|
||||
""";
|
||||
|
||||
Console.WriteLine("\nGenerating accessibility tests...\n");
|
||||
await session.SendAsync(new MessageOptions { Prompt = testGenerationPrompt });
|
||||
await done.Task;
|
||||
|
||||
Console.WriteLine("\n\n=== Tests Generated ===");
|
||||
}
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
1. **Playwright MCP server**: Configures a local MCP server running `@playwright/mcp` to provide browser automation tools
|
||||
2. **Streaming output**: Uses `Streaming = true` and `AssistantMessageDeltaEvent` for real-time token-by-token output
|
||||
3. **Accessibility snapshot**: Playwright's `browser_snapshot` tool captures the full accessibility tree of the page
|
||||
4. **Structured report**: The prompt engineers a consistent WCAG-aligned report format with emoji severity indicators
|
||||
5. **Test generation**: Optionally detects the project language and generates Playwright accessibility tests
|
||||
|
||||
## Key concepts
|
||||
|
||||
### MCP server configuration
|
||||
|
||||
The recipe configures a local MCP server that runs alongside the session:
|
||||
|
||||
```csharp
|
||||
McpServers = new Dictionary<string, object>()
|
||||
{
|
||||
["playwright"] = new McpLocalServerConfig
|
||||
{
|
||||
Type = "local",
|
||||
Command = "npx",
|
||||
Args = ["@playwright/mcp@latest"],
|
||||
Tools = ["*"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This gives the model access to Playwright browser tools like `browser_navigate`, `browser_snapshot`, and `browser_click`.
|
||||
|
||||
### Streaming with events
|
||||
|
||||
Unlike `SendAndWaitAsync`, this recipe uses streaming for real-time output:
|
||||
|
||||
```csharp
|
||||
session.On(evt =>
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case AssistantMessageDeltaEvent delta:
|
||||
Console.Write(delta.Data.DeltaContent); // Token-by-token
|
||||
break;
|
||||
case SessionIdleEvent:
|
||||
done.TrySetResult(); // Model finished
|
||||
break;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Sample interaction
|
||||
|
||||
```
|
||||
=== Accessibility Report Generator ===
|
||||
|
||||
Enter URL to analyze: github.com
|
||||
|
||||
Analyzing: https://github.com
|
||||
Please wait...
|
||||
|
||||
📊 Accessibility Report: GitHub (github.com)
|
||||
|
||||
✅ What's Working Well
|
||||
| Category | Status | Details |
|
||||
|----------|--------|---------|
|
||||
| Language | ✅ Pass | lang="en" properly set |
|
||||
| Page Title | ✅ Pass | "GitHub" is recognizable |
|
||||
| Heading Hierarchy | ✅ Pass | Proper H1/H2 structure |
|
||||
| Images | ✅ Pass | All images have alt text |
|
||||
|
||||
⚠️ Issues Found
|
||||
| Severity | Issue | WCAG Criterion | Recommendation |
|
||||
|----------|-------|----------------|----------------|
|
||||
| 🟡 Medium | Some links lack descriptive text | 2.4.4 | Add aria-label to icon-only links |
|
||||
|
||||
📋 Stats Summary
|
||||
- Total Links: 47
|
||||
- Total Headings: 8 (1× H1, proper hierarchy)
|
||||
- Focusable Elements: 52
|
||||
- Landmarks Found: banner ✅, navigation ✅, main ✅, footer ✅
|
||||
|
||||
=== Report Complete ===
|
||||
|
||||
Would you like to generate Playwright accessibility tests? (y/n): y
|
||||
|
||||
Detecting project language...
|
||||
TypeScript detected (package.json found)
|
||||
|
||||
Confirm language for tests (or enter a different one):
|
||||
|
||||
Generating accessibility tests...
|
||||
[Generated test file output...]
|
||||
|
||||
=== Tests Generated ===
|
||||
```
|
||||
260
cookbook/copilot-sdk/dotnet/ralph-loop.md
Normal file
260
cookbook/copilot-sdk/dotnet/ralph-loop.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# Ralph Loop: Autonomous AI Task Loops
|
||||
|
||||
Build autonomous coding loops where an AI agent picks tasks, implements them, validates against backpressure (tests, builds), commits, and repeats — each iteration in a fresh context window.
|
||||
|
||||
> **Runnable example:** [recipe/ralph-loop.cs](recipe/ralph-loop.cs)
|
||||
>
|
||||
> ```bash
|
||||
> cd dotnet
|
||||
> dotnet run recipe/ralph-loop.cs
|
||||
> ```
|
||||
|
||||
## What is a Ralph Loop?
|
||||
|
||||
A [Ralph loop](https://ghuntley.com/ralph/) is an autonomous development workflow where an AI agent iterates through tasks in isolated context windows. The key insight: **state lives on disk, not in the model's context**. Each iteration starts fresh, reads the current state from files, does one task, writes results back to disk, and exits.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ loop.sh │
|
||||
│ while true: │
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ Fresh session (isolated context) │ │
|
||||
│ │ │ │
|
||||
│ │ 1. Read PROMPT.md + AGENTS.md │ │
|
||||
│ │ 2. Study specs/* and code │ │
|
||||
│ │ 3. Pick next task from plan │ │
|
||||
│ │ 4. Implement + run tests │ │
|
||||
│ │ 5. Update plan, commit, exit │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
│ ↻ next iteration (fresh context) │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Core principles:**
|
||||
|
||||
- **Fresh context per iteration**: Each loop creates a new session — no context accumulation, always in the "smart zone"
|
||||
- **Disk as shared state**: `IMPLEMENTATION_PLAN.md` persists between iterations and acts as the coordination mechanism
|
||||
- **Backpressure steers quality**: Tests, builds, and lints reject bad work — the agent must fix issues before committing
|
||||
- **Two modes**: PLANNING (gap analysis → generate plan) and BUILDING (implement from plan)
|
||||
|
||||
## Simple Version
|
||||
|
||||
The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | copilot ; done`:
|
||||
|
||||
```csharp
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
var client = new CopilotClient();
|
||||
await client.StartAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var prompt = await File.ReadAllTextAsync("PROMPT.md");
|
||||
var maxIterations = 50;
|
||||
|
||||
for (var i = 1; i <= maxIterations; i++)
|
||||
{
|
||||
Console.WriteLine($"\n=== Iteration {i}/{maxIterations} ===");
|
||||
|
||||
// Fresh session each iteration — context isolation is the point
|
||||
var session = await client.CreateSessionAsync(
|
||||
new SessionConfig { Model = "gpt-5.1-codex-mini" });
|
||||
try
|
||||
{
|
||||
var done = new TaskCompletionSource<string>();
|
||||
session.On(evt =>
|
||||
{
|
||||
if (evt is AssistantMessageEvent msg)
|
||||
done.TrySetResult(msg.Data.Content);
|
||||
});
|
||||
|
||||
await session.SendAsync(new MessageOptions { Prompt = prompt });
|
||||
await done.Task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await session.DisposeAsync();
|
||||
}
|
||||
|
||||
Console.WriteLine($"Iteration {i} complete.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await client.StopAsync();
|
||||
}
|
||||
```
|
||||
|
||||
This is all you need to get started. The prompt file tells the agent what to do; the agent reads project files, does work, commits, and exits. The loop restarts with a clean slate.
|
||||
|
||||
## Ideal Version
|
||||
|
||||
The full Ralph pattern with planning and building modes, matching the [Ralph Playbook](https://github.com/ClaytonFarr/ralph-playbook) architecture:
|
||||
|
||||
```csharp
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
// Parse args: dotnet run [plan] [max_iterations]
|
||||
var mode = args.Contains("plan") ? "plan" : "build";
|
||||
var maxArg = args.FirstOrDefault(a => int.TryParse(a, out _));
|
||||
var maxIterations = maxArg != null ? int.Parse(maxArg) : 50;
|
||||
var promptFile = mode == "plan" ? "PROMPT_plan.md" : "PROMPT_build.md";
|
||||
|
||||
var client = new CopilotClient();
|
||||
await client.StartAsync();
|
||||
|
||||
Console.WriteLine(new string('━', 40));
|
||||
Console.WriteLine($"Mode: {mode}");
|
||||
Console.WriteLine($"Prompt: {promptFile}");
|
||||
Console.WriteLine($"Max: {maxIterations} iterations");
|
||||
Console.WriteLine(new string('━', 40));
|
||||
|
||||
try
|
||||
{
|
||||
var prompt = await File.ReadAllTextAsync(promptFile);
|
||||
|
||||
for (var i = 1; i <= maxIterations; i++)
|
||||
{
|
||||
Console.WriteLine($"\n=== Iteration {i}/{maxIterations} ===");
|
||||
|
||||
// Fresh session — each task gets full context budget
|
||||
var session = await client.CreateSessionAsync(
|
||||
new SessionConfig
|
||||
{
|
||||
Model = "gpt-5.1-codex-mini",
|
||||
// Pin the agent to the project directory
|
||||
WorkingDirectory = Environment.CurrentDirectory,
|
||||
// Auto-approve tool calls for unattended operation
|
||||
OnPermissionRequest = (_, _) => Task.FromResult(
|
||||
new PermissionRequestResult { Kind = "approved" }),
|
||||
});
|
||||
try
|
||||
{
|
||||
var done = new TaskCompletionSource<string>();
|
||||
session.On(evt =>
|
||||
{
|
||||
// Log tool usage for visibility
|
||||
if (evt is ToolExecutionStartEvent toolStart)
|
||||
Console.WriteLine($" ⚙ {toolStart.Data.ToolName}");
|
||||
else if (evt is AssistantMessageEvent msg)
|
||||
done.TrySetResult(msg.Data.Content);
|
||||
});
|
||||
|
||||
await session.SendAsync(new MessageOptions { Prompt = prompt });
|
||||
await done.Task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await session.DisposeAsync();
|
||||
}
|
||||
|
||||
Console.WriteLine($"\nIteration {i} complete.");
|
||||
}
|
||||
|
||||
Console.WriteLine($"\nReached max iterations: {maxIterations}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await client.StopAsync();
|
||||
}
|
||||
```
|
||||
|
||||
### Required Project Files
|
||||
|
||||
The ideal version expects this file structure in your project:
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── PROMPT_plan.md # Planning mode instructions
|
||||
├── PROMPT_build.md # Building mode instructions
|
||||
├── AGENTS.md # Operational guide (build/test commands)
|
||||
├── IMPLEMENTATION_PLAN.md # Task list (generated by planning mode)
|
||||
├── specs/ # Requirement specs (one per topic)
|
||||
│ ├── auth.md
|
||||
│ └── data-pipeline.md
|
||||
└── src/ # Your source code
|
||||
```
|
||||
|
||||
### Example `PROMPT_plan.md`
|
||||
|
||||
```markdown
|
||||
0a. Study `specs/*` to learn the application specifications.
|
||||
0b. Study IMPLEMENTATION_PLAN.md (if present) to understand the plan so far.
|
||||
0c. Study `src/` to understand existing code and shared utilities.
|
||||
|
||||
1. Compare specs against code (gap analysis). Create or update
|
||||
IMPLEMENTATION_PLAN.md as a prioritized bullet-point list of tasks
|
||||
yet to be implemented. Do NOT implement anything.
|
||||
|
||||
IMPORTANT: Do NOT assume functionality is missing — search the
|
||||
codebase first to confirm. Prefer updating existing utilities over
|
||||
creating ad-hoc copies.
|
||||
```
|
||||
|
||||
### Example `PROMPT_build.md`
|
||||
|
||||
```markdown
|
||||
0a. Study `specs/*` to learn the application specifications.
|
||||
0b. Study IMPLEMENTATION_PLAN.md.
|
||||
0c. Study `src/` for reference.
|
||||
|
||||
1. Choose the most important item from IMPLEMENTATION_PLAN.md. Before
|
||||
making changes, search the codebase (don't assume not implemented).
|
||||
2. After implementing, run the tests. If functionality is missing, add it.
|
||||
3. When you discover issues, update IMPLEMENTATION_PLAN.md immediately.
|
||||
4. When tests pass, update IMPLEMENTATION_PLAN.md, then `git add -A`
|
||||
then `git commit` with a descriptive message.
|
||||
|
||||
5. When authoring documentation, capture the why.
|
||||
6. Implement completely. No placeholders or stubs.
|
||||
7. Keep IMPLEMENTATION_PLAN.md current — future iterations depend on it.
|
||||
```
|
||||
|
||||
### Example `AGENTS.md`
|
||||
|
||||
Keep this brief (~60 lines). It's loaded every iteration, so bloat wastes context.
|
||||
|
||||
```markdown
|
||||
## Build & Run
|
||||
|
||||
dotnet build
|
||||
|
||||
## Validation
|
||||
|
||||
- Tests: `dotnet test`
|
||||
- Build: `dotnet build --no-restore`
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Fresh context per iteration**: Never accumulate context across iterations — that's the whole point
|
||||
2. **Disk is your database**: `IMPLEMENTATION_PLAN.md` is shared state between isolated sessions
|
||||
3. **Backpressure is essential**: Tests, builds, lints in `AGENTS.md` — the agent must pass them before committing
|
||||
4. **Start with PLANNING mode**: Generate the plan first, then switch to BUILDING
|
||||
5. **Observe and tune**: Watch early iterations, add guardrails to prompts when the agent fails in specific ways
|
||||
6. **The plan is disposable**: If the agent goes off track, delete `IMPLEMENTATION_PLAN.md` and re-plan
|
||||
7. **Keep `AGENTS.md` brief**: It's loaded every iteration — operational info only, no progress notes
|
||||
8. **Use a sandbox**: The agent runs autonomously with full tool access — isolate it
|
||||
9. **Set `WorkingDirectory`**: Pin the session to your project root so tool operations resolve paths correctly
|
||||
10. **Auto-approve permissions**: Use `OnPermissionRequest` to allow tool calls without interrupting the loop
|
||||
|
||||
## When to Use a Ralph Loop
|
||||
|
||||
**Good for:**
|
||||
|
||||
- Implementing features from specs with test-driven validation
|
||||
- Large refactors broken into many small tasks
|
||||
- Unattended, long-running development with clear requirements
|
||||
- Any work where backpressure (tests/builds) can verify correctness
|
||||
|
||||
**Not good for:**
|
||||
|
||||
- Tasks requiring human judgment mid-loop
|
||||
- One-shot operations that don't benefit from iteration
|
||||
- Vague requirements without testable acceptance criteria
|
||||
- Exploratory prototyping where direction isn't clear
|
||||
|
||||
## See Also
|
||||
|
||||
- [Error Handling](error-handling.md) — timeout patterns and graceful shutdown for long-running sessions
|
||||
- [Persisting Sessions](persisting-sessions.md) — save and resume sessions across restarts
|
||||
184
cookbook/copilot-sdk/dotnet/recipe/accessibility-report.cs
Normal file
184
cookbook/copilot-sdk/dotnet/recipe/accessibility-report.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
#:package GitHub.Copilot.SDK@*
|
||||
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
// Create and start client
|
||||
await using var client = new CopilotClient();
|
||||
await client.StartAsync();
|
||||
|
||||
Console.WriteLine("=== Accessibility Report Generator ===");
|
||||
Console.WriteLine();
|
||||
|
||||
Console.Write("Enter URL to analyze: ");
|
||||
var url = Console.ReadLine()?.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
Console.WriteLine("No URL provided. Exiting.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure URL has a scheme
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||
{
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
Console.WriteLine($"\nAnalyzing: {url}");
|
||||
Console.WriteLine("Please wait...\n");
|
||||
|
||||
// Create a session with Playwright MCP server
|
||||
await using var session = await client.CreateSessionAsync(new SessionConfig
|
||||
{
|
||||
Model = "claude-opus-4.6",
|
||||
Streaming = true,
|
||||
McpServers = new Dictionary<string, object>()
|
||||
{
|
||||
["playwright"] =
|
||||
new McpLocalServerConfig
|
||||
{
|
||||
Type = "local",
|
||||
Command = "npx",
|
||||
Args = ["@playwright/mcp@latest"],
|
||||
Tools = ["*"]
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for response using session.idle event
|
||||
var done = new TaskCompletionSource();
|
||||
|
||||
session.On(evt =>
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case AssistantMessageDeltaEvent delta:
|
||||
Console.Write(delta.Data.DeltaContent);
|
||||
break;
|
||||
case SessionIdleEvent:
|
||||
done.TrySetResult();
|
||||
break;
|
||||
case SessionErrorEvent error:
|
||||
Console.WriteLine($"\nError: {error.Data.Message}");
|
||||
done.TrySetResult();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
var prompt = $"""
|
||||
Use the Playwright MCP server to analyze the accessibility of this webpage: {url}
|
||||
|
||||
Please:
|
||||
1. Navigate to the URL using playwright-browser_navigate
|
||||
2. Take an accessibility snapshot using playwright-browser_snapshot
|
||||
3. Analyze the snapshot and provide a detailed accessibility report
|
||||
|
||||
Format the report EXACTLY like this structure with emoji indicators:
|
||||
|
||||
📊 Accessibility Report: [Page Title] (domain.com)
|
||||
|
||||
✅ What's Working Well
|
||||
| Category | Status | Details |
|
||||
|----------|--------|---------|
|
||||
| Language | ✅ Pass | lang="en-US" properly set |
|
||||
| Page Title | ✅ Pass | "[Title]" is descriptive |
|
||||
| Heading Hierarchy | ✅ Pass | Single H1, proper H2/H3 structure |
|
||||
| Images | ✅ Pass | All X images have alt text |
|
||||
| Viewport | ✅ Pass | Allows pinch-zoom (no user-scalable=no) |
|
||||
| Links | ✅ Pass | No ambiguous "click here" links |
|
||||
| Reduced Motion | ✅ Pass | Supports prefers-reduced-motion |
|
||||
| Autoplay Media | ✅ Pass | No autoplay audio/video |
|
||||
| Font Selector | ✅ Excellent | Includes OpenDyslexic option for dyslexia |
|
||||
| Dark/Light Mode | ✅ Excellent | User-controlled theme toggle |
|
||||
|
||||
⚠️ Issues Found
|
||||
| Severity | Issue | WCAG Criterion | Recommendation |
|
||||
|----------|-------|----------------|----------------|
|
||||
| 🔴 High | No <main> landmark | 1.3.1, 2.4.1 | Wrap main content in <main> element |
|
||||
| 🔴 High | No skip navigation link | 2.4.1 | Add "Skip to content" link at top |
|
||||
| 🟡 Medium | Focus outlines disabled | 2.4.7 | Default outline is none - ensure visible :focus styles exist |
|
||||
| 🟡 Medium | Small touch targets | 2.5.8 | Navigation links are 37px tall (below 44px minimum) |
|
||||
|
||||
📋 Stats Summary
|
||||
- Total Links: X
|
||||
- Total Headings: X (1× H1, proper hierarchy)
|
||||
- Focusable Elements: X
|
||||
- Landmarks Found: banner ✅, navigation ✅, main ❌, footer ✅
|
||||
|
||||
⚙️ Priority Recommendations
|
||||
- Add <main> landmark - Wrap page content in <main role="main"> for screen reader navigation
|
||||
- Add skip link - Hidden link at start: <a href="#main-content" class="skip-link">Skip to content</a>
|
||||
- Increase touch targets - Add padding to nav links and tags to meet 44×44px minimum
|
||||
- Verify focus styles - Test keyboard navigation; add visible :focus or :focus-visible outlines
|
||||
|
||||
Use ✅ for pass, 🔴 for high severity issues, 🟡 for medium severity, ❌ for missing items.
|
||||
Include actual findings from the page analysis - don't just copy the example.
|
||||
""";
|
||||
|
||||
await session.SendAsync(new MessageOptions { Prompt = prompt });
|
||||
await done.Task;
|
||||
|
||||
Console.WriteLine("\n\n=== Report Complete ===\n");
|
||||
|
||||
// Prompt user for test generation
|
||||
Console.Write("Would you like to generate Playwright accessibility tests? (y/n): ");
|
||||
var generateTests = Console.ReadLine()?.Trim().ToLowerInvariant();
|
||||
|
||||
if (generateTests == "y" || generateTests == "yes")
|
||||
{
|
||||
// Reset for next interaction
|
||||
done = new TaskCompletionSource();
|
||||
|
||||
var detectLanguagePrompt = $"""
|
||||
Analyze the current working directory to detect the primary programming language used in this project.
|
||||
Look for project files like package.json, *.csproj, pom.xml, requirements.txt, go.mod, etc.
|
||||
|
||||
Respond with ONLY the detected language name (e.g., "TypeScript", "JavaScript", "C#", "Python", "Java")
|
||||
and a brief explanation of why you detected it.
|
||||
If no project is detected, suggest "TypeScript" as the default for Playwright tests.
|
||||
""";
|
||||
|
||||
Console.WriteLine("\nDetecting project language...\n");
|
||||
await session.SendAsync(new MessageOptions { Prompt = detectLanguagePrompt });
|
||||
await done.Task;
|
||||
|
||||
Console.Write("\n\nConfirm language for tests (or enter a different one): ");
|
||||
var language = Console.ReadLine()?.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(language))
|
||||
{
|
||||
language = "TypeScript";
|
||||
}
|
||||
|
||||
// Reset for test generation
|
||||
done = new TaskCompletionSource();
|
||||
|
||||
var testGenerationPrompt = $"""
|
||||
Based on the accessibility report you just generated for {url}, create Playwright accessibility tests in {language}.
|
||||
|
||||
The tests should:
|
||||
1. Verify all the accessibility checks from the report
|
||||
2. Test for the issues that were found (to ensure they get fixed)
|
||||
3. Include tests for:
|
||||
- Page has proper lang attribute
|
||||
- Page has descriptive title
|
||||
- Heading hierarchy is correct (single H1, proper nesting)
|
||||
- All images have alt text
|
||||
- No autoplay media
|
||||
- Landmark regions exist (banner, nav, main, footer)
|
||||
- Skip navigation link exists and works
|
||||
- Focus indicators are visible
|
||||
- Touch targets meet minimum size requirements
|
||||
4. Use Playwright's accessibility testing features
|
||||
5. Include helpful comments explaining each test
|
||||
|
||||
Output the complete test file that can be saved and run.
|
||||
Use the Playwright MCP server tools if you need to verify any page details.
|
||||
""";
|
||||
|
||||
Console.WriteLine("\nGenerating accessibility tests...\n");
|
||||
await session.SendAsync(new MessageOptions { Prompt = testGenerationPrompt });
|
||||
await done.Task;
|
||||
|
||||
Console.WriteLine("\n\n=== Tests Generated ===");
|
||||
}
|
||||
83
cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs
Normal file
83
cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
#:package GitHub.Copilot.SDK@*
|
||||
|
||||
using GitHub.Copilot.SDK;
|
||||
|
||||
// Ralph loop: autonomous AI task loop with fresh context per iteration.
|
||||
//
|
||||
// Two modes:
|
||||
// - "plan": reads PROMPT_plan.md, generates/updates IMPLEMENTATION_PLAN.md
|
||||
// - "build": reads PROMPT_build.md, implements tasks, runs tests, commits
|
||||
//
|
||||
// Each iteration creates a fresh session so the agent always operates in
|
||||
// the "smart zone" of its context window. State is shared between
|
||||
// iterations via files on disk (IMPLEMENTATION_PLAN.md, AGENTS.md, specs/*).
|
||||
//
|
||||
// Usage:
|
||||
// dotnet run # build mode, 50 iterations
|
||||
// dotnet run plan # planning mode
|
||||
// dotnet run 20 # build mode, 20 iterations
|
||||
// dotnet run plan 5 # planning mode, 5 iterations
|
||||
|
||||
var mode = args.Contains("plan") ? "plan" : "build";
|
||||
var maxArg = args.FirstOrDefault(a => int.TryParse(a, out _));
|
||||
var maxIterations = maxArg != null ? int.Parse(maxArg) : 50;
|
||||
var promptFile = mode == "plan" ? "PROMPT_plan.md" : "PROMPT_build.md";
|
||||
|
||||
var client = new CopilotClient();
|
||||
await client.StartAsync();
|
||||
|
||||
Console.WriteLine(new string('━', 40));
|
||||
Console.WriteLine($"Mode: {mode}");
|
||||
Console.WriteLine($"Prompt: {promptFile}");
|
||||
Console.WriteLine($"Max: {maxIterations} iterations");
|
||||
Console.WriteLine(new string('━', 40));
|
||||
|
||||
try
|
||||
{
|
||||
var prompt = await File.ReadAllTextAsync(promptFile);
|
||||
|
||||
for (var i = 1; i <= maxIterations; i++)
|
||||
{
|
||||
Console.WriteLine($"\n=== Iteration {i}/{maxIterations} ===");
|
||||
|
||||
// Fresh session — each task gets full context budget
|
||||
var session = await client.CreateSessionAsync(
|
||||
new SessionConfig
|
||||
{
|
||||
Model = "gpt-5.1-codex-mini",
|
||||
// Pin the agent to the project directory
|
||||
WorkingDirectory = Environment.CurrentDirectory,
|
||||
// Auto-approve tool calls for unattended operation
|
||||
OnPermissionRequest = (_, _) => Task.FromResult(
|
||||
new PermissionRequestResult { Kind = "approved" }),
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var done = new TaskCompletionSource<string>();
|
||||
session.On(evt =>
|
||||
{
|
||||
// Log tool usage for visibility
|
||||
if (evt is ToolExecutionStartEvent toolStart)
|
||||
Console.WriteLine($" ⚙ {toolStart.Data.ToolName}");
|
||||
else if (evt is AssistantMessageEvent msg)
|
||||
done.TrySetResult(msg.Data.Content);
|
||||
});
|
||||
|
||||
await session.SendAsync(new MessageOptions { Prompt = prompt });
|
||||
await done.Task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await session.DisposeAsync();
|
||||
}
|
||||
|
||||
Console.WriteLine($"\nIteration {i} complete.");
|
||||
}
|
||||
|
||||
Console.WriteLine($"\nReached max iterations: {maxIterations}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await client.StopAsync();
|
||||
}
|
||||
291
cookbook/copilot-sdk/go/accessibility-report.md
Normal file
291
cookbook/copilot-sdk/go/accessibility-report.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# Generating Accessibility Reports
|
||||
|
||||
Build a CLI tool that analyzes web page accessibility using the Playwright MCP server and generates detailed WCAG-compliant reports with optional test generation.
|
||||
|
||||
> **Runnable example:** [recipe/accessibility-report.go](recipe/accessibility-report.go)
|
||||
>
|
||||
> ```bash
|
||||
> go run recipe/accessibility-report.go
|
||||
> ```
|
||||
|
||||
## Example scenario
|
||||
|
||||
You want to audit a website's accessibility compliance. This tool navigates to a URL using Playwright, captures an accessibility snapshot, and produces a structured report covering WCAG criteria like landmarks, heading hierarchy, focus management, and touch targets. It can also generate Playwright test files to automate future accessibility checks.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
go get github.com/github/copilot-sdk/go
|
||||
```
|
||||
|
||||
You also need `npx` available (Node.js installed) for the Playwright MCP server.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
go run accessibility-report.go
|
||||
# Enter a URL when prompted
|
||||
```
|
||||
|
||||
## Full example: accessibility-report.go
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Println("=== Accessibility Report Generator ===")
|
||||
fmt.Println()
|
||||
|
||||
fmt.Print("Enter URL to analyze: ")
|
||||
url, _ := reader.ReadString('\n')
|
||||
url = strings.TrimSpace(url)
|
||||
|
||||
if url == "" {
|
||||
fmt.Println("No URL provided. Exiting.")
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure URL has a scheme
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
fmt.Printf("\nAnalyzing: %s\n", url)
|
||||
fmt.Println("Please wait...\n")
|
||||
|
||||
// Create Copilot client with Playwright MCP server
|
||||
client := copilot.NewClient(nil)
|
||||
|
||||
if err := client.Start(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
streaming := true
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
Model: "claude-opus-4.6",
|
||||
Streaming: &streaming,
|
||||
McpServers: map[string]interface{}{
|
||||
"playwright": map[string]interface{}{
|
||||
"type": "local",
|
||||
"command": "npx",
|
||||
"args": []string{"@playwright/mcp@latest"},
|
||||
"tools": []string{"*"},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer session.Destroy()
|
||||
|
||||
// Set up streaming event handling
|
||||
done := make(chan struct{}, 1)
|
||||
|
||||
session.On(func(event copilot.SessionEvent) {
|
||||
switch event.Type {
|
||||
case "assistant.message.delta":
|
||||
if event.Data.DeltaContent != nil {
|
||||
fmt.Print(*event.Data.DeltaContent)
|
||||
}
|
||||
case "session.idle":
|
||||
select {
|
||||
case done <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
case "session.error":
|
||||
if event.Data.Message != nil {
|
||||
fmt.Printf("\nError: %s\n", *event.Data.Message)
|
||||
}
|
||||
select {
|
||||
case done <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
prompt := fmt.Sprintf(`
|
||||
Use the Playwright MCP server to analyze the accessibility of this webpage: %s
|
||||
|
||||
Please:
|
||||
1. Navigate to the URL using playwright-browser_navigate
|
||||
2. Take an accessibility snapshot using playwright-browser_snapshot
|
||||
3. Analyze the snapshot and provide a detailed accessibility report
|
||||
|
||||
Format the report with emoji indicators:
|
||||
- 📊 Accessibility Report header
|
||||
- ✅ What's Working Well (table with Category, Status, Details)
|
||||
- ⚠️ Issues Found (table with Severity, Issue, WCAG Criterion, Recommendation)
|
||||
- 📋 Stats Summary (links, headings, focusable elements, landmarks)
|
||||
- ⚙️ Priority Recommendations
|
||||
|
||||
Use ✅ for pass, 🔴 for high severity issues, 🟡 for medium severity, ❌ for missing items.
|
||||
Include actual findings from the page analysis.
|
||||
`, url)
|
||||
|
||||
if _, err := session.Send(ctx, copilot.MessageOptions{Prompt: prompt}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
<-done
|
||||
|
||||
fmt.Println("\n\n=== Report Complete ===\n")
|
||||
|
||||
// Prompt user for test generation
|
||||
fmt.Print("Would you like to generate Playwright accessibility tests? (y/n): ")
|
||||
generateTests, _ := reader.ReadString('\n')
|
||||
generateTests = strings.TrimSpace(strings.ToLower(generateTests))
|
||||
|
||||
if generateTests == "y" || generateTests == "yes" {
|
||||
detectLanguagePrompt := `
|
||||
Analyze the current working directory to detect the primary programming language.
|
||||
Respond with ONLY the detected language name and a brief explanation.
|
||||
If no project is detected, suggest "TypeScript" as the default.
|
||||
`
|
||||
|
||||
fmt.Println("\nDetecting project language...\n")
|
||||
select {
|
||||
case <-done:
|
||||
default:
|
||||
}
|
||||
if _, err := session.Send(ctx, copilot.MessageOptions{Prompt: detectLanguagePrompt}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
<-done
|
||||
|
||||
fmt.Print("\n\nConfirm language for tests (or enter a different one): ")
|
||||
language, _ := reader.ReadString('\n')
|
||||
language = strings.TrimSpace(language)
|
||||
if language == "" {
|
||||
language = "TypeScript"
|
||||
}
|
||||
|
||||
testGenerationPrompt := fmt.Sprintf(`
|
||||
Based on the accessibility report you just generated for %s,
|
||||
create Playwright accessibility tests in %s.
|
||||
|
||||
Include tests for: lang attribute, title, heading hierarchy, alt text,
|
||||
landmarks, skip navigation, focus indicators, and touch targets.
|
||||
Use Playwright's accessibility testing features with helpful comments.
|
||||
Output the complete test file.
|
||||
`, url, language)
|
||||
|
||||
fmt.Println("\nGenerating accessibility tests...\n")
|
||||
select {
|
||||
case <-done:
|
||||
default:
|
||||
}
|
||||
if _, err := session.Send(ctx, copilot.MessageOptions{Prompt: testGenerationPrompt}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
<-done
|
||||
|
||||
fmt.Println("\n\n=== Tests Generated ===")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
1. **Playwright MCP server**: Configures a local MCP server running `@playwright/mcp` to provide browser automation tools
|
||||
2. **Streaming output**: Uses `Streaming: &streaming` and `assistant.message.delta` events for real-time token-by-token output
|
||||
3. **Accessibility snapshot**: Playwright's `browser_snapshot` tool captures the full accessibility tree of the page
|
||||
4. **Structured report**: The prompt engineers a consistent WCAG-aligned report format with emoji severity indicators
|
||||
5. **Test generation**: Optionally detects the project language and generates Playwright accessibility tests
|
||||
|
||||
## Key concepts
|
||||
|
||||
### MCP server configuration
|
||||
|
||||
The recipe configures a local MCP server that runs alongside the session:
|
||||
|
||||
```go
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
McpServers: map[string]interface{}{
|
||||
"playwright": map[string]interface{}{
|
||||
"type": "local",
|
||||
"command": "npx",
|
||||
"args": []string{"@playwright/mcp@latest"},
|
||||
"tools": []string{"*"},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
This gives the model access to Playwright browser tools like `browser_navigate`, `browser_snapshot`, and `browser_click`.
|
||||
|
||||
### Streaming with events
|
||||
|
||||
Unlike `SendAndWait`, this recipe uses streaming for real-time output:
|
||||
|
||||
```go
|
||||
session.On(func(event copilot.SessionEvent) {
|
||||
switch event.Type {
|
||||
case "assistant.message.delta":
|
||||
if event.Data.DeltaContent != nil {
|
||||
fmt.Print(*event.Data.DeltaContent)
|
||||
}
|
||||
case "session.idle":
|
||||
done <- struct{}{}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Sample interaction
|
||||
|
||||
```
|
||||
=== Accessibility Report Generator ===
|
||||
|
||||
Enter URL to analyze: github.com
|
||||
|
||||
Analyzing: https://github.com
|
||||
Please wait...
|
||||
|
||||
📊 Accessibility Report: GitHub (github.com)
|
||||
|
||||
✅ What's Working Well
|
||||
| Category | Status | Details |
|
||||
|----------|--------|---------|
|
||||
| Language | ✅ Pass | lang="en" properly set |
|
||||
| Page Title | ✅ Pass | "GitHub" is recognizable |
|
||||
| Heading Hierarchy | ✅ Pass | Proper H1/H2 structure |
|
||||
| Images | ✅ Pass | All images have alt text |
|
||||
|
||||
⚠️ Issues Found
|
||||
| Severity | Issue | WCAG Criterion | Recommendation |
|
||||
|----------|-------|----------------|----------------|
|
||||
| 🟡 Medium | Some links lack descriptive text | 2.4.4 | Add aria-label to icon-only links |
|
||||
|
||||
📋 Stats Summary
|
||||
- Total Links: 47
|
||||
- Total Headings: 8 (1× H1, proper hierarchy)
|
||||
- Focusable Elements: 52
|
||||
- Landmarks Found: banner ✅, navigation ✅, main ✅, footer ✅
|
||||
|
||||
=== Report Complete ===
|
||||
|
||||
Would you like to generate Playwright accessibility tests? (y/n): y
|
||||
|
||||
Detecting project language...
|
||||
TypeScript detected (package.json found)
|
||||
|
||||
Confirm language for tests (or enter a different one):
|
||||
|
||||
Generating accessibility tests...
|
||||
[Generated test file output...]
|
||||
|
||||
=== Tests Generated ===
|
||||
```
|
||||
@@ -18,24 +18,22 @@ You need to handle various error conditions like connection failures, timeouts,
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"github.com/github/copilot-sdk/go"
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := copilot.NewClient()
|
||||
ctx := context.Background()
|
||||
client := copilot.NewClient(nil)
|
||||
|
||||
if err := client.Start(); err != nil {
|
||||
if err := client.Start(ctx); err != nil {
|
||||
log.Fatalf("Failed to start client: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := client.Stop(); err != nil {
|
||||
log.Printf("Error stopping client: %v", err)
|
||||
}
|
||||
}()
|
||||
defer client.Stop()
|
||||
|
||||
session, err := client.CreateSession(copilot.SessionConfig{
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
Model: "gpt-5",
|
||||
})
|
||||
if err != nil {
|
||||
@@ -43,19 +41,15 @@ func main() {
|
||||
}
|
||||
defer session.Destroy()
|
||||
|
||||
responseChan := make(chan string, 1)
|
||||
session.On(func(event copilot.Event) {
|
||||
if msg, ok := event.(copilot.AssistantMessageEvent); ok {
|
||||
responseChan <- msg.Data.Content
|
||||
}
|
||||
})
|
||||
|
||||
if err := session.Send(copilot.MessageOptions{Prompt: "Hello!"}); err != nil {
|
||||
result, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"})
|
||||
if err != nil {
|
||||
log.Printf("Failed to send message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
response := <-responseChan
|
||||
fmt.Println(response)
|
||||
if result != nil && result.Data.Content != nil {
|
||||
fmt.Println(*result.Data.Content)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -63,14 +57,17 @@ func main() {
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func startClient() error {
|
||||
client := copilot.NewClient()
|
||||
func startClient(ctx context.Context) error {
|
||||
client := copilot.NewClient(nil)
|
||||
|
||||
if err := client.Start(); err != nil {
|
||||
if err := client.Start(ctx); err != nil {
|
||||
var execErr *exec.Error
|
||||
if errors.As(err, &execErr) {
|
||||
return fmt.Errorf("Copilot CLI not found. Please install it first: %w", err)
|
||||
@@ -90,48 +87,41 @@ func startClient() error {
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func sendWithTimeout(session *copilot.Session) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
responseChan := make(chan string, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
session.On(func(event copilot.Event) {
|
||||
if msg, ok := event.(copilot.AssistantMessageEvent); ok {
|
||||
responseChan <- msg.Data.Content
|
||||
result, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Complex question..."})
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return fmt.Errorf("request timed out")
|
||||
}
|
||||
})
|
||||
|
||||
if err := session.Send(copilot.MessageOptions{Prompt: "Complex question..."}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case response := <-responseChan:
|
||||
fmt.Println(response)
|
||||
return nil
|
||||
case err := <-errChan:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("request timed out")
|
||||
if result != nil && result.Data.Content != nil {
|
||||
fmt.Println(*result.Data.Content)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Aborting a request
|
||||
|
||||
```go
|
||||
func abortAfterDelay(session *copilot.Session) {
|
||||
// Start a request
|
||||
session.Send(copilot.MessageOptions{Prompt: "Write a very long story..."})
|
||||
func abortAfterDelay(ctx context.Context, session *copilot.Session) {
|
||||
// Start a request (non-blocking send)
|
||||
session.Send(ctx, copilot.MessageOptions{Prompt: "Write a very long story..."})
|
||||
|
||||
// Abort it after some condition
|
||||
time.AfterFunc(5*time.Second, func() {
|
||||
if err := session.Abort(); err != nil {
|
||||
if err := session.Abort(ctx); err != nil {
|
||||
log.Printf("Failed to abort: %v", err)
|
||||
}
|
||||
fmt.Println("Request aborted")
|
||||
@@ -143,13 +133,18 @@ func abortAfterDelay(session *copilot.Session) {
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := copilot.NewClient()
|
||||
ctx := context.Background()
|
||||
client := copilot.NewClient(nil)
|
||||
|
||||
// Set up signal handling
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
@@ -158,15 +153,11 @@ func main() {
|
||||
go func() {
|
||||
<-sigChan
|
||||
fmt.Println("\nShutting down...")
|
||||
|
||||
if err := client.Stop(); err != nil {
|
||||
log.Printf("Cleanup errors: %v", err)
|
||||
}
|
||||
|
||||
client.Stop()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
if err := client.Start(); err != nil {
|
||||
if err := client.Start(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -178,14 +169,15 @@ func main() {
|
||||
|
||||
```go
|
||||
func doWork() error {
|
||||
client := copilot.NewClient()
|
||||
ctx := context.Background()
|
||||
client := copilot.NewClient(nil)
|
||||
|
||||
if err := client.Start(); err != nil {
|
||||
if err := client.Start(ctx); err != nil {
|
||||
return fmt.Errorf("failed to start: %w", err)
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
session, err := client.CreateSession(copilot.SessionConfig{Model: "gpt-5"})
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create session: %w", err)
|
||||
}
|
||||
|
||||
@@ -18,23 +18,26 @@ You have a folder with many files and want to organize them into subfolders base
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"github.com/github/copilot-sdk/go"
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create and start client
|
||||
client := copilot.NewClient()
|
||||
if err := client.Start(); err != nil {
|
||||
client := copilot.NewClient(nil)
|
||||
if err := client.Start(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
// Create session
|
||||
session, err := client.CreateSession(copilot.SessionConfig{
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
Model: "gpt-5",
|
||||
})
|
||||
if err != nil {
|
||||
@@ -43,14 +46,20 @@ func main() {
|
||||
defer session.Destroy()
|
||||
|
||||
// Event handler
|
||||
session.On(func(event copilot.Event) {
|
||||
switch e := event.(type) {
|
||||
case copilot.AssistantMessageEvent:
|
||||
fmt.Printf("\nCopilot: %s\n", e.Data.Content)
|
||||
case copilot.ToolExecutionStartEvent:
|
||||
fmt.Printf(" → Running: %s\n", e.Data.ToolName)
|
||||
case copilot.ToolExecutionCompleteEvent:
|
||||
fmt.Printf(" ✓ Completed: %s\n", e.Data.ToolName)
|
||||
session.On(func(event copilot.SessionEvent) {
|
||||
switch event.Type {
|
||||
case "assistant.message":
|
||||
if event.Data.Content != nil {
|
||||
fmt.Printf("\nCopilot: %s\n", *event.Data.Content)
|
||||
}
|
||||
case "tool.execution_start":
|
||||
if event.Data.ToolName != nil {
|
||||
fmt.Printf(" → Running: %s\n", *event.Data.ToolName)
|
||||
}
|
||||
case "tool.execution_complete":
|
||||
if event.Data.ToolName != nil {
|
||||
fmt.Printf(" ✓ Completed: %s\n", *event.Data.ToolName)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -69,11 +78,10 @@ Analyze the files in "%s" and organize them into subfolders.
|
||||
Please confirm before moving any files.
|
||||
`, targetFolder)
|
||||
|
||||
if err := session.Send(copilot.MessageOptions{Prompt: prompt}); err != nil {
|
||||
_, err = session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
session.WaitForIdle()
|
||||
}
|
||||
```
|
||||
|
||||
@@ -116,7 +124,7 @@ Analyze files in "%s" and show me how you would organize them
|
||||
by file type. DO NOT move any files - just show me the plan.
|
||||
`, targetFolder)
|
||||
|
||||
session.Send(copilot.MessageOptions{Prompt: prompt})
|
||||
session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt})
|
||||
```
|
||||
|
||||
## Custom grouping with AI analysis
|
||||
@@ -134,7 +142,7 @@ Consider:
|
||||
Propose folder names that are descriptive and useful.
|
||||
`, targetFolder)
|
||||
|
||||
session.Send(copilot.MessageOptions{Prompt: prompt})
|
||||
session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt})
|
||||
```
|
||||
|
||||
## Safety considerations
|
||||
|
||||
@@ -18,47 +18,49 @@ You need to run multiple conversations in parallel, each with its own context an
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"github.com/github/copilot-sdk/go"
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := copilot.NewClient()
|
||||
ctx := context.Background()
|
||||
client := copilot.NewClient(nil)
|
||||
|
||||
if err := client.Start(); err != nil {
|
||||
if err := client.Start(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
// Create multiple independent sessions
|
||||
session1, err := client.CreateSession(copilot.SessionConfig{Model: "gpt-5"})
|
||||
session1, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer session1.Destroy()
|
||||
|
||||
session2, err := client.CreateSession(copilot.SessionConfig{Model: "gpt-5"})
|
||||
session2, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer session2.Destroy()
|
||||
|
||||
session3, err := client.CreateSession(copilot.SessionConfig{Model: "claude-sonnet-4.5"})
|
||||
session3, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "claude-sonnet-4.5"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer session3.Destroy()
|
||||
|
||||
// Each session maintains its own conversation history
|
||||
session1.Send(copilot.MessageOptions{Prompt: "You are helping with a Python project"})
|
||||
session2.Send(copilot.MessageOptions{Prompt: "You are helping with a TypeScript project"})
|
||||
session3.Send(copilot.MessageOptions{Prompt: "You are helping with a Go project"})
|
||||
session1.Send(ctx, copilot.MessageOptions{Prompt: "You are helping with a Python project"})
|
||||
session2.Send(ctx, copilot.MessageOptions{Prompt: "You are helping with a TypeScript project"})
|
||||
session3.Send(ctx, copilot.MessageOptions{Prompt: "You are helping with a Go project"})
|
||||
|
||||
// Follow-up messages stay in their respective contexts
|
||||
session1.Send(copilot.MessageOptions{Prompt: "How do I create a virtual environment?"})
|
||||
session2.Send(copilot.MessageOptions{Prompt: "How do I set up tsconfig?"})
|
||||
session3.Send(copilot.MessageOptions{Prompt: "How do I initialize a module?"})
|
||||
session1.Send(ctx, copilot.MessageOptions{Prompt: "How do I create a virtual environment?"})
|
||||
session2.Send(ctx, copilot.MessageOptions{Prompt: "How do I set up tsconfig?"})
|
||||
session3.Send(ctx, copilot.MessageOptions{Prompt: "How do I initialize a module?"})
|
||||
}
|
||||
```
|
||||
|
||||
@@ -67,7 +69,7 @@ func main() {
|
||||
Use custom IDs for easier tracking:
|
||||
|
||||
```go
|
||||
session, err := client.CreateSession(copilot.SessionConfig{
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
SessionID: "user-123-chat",
|
||||
Model: "gpt-5",
|
||||
})
|
||||
@@ -81,7 +83,7 @@ fmt.Println(session.SessionID) // "user-123-chat"
|
||||
## Listing sessions
|
||||
|
||||
```go
|
||||
sessions, err := client.ListSessions()
|
||||
sessions, err := client.ListSessions(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -95,7 +97,7 @@ for _, sessionInfo := range sessions {
|
||||
|
||||
```go
|
||||
// Delete a specific session
|
||||
if err := client.DeleteSession("user-123-chat"); err != nil {
|
||||
if err := client.DeleteSession(ctx, "user-123-chat"); err != nil {
|
||||
log.Printf("Failed to delete session: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -19,22 +19,24 @@ You want users to be able to continue a conversation even after closing and reop
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/github/copilot-sdk/go"
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := copilot.NewClient()
|
||||
client.Start()
|
||||
ctx := context.Background()
|
||||
client := copilot.NewClient(nil)
|
||||
client.Start(ctx)
|
||||
defer client.Stop()
|
||||
|
||||
// Create session with a memorable ID
|
||||
session, _ := client.CreateSession(copilot.SessionConfig{
|
||||
session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
SessionID: "user-123-conversation",
|
||||
Model: "gpt-5",
|
||||
})
|
||||
|
||||
session.Send(copilot.MessageOptions{Prompt: "Let's discuss TypeScript generics"})
|
||||
session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Let's discuss TypeScript generics"})
|
||||
|
||||
// Session ID is preserved
|
||||
fmt.Println(session.SessionID)
|
||||
@@ -47,15 +49,16 @@ func main() {
|
||||
### Resuming a session
|
||||
|
||||
```go
|
||||
client := copilot.NewClient()
|
||||
client.Start()
|
||||
ctx := context.Background()
|
||||
client := copilot.NewClient(nil)
|
||||
client.Start(ctx)
|
||||
defer client.Stop()
|
||||
|
||||
// Resume the previous session
|
||||
session, _ := client.ResumeSession("user-123-conversation")
|
||||
session, _ := client.ResumeSession(ctx, "user-123-conversation")
|
||||
|
||||
// Previous context is restored
|
||||
session.Send(copilot.MessageOptions{Prompt: "What were we discussing?"})
|
||||
session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "What were we discussing?"})
|
||||
|
||||
session.Destroy()
|
||||
```
|
||||
@@ -63,7 +66,7 @@ session.Destroy()
|
||||
### Listing available sessions
|
||||
|
||||
```go
|
||||
sessions, _ := client.ListSessions()
|
||||
sessions, _ := client.ListSessions(ctx)
|
||||
for _, s := range sessions {
|
||||
fmt.Println("Session:", s.SessionID)
|
||||
}
|
||||
@@ -73,15 +76,17 @@ for _, s := range sessions {
|
||||
|
||||
```go
|
||||
// Remove session and all its data from disk
|
||||
client.DeleteSession("user-123-conversation")
|
||||
client.DeleteSession(ctx, "user-123-conversation")
|
||||
```
|
||||
|
||||
### Getting session history
|
||||
|
||||
```go
|
||||
messages, _ := session.GetMessages()
|
||||
messages, _ := session.GetMessages(ctx)
|
||||
for _, msg := range messages {
|
||||
fmt.Printf("[%s] %v\n", msg.Type, msg.Data)
|
||||
if msg.Data.Content != nil {
|
||||
fmt.Printf("[%s] %s\n", msg.Type, *msg.Data.Content)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -46,7 +47,7 @@ import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"github.com/github/copilot-sdk/go"
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
@@ -94,6 +95,7 @@ func promptForRepo() string {
|
||||
// ============================================================================
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
repoFlag := flag.String("repo", "", "GitHub repository (owner/repo)")
|
||||
flag.Parse()
|
||||
|
||||
@@ -126,18 +128,18 @@ func main() {
|
||||
parts := strings.SplitN(repo, "/", 2)
|
||||
owner, repoName := parts[0], parts[1]
|
||||
|
||||
// Create Copilot client - no custom tools needed!
|
||||
client := copilot.NewClient(copilot.ClientConfig{LogLevel: "error"})
|
||||
// Create Copilot client
|
||||
client := copilot.NewClient(nil)
|
||||
|
||||
if err := client.Start(); err != nil {
|
||||
if err := client.Start(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
session, err := client.CreateSession(copilot.SessionConfig{
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
Model: "gpt-5",
|
||||
SystemMessage: copilot.SystemMessage{
|
||||
SystemMessage: &copilot.SystemMessageConfig{
|
||||
Content: fmt.Sprintf(`
|
||||
<context>
|
||||
You are analyzing pull requests for the GitHub repository: %s/%s
|
||||
@@ -159,12 +161,16 @@ The current working directory is: %s
|
||||
defer session.Destroy()
|
||||
|
||||
// Set up event handling
|
||||
session.On(func(event copilot.Event) {
|
||||
switch e := event.(type) {
|
||||
case copilot.AssistantMessageEvent:
|
||||
fmt.Printf("\n🤖 %s\n\n", e.Data.Content)
|
||||
case copilot.ToolExecutionStartEvent:
|
||||
fmt.Printf(" ⚙️ %s\n", e.Data.ToolName)
|
||||
session.On(func(event copilot.SessionEvent) {
|
||||
switch event.Type {
|
||||
case "assistant.message":
|
||||
if event.Data.Content != nil {
|
||||
fmt.Printf("\n🤖 %s\n\n", *event.Data.Content)
|
||||
}
|
||||
case "tool.execution_start":
|
||||
if event.Data.ToolName != nil {
|
||||
fmt.Printf(" ⚙️ %s\n", *event.Data.ToolName)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -180,12 +186,10 @@ The current working directory is: %s
|
||||
Finally, summarize the PR health - average age, oldest PR, and how many might be considered stale.
|
||||
`, owner, repoName)
|
||||
|
||||
if err := session.Send(copilot.MessageOptions{Prompt: prompt}); err != nil {
|
||||
if _, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
session.WaitForIdle()
|
||||
|
||||
// Interactive loop
|
||||
fmt.Println("\n💡 Ask follow-up questions or type \"exit\" to quit.\n")
|
||||
fmt.Println("Examples:")
|
||||
@@ -209,11 +213,9 @@ The current working directory is: %s
|
||||
break
|
||||
}
|
||||
|
||||
if err := session.Send(copilot.MessageOptions{Prompt: input}); err != nil {
|
||||
if _, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: input}); err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
}
|
||||
|
||||
session.WaitForIdle()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
296
cookbook/copilot-sdk/go/ralph-loop.md
Normal file
296
cookbook/copilot-sdk/go/ralph-loop.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# Ralph Loop: Autonomous AI Task Loops
|
||||
|
||||
Build autonomous coding loops where an AI agent picks tasks, implements them, validates against backpressure (tests, builds), commits, and repeats — each iteration in a fresh context window.
|
||||
|
||||
> **Runnable example:** [recipe/ralph-loop.go](recipe/ralph-loop.go)
|
||||
>
|
||||
> ```bash
|
||||
> cd go
|
||||
> go run recipe/ralph-loop.go
|
||||
> ```
|
||||
|
||||
## What is a Ralph Loop?
|
||||
|
||||
A [Ralph loop](https://ghuntley.com/ralph/) is an autonomous development workflow where an AI agent iterates through tasks in isolated context windows. The key insight: **state lives on disk, not in the model's context**. Each iteration starts fresh, reads the current state from files, does one task, writes results back to disk, and exits.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ loop.sh │
|
||||
│ while true: │
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ Fresh session (isolated context) │ │
|
||||
│ │ │ │
|
||||
│ │ 1. Read PROMPT.md + AGENTS.md │ │
|
||||
│ │ 2. Study specs/* and code │ │
|
||||
│ │ 3. Pick next task from plan │ │
|
||||
│ │ 4. Implement + run tests │ │
|
||||
│ │ 5. Update plan, commit, exit │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
│ ↻ next iteration (fresh context) │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Core principles:**
|
||||
|
||||
- **Fresh context per iteration**: Each loop creates a new session — no context accumulation, always in the "smart zone"
|
||||
- **Disk as shared state**: `IMPLEMENTATION_PLAN.md` persists between iterations and acts as the coordination mechanism
|
||||
- **Backpressure steers quality**: Tests, builds, and lints reject bad work — the agent must fix issues before committing
|
||||
- **Two modes**: PLANNING (gap analysis → generate plan) and BUILDING (implement from plan)
|
||||
|
||||
## Simple Version
|
||||
|
||||
The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | copilot ; done`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func ralphLoop(ctx context.Context, promptFile string, maxIterations int) error {
|
||||
client := copilot.NewClient(nil)
|
||||
if err := client.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
prompt, err := os.ReadFile(promptFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 1; i <= maxIterations; i++ {
|
||||
fmt.Printf("\n=== Iteration %d/%d ===\n", i, maxIterations)
|
||||
|
||||
// Fresh session each iteration — context isolation is the point
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
Model: "gpt-5.1-codex-mini",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = session.SendAndWait(ctx, copilot.MessageOptions{
|
||||
Prompt: string(prompt),
|
||||
})
|
||||
session.Destroy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Iteration %d complete.\n", i)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := ralphLoop(context.Background(), "PROMPT.md", 20); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is all you need to get started. The prompt file tells the agent what to do; the agent reads project files, does work, commits, and exits. The loop restarts with a clean slate.
|
||||
|
||||
## Ideal Version
|
||||
|
||||
The full Ralph pattern with planning and building modes, matching the [Ralph Playbook](https://github.com/ClaytonFarr/ralph-playbook) architecture:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func ralphLoop(ctx context.Context, mode string, maxIterations int) error {
|
||||
promptFile := "PROMPT_build.md"
|
||||
if mode == "plan" {
|
||||
promptFile = "PROMPT_plan.md"
|
||||
}
|
||||
|
||||
client := copilot.NewClient(nil)
|
||||
if err := client.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
fmt.Println(strings.Repeat("━", 40))
|
||||
fmt.Printf("Mode: %s\n", mode)
|
||||
fmt.Printf("Prompt: %s\n", promptFile)
|
||||
fmt.Printf("Max: %d iterations\n", maxIterations)
|
||||
fmt.Println(strings.Repeat("━", 40))
|
||||
|
||||
prompt, err := os.ReadFile(promptFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 1; i <= maxIterations; i++ {
|
||||
fmt.Printf("\n=== Iteration %d/%d ===\n", i, maxIterations)
|
||||
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
Model: "gpt-5.1-codex-mini",
|
||||
WorkingDirectory: cwd,
|
||||
OnPermissionRequest: func(_ copilot.PermissionRequest, _ map[string]string) copilot.PermissionRequestResult {
|
||||
return copilot.PermissionRequestResult{Kind: "approved"}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Log tool usage for visibility
|
||||
session.On(func(event copilot.Event) {
|
||||
if toolExecution, ok := event.(copilot.ToolExecutionStartEvent); ok {
|
||||
fmt.Printf(" ⚙ %s\n", toolExecution.Data.ToolName)
|
||||
}
|
||||
})
|
||||
|
||||
_, err = session.SendAndWait(ctx, copilot.MessageOptions{
|
||||
Prompt: string(prompt),
|
||||
})
|
||||
session.Destroy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("\nIteration %d complete.\n", i)
|
||||
}
|
||||
|
||||
fmt.Printf("\nReached max iterations: %d\n", maxIterations)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
mode := "build"
|
||||
maxIterations := 50
|
||||
|
||||
for _, arg := range os.Args[1:] {
|
||||
if arg == "plan" {
|
||||
mode = "plan"
|
||||
} else if n, err := strconv.Atoi(arg); err == nil {
|
||||
maxIterations = n
|
||||
}
|
||||
}
|
||||
|
||||
if err := ralphLoop(context.Background(), mode, maxIterations); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Required Project Files
|
||||
|
||||
The ideal version expects this file structure in your project:
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── PROMPT_plan.md # Planning mode instructions
|
||||
├── PROMPT_build.md # Building mode instructions
|
||||
├── AGENTS.md # Operational guide (build/test commands)
|
||||
├── IMPLEMENTATION_PLAN.md # Task list (generated by planning mode)
|
||||
├── specs/ # Requirement specs (one per topic)
|
||||
│ ├── auth.md
|
||||
│ └── data-pipeline.md
|
||||
└── src/ # Your source code
|
||||
```
|
||||
|
||||
### Example `PROMPT_plan.md`
|
||||
|
||||
```markdown
|
||||
0a. Study `specs/*` to learn the application specifications.
|
||||
0b. Study IMPLEMENTATION_PLAN.md (if present) to understand the plan so far.
|
||||
0c. Study `src/` to understand existing code and shared utilities.
|
||||
|
||||
1. Compare specs against code (gap analysis). Create or update
|
||||
IMPLEMENTATION_PLAN.md as a prioritized bullet-point list of tasks
|
||||
yet to be implemented. Do NOT implement anything.
|
||||
|
||||
IMPORTANT: Do NOT assume functionality is missing — search the
|
||||
codebase first to confirm. Prefer updating existing utilities over
|
||||
creating ad-hoc copies.
|
||||
```
|
||||
|
||||
### Example `PROMPT_build.md`
|
||||
|
||||
```markdown
|
||||
0a. Study `specs/*` to learn the application specifications.
|
||||
0b. Study IMPLEMENTATION_PLAN.md.
|
||||
0c. Study `src/` for reference.
|
||||
|
||||
1. Choose the most important item from IMPLEMENTATION_PLAN.md. Before
|
||||
making changes, search the codebase (don't assume not implemented).
|
||||
2. After implementing, run the tests. If functionality is missing, add it.
|
||||
3. When you discover issues, update IMPLEMENTATION_PLAN.md immediately.
|
||||
4. When tests pass, update IMPLEMENTATION_PLAN.md, then `git add -A`
|
||||
then `git commit` with a descriptive message.
|
||||
|
||||
5. When authoring documentation, capture the why.
|
||||
6. Implement completely. No placeholders or stubs.
|
||||
7. Keep IMPLEMENTATION_PLAN.md current — future iterations depend on it.
|
||||
```
|
||||
|
||||
### Example `AGENTS.md`
|
||||
|
||||
Keep this brief (~60 lines). It's loaded every iteration, so bloat wastes context.
|
||||
|
||||
```markdown
|
||||
## Build & Run
|
||||
|
||||
go build ./...
|
||||
|
||||
## Validation
|
||||
|
||||
- Tests: `go test ./...`
|
||||
- Vet: `go vet ./...`
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Fresh context per iteration**: Never accumulate context across iterations — that's the whole point
|
||||
2. **Disk is your database**: `IMPLEMENTATION_PLAN.md` is shared state between isolated sessions
|
||||
3. **Backpressure is essential**: Tests, builds, lints in `AGENTS.md` — the agent must pass them before committing
|
||||
4. **Start with PLANNING mode**: Generate the plan first, then switch to BUILDING
|
||||
5. **Observe and tune**: Watch early iterations, add guardrails to prompts when the agent fails in specific ways
|
||||
6. **The plan is disposable**: If the agent goes off track, delete `IMPLEMENTATION_PLAN.md` and re-plan
|
||||
7. **Keep `AGENTS.md` brief**: It's loaded every iteration — operational info only, no progress notes
|
||||
8. **Use a sandbox**: The agent runs autonomously with full tool access — isolate it
|
||||
9. **Set `WorkingDirectory`**: Pin the session to your project root so tool operations resolve paths correctly
|
||||
10. **Auto-approve permissions**: Use `OnPermissionRequest` to allow tool calls without interrupting the loop
|
||||
|
||||
## When to Use a Ralph Loop
|
||||
|
||||
**Good for:**
|
||||
|
||||
- Implementing features from specs with test-driven validation
|
||||
- Large refactors broken into many small tasks
|
||||
- Unattended, long-running development with clear requirements
|
||||
- Any work where backpressure (tests/builds) can verify correctness
|
||||
|
||||
**Not good for:**
|
||||
|
||||
- Tasks requiring human judgment mid-loop
|
||||
- One-shot operations that don't benefit from iteration
|
||||
- Vague requirements without testable acceptance criteria
|
||||
- Exploratory prototyping where direction isn't clear
|
||||
|
||||
## See Also
|
||||
|
||||
- [Error Handling](error-handling.md) — timeout patterns and graceful shutdown for long-running sessions
|
||||
- [Persisting Sessions](persisting-sessions.md) — save and resume sessions across restarts
|
||||
213
cookbook/copilot-sdk/go/recipe/accessibility-report.go
Normal file
213
cookbook/copilot-sdk/go/recipe/accessibility-report.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Println("=== Accessibility Report Generator ===")
|
||||
fmt.Println()
|
||||
|
||||
fmt.Print("Enter URL to analyze: ")
|
||||
url, _ := reader.ReadString('\n')
|
||||
url = strings.TrimSpace(url)
|
||||
|
||||
if url == "" {
|
||||
fmt.Println("No URL provided. Exiting.")
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure URL has a scheme
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
fmt.Printf("\nAnalyzing: %s\n", url)
|
||||
fmt.Println("Please wait...\n")
|
||||
|
||||
// Create Copilot client with Playwright MCP server
|
||||
client := copilot.NewClient(nil)
|
||||
|
||||
if err := client.Start(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
streaming := true
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
Model: "claude-opus-4.6",
|
||||
Streaming: &streaming,
|
||||
McpServers: map[string]interface{}{
|
||||
"playwright": map[string]interface{}{
|
||||
"type": "local",
|
||||
"command": "npx",
|
||||
"args": []string{"@playwright/mcp@latest"},
|
||||
"tools": []string{"*"},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer session.Destroy()
|
||||
|
||||
// Set up streaming event handling
|
||||
done := make(chan struct{}, 1)
|
||||
|
||||
session.On(func(event copilot.SessionEvent) {
|
||||
switch event.Type {
|
||||
case "assistant.message.delta":
|
||||
if event.Data.DeltaContent != nil {
|
||||
fmt.Print(*event.Data.DeltaContent)
|
||||
}
|
||||
case "session.idle":
|
||||
select {
|
||||
case done <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
case "session.error":
|
||||
if event.Data.Message != nil {
|
||||
fmt.Printf("\nError: %s\n", *event.Data.Message)
|
||||
}
|
||||
select {
|
||||
case done <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
prompt := fmt.Sprintf(`
|
||||
Use the Playwright MCP server to analyze the accessibility of this webpage: %s
|
||||
|
||||
Please:
|
||||
1. Navigate to the URL using playwright-browser_navigate
|
||||
2. Take an accessibility snapshot using playwright-browser_snapshot
|
||||
3. Analyze the snapshot and provide a detailed accessibility report
|
||||
|
||||
Format the report EXACTLY like this structure with emoji indicators:
|
||||
|
||||
📊 Accessibility Report: [Page Title] (domain.com)
|
||||
|
||||
✅ What's Working Well
|
||||
| Category | Status | Details |
|
||||
|----------|--------|---------|
|
||||
| Language | ✅ Pass | lang="en-US" properly set |
|
||||
| Page Title | ✅ Pass | "[Title]" is descriptive |
|
||||
| Heading Hierarchy | ✅ Pass | Single H1, proper H2/H3 structure |
|
||||
| Images | ✅ Pass | All X images have alt text |
|
||||
| Viewport | ✅ Pass | Allows pinch-zoom (no user-scalable=no) |
|
||||
| Links | ✅ Pass | No ambiguous "click here" links |
|
||||
| Reduced Motion | ✅ Pass | Supports prefers-reduced-motion |
|
||||
| Autoplay Media | ✅ Pass | No autoplay audio/video |
|
||||
|
||||
⚠️ Issues Found
|
||||
| Severity | Issue | WCAG Criterion | Recommendation |
|
||||
|----------|-------|----------------|----------------|
|
||||
| 🔴 High | No <main> landmark | 1.3.1, 2.4.1 | Wrap main content in <main> element |
|
||||
| 🔴 High | No skip navigation link | 2.4.1 | Add "Skip to content" link at top |
|
||||
| 🟡 Medium | Focus outlines disabled | 2.4.7 | Default outline is none - ensure visible :focus styles exist |
|
||||
| 🟡 Medium | Small touch targets | 2.5.8 | Navigation links are 37px tall (below 44px minimum) |
|
||||
|
||||
📋 Stats Summary
|
||||
- Total Links: X
|
||||
- Total Headings: X (1× H1, proper hierarchy)
|
||||
- Focusable Elements: X
|
||||
- Landmarks Found: banner ✅, navigation ✅, main ❌, footer ✅
|
||||
|
||||
⚙️ Priority Recommendations
|
||||
- Add <main> landmark - Wrap page content in <main role="main"> for screen reader navigation
|
||||
- Add skip link - Hidden link at start: <a href="#main-content" class="skip-link">Skip to content</a>
|
||||
- Increase touch targets - Add padding to nav links and tags to meet 44×44px minimum
|
||||
- Verify focus styles - Test keyboard navigation; add visible :focus or :focus-visible outlines
|
||||
|
||||
Use ✅ for pass, 🔴 for high severity issues, 🟡 for medium severity, ❌ for missing items.
|
||||
Include actual findings from the page analysis - don't just copy the example.
|
||||
`, url)
|
||||
|
||||
if _, err := session.Send(ctx, copilot.MessageOptions{Prompt: prompt}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
<-done
|
||||
|
||||
fmt.Println("\n\n=== Report Complete ===\n")
|
||||
|
||||
// Prompt user for test generation
|
||||
fmt.Print("Would you like to generate Playwright accessibility tests? (y/n): ")
|
||||
generateTests, _ := reader.ReadString('\n')
|
||||
generateTests = strings.TrimSpace(strings.ToLower(generateTests))
|
||||
|
||||
if generateTests == "y" || generateTests == "yes" {
|
||||
detectLanguagePrompt := `
|
||||
Analyze the current working directory to detect the primary programming language used in this project.
|
||||
Look for project files like package.json, *.csproj, pom.xml, requirements.txt, go.mod, etc.
|
||||
|
||||
Respond with ONLY the detected language name (e.g., "TypeScript", "JavaScript", "C#", "Python", "Java")
|
||||
and a brief explanation of why you detected it.
|
||||
If no project is detected, suggest "TypeScript" as the default for Playwright tests.
|
||||
`
|
||||
|
||||
fmt.Println("\nDetecting project language...\n")
|
||||
// Drain the previous done signal
|
||||
select {
|
||||
case <-done:
|
||||
default:
|
||||
}
|
||||
if _, err := session.Send(ctx, copilot.MessageOptions{Prompt: detectLanguagePrompt}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
<-done
|
||||
|
||||
fmt.Print("\n\nConfirm language for tests (or enter a different one): ")
|
||||
language, _ := reader.ReadString('\n')
|
||||
language = strings.TrimSpace(language)
|
||||
if language == "" {
|
||||
language = "TypeScript"
|
||||
}
|
||||
|
||||
testGenerationPrompt := fmt.Sprintf(`
|
||||
Based on the accessibility report you just generated for %s, create Playwright accessibility tests in %s.
|
||||
|
||||
The tests should:
|
||||
1. Verify all the accessibility checks from the report
|
||||
2. Test for the issues that were found (to ensure they get fixed)
|
||||
3. Include tests for:
|
||||
- Page has proper lang attribute
|
||||
- Page has descriptive title
|
||||
- Heading hierarchy is correct (single H1, proper nesting)
|
||||
- All images have alt text
|
||||
- No autoplay media
|
||||
- Landmark regions exist (banner, nav, main, footer)
|
||||
- Skip navigation link exists and works
|
||||
- Focus indicators are visible
|
||||
- Touch targets meet minimum size requirements
|
||||
4. Use Playwright's accessibility testing features
|
||||
5. Include helpful comments explaining each test
|
||||
|
||||
Output the complete test file that can be saved and run.
|
||||
Use the Playwright MCP server tools if you need to verify any page details.
|
||||
`, url, language)
|
||||
|
||||
fmt.Println("\nGenerating accessibility tests...\n")
|
||||
// Drain the previous done signal
|
||||
select {
|
||||
case <-done:
|
||||
default:
|
||||
}
|
||||
if _, err := session.Send(ctx, copilot.MessageOptions{Prompt: testGenerationPrompt}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
<-done
|
||||
|
||||
fmt.Println("\n\n=== Tests Generated ===")
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/github/copilot-sdk/go"
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := copilot.NewClient()
|
||||
ctx := context.Background()
|
||||
client := copilot.NewClient(nil)
|
||||
|
||||
if err := client.Start(); err != nil {
|
||||
if err := client.Start(ctx); err != nil {
|
||||
log.Fatalf("Failed to start client: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := client.Stop(); err != nil {
|
||||
log.Printf("Error stopping client: %v", err)
|
||||
}
|
||||
}()
|
||||
defer client.Stop()
|
||||
|
||||
session, err := client.CreateSession(copilot.SessionConfig{
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
Model: "gpt-5",
|
||||
})
|
||||
if err != nil {
|
||||
@@ -27,18 +25,13 @@ func main() {
|
||||
}
|
||||
defer session.Destroy()
|
||||
|
||||
responseChan := make(chan string, 1)
|
||||
session.On(func(event copilot.Event) {
|
||||
if msg, ok := event.(copilot.AssistantMessageEvent); ok {
|
||||
responseChan <- msg.Data.Content
|
||||
}
|
||||
})
|
||||
|
||||
if err := session.Send(copilot.MessageOptions{Prompt: "Hello!"}); err != nil {
|
||||
result, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"})
|
||||
if err != nil {
|
||||
log.Printf("Failed to send message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
response := <-responseChan
|
||||
fmt.Println(response)
|
||||
if result != nil && result.Data.Content != nil {
|
||||
fmt.Println(*result.Data.Content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/github/copilot-sdk/go"
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create and start client
|
||||
client := copilot.NewClient()
|
||||
if err := client.Start(); err != nil {
|
||||
client := copilot.NewClient(nil)
|
||||
if err := client.Start(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
// Create session
|
||||
session, err := client.CreateSession(copilot.SessionConfig{
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
Model: "gpt-5",
|
||||
})
|
||||
if err != nil {
|
||||
@@ -27,14 +30,20 @@ func main() {
|
||||
defer session.Destroy()
|
||||
|
||||
// Event handler
|
||||
session.On(func(event copilot.Event) {
|
||||
switch e := event.(type) {
|
||||
case copilot.AssistantMessageEvent:
|
||||
fmt.Printf("\nCopilot: %s\n", e.Data.Content)
|
||||
case copilot.ToolExecutionStartEvent:
|
||||
fmt.Printf(" → Running: %s\n", e.Data.ToolName)
|
||||
case copilot.ToolExecutionCompleteEvent:
|
||||
fmt.Printf(" ✓ Completed: %s\n", e.Data.ToolName)
|
||||
session.On(func(event copilot.SessionEvent) {
|
||||
switch event.Type {
|
||||
case "assistant.message":
|
||||
if event.Data.Content != nil {
|
||||
fmt.Printf("\nCopilot: %s\n", *event.Data.Content)
|
||||
}
|
||||
case "tool.execution_start":
|
||||
if event.Data.ToolName != nil {
|
||||
fmt.Printf(" → Running: %s\n", *event.Data.ToolName)
|
||||
}
|
||||
case "tool.execution_complete":
|
||||
if event.Data.ToolName != nil {
|
||||
fmt.Printf(" ✓ Completed: %s\n", *event.Data.ToolName)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -54,9 +63,8 @@ Analyze the files in "%s" and organize them into subfolders.
|
||||
Please confirm before moving any files.
|
||||
`, targetFolder)
|
||||
|
||||
if err := session.Send(copilot.MessageOptions{Prompt: prompt}); err != nil {
|
||||
_, err = session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
session.WaitForIdle()
|
||||
}
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/github/copilot-sdk/go"
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := copilot.NewClient()
|
||||
ctx := context.Background()
|
||||
client := copilot.NewClient(nil)
|
||||
|
||||
if err := client.Start(); err != nil {
|
||||
if err := client.Start(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
// Create multiple independent sessions
|
||||
session1, err := client.CreateSession(copilot.SessionConfig{Model: "gpt-5"})
|
||||
session1, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer session1.Destroy()
|
||||
|
||||
session2, err := client.CreateSession(copilot.SessionConfig{Model: "gpt-5"})
|
||||
session2, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer session2.Destroy()
|
||||
|
||||
session3, err := client.CreateSession(copilot.SessionConfig{Model: "claude-sonnet-4.5"})
|
||||
session3, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "claude-sonnet-4.5"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -37,16 +39,16 @@ func main() {
|
||||
fmt.Println("Created 3 independent sessions")
|
||||
|
||||
// Each session maintains its own conversation history
|
||||
session1.Send(copilot.MessageOptions{Prompt: "You are helping with a Python project"})
|
||||
session2.Send(copilot.MessageOptions{Prompt: "You are helping with a TypeScript project"})
|
||||
session3.Send(copilot.MessageOptions{Prompt: "You are helping with a Go project"})
|
||||
session1.Send(ctx, copilot.MessageOptions{Prompt: "You are helping with a Python project"})
|
||||
session2.Send(ctx, copilot.MessageOptions{Prompt: "You are helping with a TypeScript project"})
|
||||
session3.Send(ctx, copilot.MessageOptions{Prompt: "You are helping with a Go project"})
|
||||
|
||||
fmt.Println("Sent initial context to all sessions")
|
||||
|
||||
// Follow-up messages stay in their respective contexts
|
||||
session1.Send(copilot.MessageOptions{Prompt: "How do I create a virtual environment?"})
|
||||
session2.Send(copilot.MessageOptions{Prompt: "How do I set up tsconfig?"})
|
||||
session3.Send(copilot.MessageOptions{Prompt: "How do I initialize a module?"})
|
||||
session1.Send(ctx, copilot.MessageOptions{Prompt: "How do I create a virtual environment?"})
|
||||
session2.Send(ctx, copilot.MessageOptions{Prompt: "How do I set up tsconfig?"})
|
||||
session3.Send(ctx, copilot.MessageOptions{Prompt: "How do I initialize a module?"})
|
||||
|
||||
fmt.Println("Sent follow-up questions to each session")
|
||||
fmt.Println("All sessions will be destroyed on exit")
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/github/copilot-sdk/go"
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := copilot.NewClient()
|
||||
if err := client.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Stop()
|
||||
ctx := context.Background()
|
||||
client := copilot.NewClient(nil)
|
||||
if err := client.Start(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
// Create session with a memorable ID
|
||||
session, err := client.CreateSession(copilot.SessionConfig{
|
||||
SessionID: "user-123-conversation",
|
||||
Model: "gpt-5",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Create session with a memorable ID
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
SessionID: "user-123-conversation",
|
||||
Model: "gpt-5",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := session.Send(copilot.MessageOptions{Prompt: "Let's discuss TypeScript generics"}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Session created: %s\n", session.SessionID)
|
||||
_, err = session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Let's discuss TypeScript generics"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Session created: %s\n", session.SessionID)
|
||||
|
||||
// Destroy session but keep data on disk
|
||||
if err := session.Destroy(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Session destroyed (state persisted)")
|
||||
// Destroy session but keep data on disk
|
||||
session.Destroy()
|
||||
fmt.Println("Session destroyed (state persisted)")
|
||||
|
||||
// Resume the previous session
|
||||
resumed, err := client.ResumeSession("user-123-conversation")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Resumed: %s\n", resumed.SessionID)
|
||||
// Resume the previous session
|
||||
resumed, err := client.ResumeSession(ctx, "user-123-conversation")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Resumed: %s\n", resumed.SessionID)
|
||||
|
||||
if err := resumed.Send(copilot.MessageOptions{Prompt: "What were we discussing?"}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
_, err = resumed.SendAndWait(ctx, copilot.MessageOptions{Prompt: "What were we discussing?"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// List sessions
|
||||
sessions, err := client.ListSessions()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ids := make([]string, 0, len(sessions))
|
||||
for _, s := range sessions {
|
||||
ids = append(ids, s.SessionID)
|
||||
}
|
||||
fmt.Printf("Sessions: %v\n", ids)
|
||||
// List sessions
|
||||
sessions, err := client.ListSessions(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ids := make([]string, 0, len(sessions))
|
||||
for _, s := range sessions {
|
||||
ids = append(ids, s.SessionID)
|
||||
}
|
||||
fmt.Printf("Sessions: %v\n", ids)
|
||||
|
||||
// Delete session permanently
|
||||
if err := client.DeleteSession("user-123-conversation"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Session deleted")
|
||||
// Delete session permanently
|
||||
if err := client.DeleteSession(ctx, "user-123-conversation"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Session deleted")
|
||||
|
||||
if err := resumed.Destroy(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
resumed.Destroy()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/copilot-sdk/go"
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
@@ -58,6 +59,7 @@ func promptForRepo() string {
|
||||
// ============================================================================
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
repoFlag := flag.String("repo", "", "GitHub repository (owner/repo)")
|
||||
flag.Parse()
|
||||
|
||||
@@ -90,18 +92,18 @@ func main() {
|
||||
parts := strings.SplitN(repo, "/", 2)
|
||||
owner, repoName := parts[0], parts[1]
|
||||
|
||||
// Create Copilot client - no custom tools needed!
|
||||
client := copilot.NewClient(copilot.ClientConfig{LogLevel: "error"})
|
||||
// Create Copilot client
|
||||
client := copilot.NewClient(nil)
|
||||
|
||||
if err := client.Start(); err != nil {
|
||||
if err := client.Start(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
session, err := client.CreateSession(copilot.SessionConfig{
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
Model: "gpt-5",
|
||||
SystemMessage: copilot.SystemMessage{
|
||||
SystemMessage: &copilot.SystemMessageConfig{
|
||||
Content: fmt.Sprintf(`
|
||||
<context>
|
||||
You are analyzing pull requests for the GitHub repository: %s/%s
|
||||
@@ -123,12 +125,16 @@ The current working directory is: %s
|
||||
defer session.Destroy()
|
||||
|
||||
// Set up event handling
|
||||
session.On(func(event copilot.Event) {
|
||||
switch e := event.(type) {
|
||||
case copilot.AssistantMessageEvent:
|
||||
fmt.Printf("\n🤖 %s\n\n", e.Data.Content)
|
||||
case copilot.ToolExecutionStartEvent:
|
||||
fmt.Printf(" ⚙️ %s\n", e.Data.ToolName)
|
||||
session.On(func(event copilot.SessionEvent) {
|
||||
switch event.Type {
|
||||
case "assistant.message":
|
||||
if event.Data.Content != nil {
|
||||
fmt.Printf("\n🤖 %s\n\n", *event.Data.Content)
|
||||
}
|
||||
case "tool.execution_start":
|
||||
if event.Data.ToolName != nil {
|
||||
fmt.Printf(" ⚙️ %s\n", *event.Data.ToolName)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -144,12 +150,10 @@ The current working directory is: %s
|
||||
Finally, summarize the PR health - average age, oldest PR, and how many might be considered stale.
|
||||
`, owner, repoName)
|
||||
|
||||
if err := session.Send(copilot.MessageOptions{Prompt: prompt}); err != nil {
|
||||
if _, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
session.WaitForIdle()
|
||||
|
||||
// Interactive loop
|
||||
fmt.Println("\n💡 Ask follow-up questions or type \"exit\" to quit.\n")
|
||||
fmt.Println("Examples:")
|
||||
@@ -173,10 +177,8 @@ The current working directory is: %s
|
||||
break
|
||||
}
|
||||
|
||||
if err := session.Send(copilot.MessageOptions{Prompt: input}); err != nil {
|
||||
if _, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: input}); err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
}
|
||||
|
||||
session.WaitForIdle()
|
||||
}
|
||||
}
|
||||
|
||||
111
cookbook/copilot-sdk/go/recipe/ralph-loop.go
Normal file
111
cookbook/copilot-sdk/go/recipe/ralph-loop.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
copilot "github.com/github/copilot-sdk/go"
|
||||
)
|
||||
|
||||
// Ralph loop: autonomous AI task loop with fresh context per iteration.
|
||||
//
|
||||
// Two modes:
|
||||
// - "plan": reads PROMPT_plan.md, generates/updates IMPLEMENTATION_PLAN.md
|
||||
// - "build": reads PROMPT_build.md, implements tasks, runs tests, commits
|
||||
//
|
||||
// Each iteration creates a fresh session so the agent always operates in
|
||||
// the "smart zone" of its context window. State is shared between
|
||||
// iterations via files on disk (IMPLEMENTATION_PLAN.md, AGENTS.md, specs/*).
|
||||
//
|
||||
// Usage:
|
||||
// go run ralph-loop.go # build mode, 50 iterations
|
||||
// go run ralph-loop.go plan # planning mode
|
||||
// go run ralph-loop.go 20 # build mode, 20 iterations
|
||||
// go run ralph-loop.go plan 5 # planning mode, 5 iterations
|
||||
|
||||
func ralphLoop(ctx context.Context, mode string, maxIterations int) error {
|
||||
promptFile := "PROMPT_build.md"
|
||||
if mode == "plan" {
|
||||
promptFile = "PROMPT_plan.md"
|
||||
}
|
||||
|
||||
client := copilot.NewClient(nil)
|
||||
if err := client.Start(ctx); err != nil {
|
||||
return fmt.Errorf("failed to start client: %w", err)
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get working directory: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(strings.Repeat("━", 40))
|
||||
fmt.Printf("Mode: %s\n", mode)
|
||||
fmt.Printf("Prompt: %s\n", promptFile)
|
||||
fmt.Printf("Max: %d iterations\n", maxIterations)
|
||||
fmt.Println(strings.Repeat("━", 40))
|
||||
|
||||
prompt, err := os.ReadFile(promptFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read %s: %w", promptFile, err)
|
||||
}
|
||||
|
||||
for i := 1; i <= maxIterations; i++ {
|
||||
fmt.Printf("\n=== Iteration %d/%d ===\n", i, maxIterations)
|
||||
|
||||
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
|
||||
Model: "gpt-5.1-codex-mini",
|
||||
WorkingDirectory: cwd,
|
||||
OnPermissionRequest: func(_ copilot.PermissionRequest, _ map[string]string) copilot.PermissionRequestResult {
|
||||
return copilot.PermissionRequestResult{Kind: "approved"}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create session: %w", err)
|
||||
}
|
||||
|
||||
// Log tool usage for visibility
|
||||
session.On(func(event copilot.Event) {
|
||||
if toolExecution, ok := event.(copilot.ToolExecutionStartEvent); ok {
|
||||
fmt.Printf(" ⚙ %s\n", toolExecution.Data.ToolName)
|
||||
}
|
||||
})
|
||||
|
||||
_, err = session.SendAndWait(ctx, copilot.MessageOptions{
|
||||
Prompt: string(prompt),
|
||||
})
|
||||
if destroyErr := session.Destroy(); destroyErr != nil {
|
||||
log.Printf("failed to destroy session on iteration %d: %v", i, destroyErr)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("send failed on iteration %d: %w", i, err)
|
||||
}
|
||||
|
||||
fmt.Printf("\nIteration %d complete.\n", i)
|
||||
}
|
||||
|
||||
fmt.Printf("\nReached max iterations: %d\n", maxIterations)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
mode := "build"
|
||||
maxIterations := 50
|
||||
|
||||
for _, arg := range os.Args[1:] {
|
||||
if arg == "plan" {
|
||||
mode = "plan"
|
||||
} else if n, err := strconv.Atoi(arg); err == nil {
|
||||
maxIterations = n
|
||||
}
|
||||
}
|
||||
|
||||
if err := ralphLoop(context.Background(), mode, maxIterations); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
265
cookbook/copilot-sdk/nodejs/accessibility-report.md
Normal file
265
cookbook/copilot-sdk/nodejs/accessibility-report.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# Generating Accessibility Reports
|
||||
|
||||
Build a CLI tool that analyzes web page accessibility using the Playwright MCP server and generates detailed WCAG-compliant reports with optional test generation.
|
||||
|
||||
> **Runnable example:** [recipe/accessibility-report.ts](recipe/accessibility-report.ts)
|
||||
>
|
||||
> ```bash
|
||||
> cd recipe && npm install
|
||||
> npx tsx accessibility-report.ts
|
||||
> # or: npm run accessibility-report
|
||||
> ```
|
||||
|
||||
## Example scenario
|
||||
|
||||
You want to audit a website's accessibility compliance. This tool navigates to a URL using Playwright, captures an accessibility snapshot, and produces a structured report covering WCAG criteria like landmarks, heading hierarchy, focus management, and touch targets. It can also generate Playwright test files to automate future accessibility checks.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
npm install @github/copilot-sdk
|
||||
npm install -D typescript tsx @types/node
|
||||
```
|
||||
|
||||
You also need `npx` available (Node.js installed) for the Playwright MCP server.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
npx tsx accessibility-report.ts
|
||||
# Enter a URL when prompted
|
||||
```
|
||||
|
||||
## Full example: accessibility-report.ts
|
||||
|
||||
```typescript
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import { CopilotClient } from "@github/copilot-sdk";
|
||||
import * as readline from "node:readline";
|
||||
|
||||
// ============================================================================
|
||||
// Main Application
|
||||
// ============================================================================
|
||||
|
||||
async function main() {
|
||||
console.log("=== Accessibility Report Generator ===\n");
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const askQuestion = (query: string): Promise<string> =>
|
||||
new Promise((resolve) => rl.question(query, (answer) => resolve(answer.trim())));
|
||||
|
||||
let url = await askQuestion("Enter URL to analyze: ");
|
||||
|
||||
if (!url) {
|
||||
console.log("No URL provided. Exiting.");
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure URL has a scheme
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
console.log(`\nAnalyzing: ${url}`);
|
||||
console.log("Please wait...\n");
|
||||
|
||||
// Create Copilot client with Playwright MCP server
|
||||
const client = new CopilotClient();
|
||||
|
||||
const session = await client.createSession({
|
||||
model: "claude-opus-4.6",
|
||||
streaming: true,
|
||||
mcpServers: {
|
||||
playwright: {
|
||||
type: "local",
|
||||
command: "npx",
|
||||
args: ["@playwright/mcp@latest"],
|
||||
tools: ["*"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Set up streaming event handling
|
||||
let idleResolve: (() => void) | null = null;
|
||||
|
||||
session.on((event) => {
|
||||
if (event.type === "assistant.message.delta") {
|
||||
process.stdout.write(event.data.deltaContent ?? "");
|
||||
} else if (event.type === "session.idle") {
|
||||
idleResolve?.();
|
||||
} else if (event.type === "session.error") {
|
||||
console.error(`\nError: ${event.data.message}`);
|
||||
idleResolve?.();
|
||||
}
|
||||
});
|
||||
|
||||
const waitForIdle = (): Promise<void> =>
|
||||
new Promise((resolve) => {
|
||||
idleResolve = resolve;
|
||||
});
|
||||
|
||||
const prompt = `
|
||||
Use the Playwright MCP server to analyze the accessibility of this webpage: ${url}
|
||||
|
||||
Please:
|
||||
1. Navigate to the URL using playwright-browser_navigate
|
||||
2. Take an accessibility snapshot using playwright-browser_snapshot
|
||||
3. Analyze the snapshot and provide a detailed accessibility report
|
||||
|
||||
Format the report with emoji indicators:
|
||||
- 📊 Accessibility Report header
|
||||
- ✅ What's Working Well (table with Category, Status, Details)
|
||||
- ⚠️ Issues Found (table with Severity, Issue, WCAG Criterion, Recommendation)
|
||||
- 📋 Stats Summary (links, headings, focusable elements, landmarks)
|
||||
- ⚙️ Priority Recommendations
|
||||
|
||||
Use ✅ for pass, 🔴 for high severity issues, 🟡 for medium severity, ❌ for missing items.
|
||||
Include actual findings from the page analysis.
|
||||
`;
|
||||
|
||||
let idle = waitForIdle();
|
||||
await session.send({ prompt });
|
||||
await idle;
|
||||
|
||||
console.log("\n\n=== Report Complete ===\n");
|
||||
|
||||
// Prompt user for test generation
|
||||
const generateTests = await askQuestion(
|
||||
"Would you like to generate Playwright accessibility tests? (y/n): "
|
||||
);
|
||||
|
||||
if (generateTests.toLowerCase() === "y" || generateTests.toLowerCase() === "yes") {
|
||||
const detectLanguagePrompt = `
|
||||
Analyze the current working directory to detect the primary programming language.
|
||||
Respond with ONLY the detected language name and a brief explanation.
|
||||
If no project is detected, suggest "TypeScript" as the default.
|
||||
`;
|
||||
|
||||
console.log("\nDetecting project language...\n");
|
||||
idle = waitForIdle();
|
||||
await session.send({ prompt: detectLanguagePrompt });
|
||||
await idle;
|
||||
|
||||
let language = await askQuestion("\n\nConfirm language for tests (or enter a different one): ");
|
||||
if (!language) language = "TypeScript";
|
||||
|
||||
const testGenerationPrompt = `
|
||||
Based on the accessibility report you just generated for ${url},
|
||||
create Playwright accessibility tests in ${language}.
|
||||
|
||||
Include tests for: lang attribute, title, heading hierarchy, alt text,
|
||||
landmarks, skip navigation, focus indicators, and touch targets.
|
||||
Use Playwright's accessibility testing features with helpful comments.
|
||||
Output the complete test file.
|
||||
`;
|
||||
|
||||
console.log("\nGenerating accessibility tests...\n");
|
||||
idle = waitForIdle();
|
||||
await session.send({ prompt: testGenerationPrompt });
|
||||
await idle;
|
||||
|
||||
console.log("\n\n=== Tests Generated ===");
|
||||
}
|
||||
|
||||
rl.close();
|
||||
await session.destroy();
|
||||
await client.stop();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
1. **Playwright MCP server**: Configures a local MCP server running `@playwright/mcp` to provide browser automation tools
|
||||
2. **Streaming output**: Uses `streaming: true` and `assistant.message.delta` events for real-time token-by-token output
|
||||
3. **Accessibility snapshot**: Playwright's `browser_snapshot` tool captures the full accessibility tree of the page
|
||||
4. **Structured report**: The prompt engineers a consistent WCAG-aligned report format with emoji severity indicators
|
||||
5. **Test generation**: Optionally detects the project language and generates Playwright accessibility tests
|
||||
|
||||
## Key concepts
|
||||
|
||||
### MCP server configuration
|
||||
|
||||
The recipe configures a local MCP server that runs alongside the session:
|
||||
|
||||
```typescript
|
||||
const session = await client.createSession({
|
||||
mcpServers: {
|
||||
playwright: {
|
||||
type: "local",
|
||||
command: "npx",
|
||||
args: ["@playwright/mcp@latest"],
|
||||
tools: ["*"],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This gives the model access to Playwright browser tools like `browser_navigate`, `browser_snapshot`, and `browser_click`.
|
||||
|
||||
### Streaming with events
|
||||
|
||||
Unlike `sendAndWait`, this recipe uses streaming for real-time output:
|
||||
|
||||
```typescript
|
||||
session.on((event) => {
|
||||
if (event.type === "assistant.message.delta") {
|
||||
process.stdout.write(event.data.deltaContent ?? "");
|
||||
} else if (event.type === "session.idle") {
|
||||
idleResolve?.();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Sample interaction
|
||||
|
||||
```
|
||||
=== Accessibility Report Generator ===
|
||||
|
||||
Enter URL to analyze: github.com
|
||||
|
||||
Analyzing: https://github.com
|
||||
Please wait...
|
||||
|
||||
📊 Accessibility Report: GitHub (github.com)
|
||||
|
||||
✅ What's Working Well
|
||||
| Category | Status | Details |
|
||||
|----------|--------|---------|
|
||||
| Language | ✅ Pass | lang="en" properly set |
|
||||
| Page Title | ✅ Pass | "GitHub" is recognizable |
|
||||
| Heading Hierarchy | ✅ Pass | Proper H1/H2 structure |
|
||||
| Images | ✅ Pass | All images have alt text |
|
||||
|
||||
⚠️ Issues Found
|
||||
| Severity | Issue | WCAG Criterion | Recommendation |
|
||||
|----------|-------|----------------|----------------|
|
||||
| 🟡 Medium | Some links lack descriptive text | 2.4.4 | Add aria-label to icon-only links |
|
||||
|
||||
📋 Stats Summary
|
||||
- Total Links: 47
|
||||
- Total Headings: 8 (1× H1, proper hierarchy)
|
||||
- Focusable Elements: 52
|
||||
- Landmarks Found: banner ✅, navigation ✅, main ✅, footer ✅
|
||||
|
||||
=== Report Complete ===
|
||||
|
||||
Would you like to generate Playwright accessibility tests? (y/n): y
|
||||
|
||||
Detecting project language...
|
||||
TypeScript detected (package.json found)
|
||||
|
||||
Confirm language for tests (or enter a different one):
|
||||
|
||||
Generating accessibility tests...
|
||||
[Generated test file output...]
|
||||
|
||||
=== Tests Generated ===
|
||||
```
|
||||
238
cookbook/copilot-sdk/nodejs/ralph-loop.md
Normal file
238
cookbook/copilot-sdk/nodejs/ralph-loop.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# Ralph Loop: Autonomous AI Task Loops
|
||||
|
||||
Build autonomous coding loops where an AI agent picks tasks, implements them, validates against backpressure (tests, builds), commits, and repeats — each iteration in a fresh context window.
|
||||
|
||||
> **Runnable example:** [recipe/ralph-loop.ts](recipe/ralph-loop.ts)
|
||||
>
|
||||
> ```bash
|
||||
> npm install
|
||||
> npx tsx recipe/ralph-loop.ts
|
||||
> ```
|
||||
|
||||
## What is a Ralph Loop?
|
||||
|
||||
A [Ralph loop](https://ghuntley.com/ralph/) is an autonomous development workflow where an AI agent iterates through tasks in isolated context windows. The key insight: **state lives on disk, not in the model's context**. Each iteration starts fresh, reads the current state from files, does one task, writes results back to disk, and exits.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ loop.sh │
|
||||
│ while true: │
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ Fresh session (isolated context) │ │
|
||||
│ │ │ │
|
||||
│ │ 1. Read PROMPT.md + AGENTS.md │ │
|
||||
│ │ 2. Study specs/* and code │ │
|
||||
│ │ 3. Pick next task from plan │ │
|
||||
│ │ 4. Implement + run tests │ │
|
||||
│ │ 5. Update plan, commit, exit │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
│ ↻ next iteration (fresh context) │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Core principles:**
|
||||
|
||||
- **Fresh context per iteration**: Each loop creates a new session — no context accumulation, always in the "smart zone"
|
||||
- **Disk as shared state**: `IMPLEMENTATION_PLAN.md` persists between iterations and acts as the coordination mechanism
|
||||
- **Backpressure steers quality**: Tests, builds, and lints reject bad work — the agent must fix issues before committing
|
||||
- **Two modes**: PLANNING (gap analysis → generate plan) and BUILDING (implement from plan)
|
||||
|
||||
## Simple Version
|
||||
|
||||
The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | copilot ; done`:
|
||||
|
||||
```typescript
|
||||
import { readFile } from "fs/promises";
|
||||
import { CopilotClient } from "@github/copilot-sdk";
|
||||
|
||||
async function ralphLoop(promptFile: string, maxIterations: number = 50) {
|
||||
const client = new CopilotClient();
|
||||
await client.start();
|
||||
|
||||
try {
|
||||
const prompt = await readFile(promptFile, "utf-8");
|
||||
|
||||
for (let i = 1; i <= maxIterations; i++) {
|
||||
console.log(`\n=== Iteration ${i}/${maxIterations} ===`);
|
||||
|
||||
// Fresh session each iteration — context isolation is the point
|
||||
const session = await client.createSession({ model: "gpt-5.1-codex-mini" });
|
||||
try {
|
||||
await session.sendAndWait({ prompt }, 600_000);
|
||||
} finally {
|
||||
await session.destroy();
|
||||
}
|
||||
|
||||
console.log(`Iteration ${i} complete.`);
|
||||
}
|
||||
} finally {
|
||||
await client.stop();
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: point at your PROMPT.md
|
||||
ralphLoop("PROMPT.md", 20);
|
||||
```
|
||||
|
||||
This is all you need to get started. The prompt file tells the agent what to do; the agent reads project files, does work, commits, and exits. The loop restarts with a clean slate.
|
||||
|
||||
## Ideal Version
|
||||
|
||||
The full Ralph pattern with planning and building modes, matching the [Ralph Playbook](https://github.com/ClaytonFarr/ralph-playbook) architecture:
|
||||
|
||||
```typescript
|
||||
import { readFile } from "fs/promises";
|
||||
import { CopilotClient } from "@github/copilot-sdk";
|
||||
|
||||
type Mode = "plan" | "build";
|
||||
|
||||
async function ralphLoop(mode: Mode, maxIterations: number = 50) {
|
||||
const promptFile = mode === "plan" ? "PROMPT_plan.md" : "PROMPT_build.md";
|
||||
const client = new CopilotClient();
|
||||
await client.start();
|
||||
|
||||
console.log(`Mode: ${mode} | Prompt: ${promptFile}`);
|
||||
|
||||
try {
|
||||
const prompt = await readFile(promptFile, "utf-8");
|
||||
|
||||
for (let i = 1; i <= maxIterations; i++) {
|
||||
console.log(`\n=== Iteration ${i}/${maxIterations} ===`);
|
||||
|
||||
const session = await client.createSession({
|
||||
model: "gpt-5.1-codex-mini",
|
||||
// Pin the agent to the project directory
|
||||
workingDirectory: process.cwd(),
|
||||
// Auto-approve tool calls for unattended operation
|
||||
onPermissionRequest: async () => ({ allow: true }),
|
||||
});
|
||||
|
||||
// Log tool usage for visibility
|
||||
session.on((event) => {
|
||||
if (event.type === "tool.execution_start") {
|
||||
console.log(` ⚙ ${event.data.toolName}`);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await session.sendAndWait({ prompt }, 600_000);
|
||||
} finally {
|
||||
await session.destroy();
|
||||
}
|
||||
|
||||
console.log(`Iteration ${i} complete.`);
|
||||
}
|
||||
} finally {
|
||||
await client.stop();
|
||||
}
|
||||
}
|
||||
|
||||
// Parse CLI args: npx tsx ralph-loop.ts [plan] [max_iterations]
|
||||
const args = process.argv.slice(2);
|
||||
const mode: Mode = args.includes("plan") ? "plan" : "build";
|
||||
const maxArg = args.find((a) => /^\d+$/.test(a));
|
||||
const maxIterations = maxArg ? parseInt(maxArg) : 50;
|
||||
|
||||
ralphLoop(mode, maxIterations);
|
||||
```
|
||||
|
||||
### Required Project Files
|
||||
|
||||
The ideal version expects this file structure in your project:
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── PROMPT_plan.md # Planning mode instructions
|
||||
├── PROMPT_build.md # Building mode instructions
|
||||
├── AGENTS.md # Operational guide (build/test commands)
|
||||
├── IMPLEMENTATION_PLAN.md # Task list (generated by planning mode)
|
||||
├── specs/ # Requirement specs (one per topic)
|
||||
│ ├── auth.md
|
||||
│ └── data-pipeline.md
|
||||
└── src/ # Your source code
|
||||
```
|
||||
|
||||
### Example `PROMPT_plan.md`
|
||||
|
||||
```markdown
|
||||
0a. Study `specs/*` to learn the application specifications.
|
||||
0b. Study IMPLEMENTATION_PLAN.md (if present) to understand the plan so far.
|
||||
0c. Study `src/` to understand existing code and shared utilities.
|
||||
|
||||
1. Compare specs against code (gap analysis). Create or update
|
||||
IMPLEMENTATION_PLAN.md as a prioritized bullet-point list of tasks
|
||||
yet to be implemented. Do NOT implement anything.
|
||||
|
||||
IMPORTANT: Do NOT assume functionality is missing — search the
|
||||
codebase first to confirm. Prefer updating existing utilities over
|
||||
creating ad-hoc copies.
|
||||
```
|
||||
|
||||
### Example `PROMPT_build.md`
|
||||
|
||||
```markdown
|
||||
0a. Study `specs/*` to learn the application specifications.
|
||||
0b. Study IMPLEMENTATION_PLAN.md.
|
||||
0c. Study `src/` for reference.
|
||||
|
||||
1. Choose the most important item from IMPLEMENTATION_PLAN.md. Before
|
||||
making changes, search the codebase (don't assume not implemented).
|
||||
2. After implementing, run the tests. If functionality is missing, add it.
|
||||
3. When you discover issues, update IMPLEMENTATION_PLAN.md immediately.
|
||||
4. When tests pass, update IMPLEMENTATION_PLAN.md, then `git add -A`
|
||||
then `git commit` with a descriptive message.
|
||||
|
||||
5. When authoring documentation, capture the why.
|
||||
6. Implement completely. No placeholders or stubs.
|
||||
7. Keep IMPLEMENTATION_PLAN.md current — future iterations depend on it.
|
||||
```
|
||||
|
||||
### Example `AGENTS.md`
|
||||
|
||||
Keep this brief (~60 lines). It's loaded every iteration, so bloat wastes context.
|
||||
|
||||
```markdown
|
||||
## Build & Run
|
||||
|
||||
npm run build
|
||||
|
||||
## Validation
|
||||
|
||||
- Tests: `npm test`
|
||||
- Typecheck: `npx tsc --noEmit`
|
||||
- Lint: `npm run lint`
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Fresh context per iteration**: Never accumulate context across iterations — that's the whole point
|
||||
2. **Disk is your database**: `IMPLEMENTATION_PLAN.md` is shared state between isolated sessions
|
||||
3. **Backpressure is essential**: Tests, builds, lints in `AGENTS.md` — the agent must pass them before committing
|
||||
4. **Start with PLANNING mode**: Generate the plan first, then switch to BUILDING
|
||||
5. **Observe and tune**: Watch early iterations, add guardrails to prompts when the agent fails in specific ways
|
||||
6. **The plan is disposable**: If the agent goes off track, delete `IMPLEMENTATION_PLAN.md` and re-plan
|
||||
7. **Keep `AGENTS.md` brief**: It's loaded every iteration — operational info only, no progress notes
|
||||
8. **Use a sandbox**: The agent runs autonomously with full tool access — isolate it
|
||||
9. **Set `workingDirectory`**: Pin the session to your project root so tool operations resolve paths correctly
|
||||
10. **Auto-approve permissions**: Use `onPermissionRequest` to allow tool calls without interrupting the loop
|
||||
|
||||
## When to Use a Ralph Loop
|
||||
|
||||
**Good for:**
|
||||
|
||||
- Implementing features from specs with test-driven validation
|
||||
- Large refactors broken into many small tasks
|
||||
- Unattended, long-running development with clear requirements
|
||||
- Any work where backpressure (tests/builds) can verify correctness
|
||||
|
||||
**Not good for:**
|
||||
|
||||
- Tasks requiring human judgment mid-loop
|
||||
- One-shot operations that don't benefit from iteration
|
||||
- Vague requirements without testable acceptance criteria
|
||||
- Exploratory prototyping where direction isn't clear
|
||||
|
||||
## See Also
|
||||
|
||||
- [Error Handling](error-handling.md) — timeout patterns and graceful shutdown for long-running sessions
|
||||
- [Persisting Sessions](persisting-sessions.md) — save and resume sessions across restarts
|
||||
187
cookbook/copilot-sdk/nodejs/recipe/accessibility-report.ts
Normal file
187
cookbook/copilot-sdk/nodejs/recipe/accessibility-report.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { CopilotClient } from "@github/copilot-sdk";
|
||||
import * as readline from "node:readline";
|
||||
|
||||
// ============================================================================
|
||||
// Main Application
|
||||
// ============================================================================
|
||||
|
||||
async function main() {
|
||||
console.log("=== Accessibility Report Generator ===\n");
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const askQuestion = (query: string): Promise<string> =>
|
||||
new Promise((resolve) => rl.question(query, (answer) => resolve(answer.trim())));
|
||||
|
||||
let url = await askQuestion("Enter URL to analyze: ");
|
||||
|
||||
if (!url) {
|
||||
console.log("No URL provided. Exiting.");
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure URL has a scheme
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
console.log(`\nAnalyzing: ${url}`);
|
||||
console.log("Please wait...\n");
|
||||
|
||||
// Create Copilot client with Playwright MCP server
|
||||
const client = new CopilotClient();
|
||||
|
||||
const session = await client.createSession({
|
||||
model: "claude-opus-4.6",
|
||||
streaming: true,
|
||||
mcpServers: {
|
||||
playwright: {
|
||||
type: "local",
|
||||
command: "npx",
|
||||
args: ["@playwright/mcp@latest"],
|
||||
tools: ["*"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Set up streaming event handling
|
||||
let idleResolve: (() => void) | null = null;
|
||||
|
||||
session.on((event) => {
|
||||
if (event.type === "assistant.message.delta") {
|
||||
process.stdout.write(event.data.deltaContent ?? "");
|
||||
} else if (event.type === "session.idle") {
|
||||
idleResolve?.();
|
||||
} else if (event.type === "session.error") {
|
||||
console.error(`\nError: ${event.data.message}`);
|
||||
idleResolve?.();
|
||||
}
|
||||
});
|
||||
|
||||
const waitForIdle = (): Promise<void> =>
|
||||
new Promise((resolve) => {
|
||||
idleResolve = resolve;
|
||||
});
|
||||
|
||||
const prompt = `
|
||||
Use the Playwright MCP server to analyze the accessibility of this webpage: ${url}
|
||||
|
||||
Please:
|
||||
1. Navigate to the URL using playwright-browser_navigate
|
||||
2. Take an accessibility snapshot using playwright-browser_snapshot
|
||||
3. Analyze the snapshot and provide a detailed accessibility report
|
||||
|
||||
Format the report EXACTLY like this structure with emoji indicators:
|
||||
|
||||
📊 Accessibility Report: [Page Title] (domain.com)
|
||||
|
||||
✅ What's Working Well
|
||||
| Category | Status | Details |
|
||||
|----------|--------|---------|
|
||||
| Language | ✅ Pass | lang="en-US" properly set |
|
||||
| Page Title | ✅ Pass | "[Title]" is descriptive |
|
||||
| Heading Hierarchy | ✅ Pass | Single H1, proper H2/H3 structure |
|
||||
| Images | ✅ Pass | All X images have alt text |
|
||||
| Viewport | ✅ Pass | Allows pinch-zoom (no user-scalable=no) |
|
||||
| Links | ✅ Pass | No ambiguous "click here" links |
|
||||
| Reduced Motion | ✅ Pass | Supports prefers-reduced-motion |
|
||||
| Autoplay Media | ✅ Pass | No autoplay audio/video |
|
||||
|
||||
⚠️ Issues Found
|
||||
| Severity | Issue | WCAG Criterion | Recommendation |
|
||||
|----------|-------|----------------|----------------|
|
||||
| 🔴 High | No <main> landmark | 1.3.1, 2.4.1 | Wrap main content in <main> element |
|
||||
| 🔴 High | No skip navigation link | 2.4.1 | Add "Skip to content" link at top |
|
||||
| 🟡 Medium | Focus outlines disabled | 2.4.7 | Default outline is none - ensure visible :focus styles exist |
|
||||
| 🟡 Medium | Small touch targets | 2.5.8 | Navigation links are 37px tall (below 44px minimum) |
|
||||
|
||||
📋 Stats Summary
|
||||
- Total Links: X
|
||||
- Total Headings: X (1× H1, proper hierarchy)
|
||||
- Focusable Elements: X
|
||||
- Landmarks Found: banner ✅, navigation ✅, main ❌, footer ✅
|
||||
|
||||
⚙️ Priority Recommendations
|
||||
- Add <main> landmark - Wrap page content in <main role="main"> for screen reader navigation
|
||||
- Add skip link - Hidden link at start: <a href="#main-content" class="skip-link">Skip to content</a>
|
||||
- Increase touch targets - Add padding to nav links and tags to meet 44×44px minimum
|
||||
- Verify focus styles - Test keyboard navigation; add visible :focus or :focus-visible outlines
|
||||
|
||||
Use ✅ for pass, 🔴 for high severity issues, 🟡 for medium severity, ❌ for missing items.
|
||||
Include actual findings from the page analysis - don't just copy the example.
|
||||
`;
|
||||
|
||||
let idle = waitForIdle();
|
||||
await session.send({ prompt });
|
||||
await idle;
|
||||
|
||||
console.log("\n\n=== Report Complete ===\n");
|
||||
|
||||
// Prompt user for test generation
|
||||
const generateTests = await askQuestion(
|
||||
"Would you like to generate Playwright accessibility tests? (y/n): "
|
||||
);
|
||||
|
||||
if (generateTests.toLowerCase() === "y" || generateTests.toLowerCase() === "yes") {
|
||||
const detectLanguagePrompt = `
|
||||
Analyze the current working directory to detect the primary programming language used in this project.
|
||||
Look for project files like package.json, *.csproj, pom.xml, requirements.txt, go.mod, etc.
|
||||
|
||||
Respond with ONLY the detected language name (e.g., "TypeScript", "JavaScript", "C#", "Python", "Java")
|
||||
and a brief explanation of why you detected it.
|
||||
If no project is detected, suggest "TypeScript" as the default for Playwright tests.
|
||||
`;
|
||||
|
||||
console.log("\nDetecting project language...\n");
|
||||
idle = waitForIdle();
|
||||
await session.send({ prompt: detectLanguagePrompt });
|
||||
await idle;
|
||||
|
||||
let language = await askQuestion("\n\nConfirm language for tests (or enter a different one): ");
|
||||
if (!language) {
|
||||
language = "TypeScript";
|
||||
}
|
||||
|
||||
const testGenerationPrompt = `
|
||||
Based on the accessibility report you just generated for ${url}, create Playwright accessibility tests in ${language}.
|
||||
|
||||
The tests should:
|
||||
1. Verify all the accessibility checks from the report
|
||||
2. Test for the issues that were found (to ensure they get fixed)
|
||||
3. Include tests for:
|
||||
- Page has proper lang attribute
|
||||
- Page has descriptive title
|
||||
- Heading hierarchy is correct (single H1, proper nesting)
|
||||
- All images have alt text
|
||||
- No autoplay media
|
||||
- Landmark regions exist (banner, nav, main, footer)
|
||||
- Skip navigation link exists and works
|
||||
- Focus indicators are visible
|
||||
- Touch targets meet minimum size requirements
|
||||
4. Use Playwright's accessibility testing features
|
||||
5. Include helpful comments explaining each test
|
||||
|
||||
Output the complete test file that can be saved and run.
|
||||
Use the Playwright MCP server tools if you need to verify any page details.
|
||||
`;
|
||||
|
||||
console.log("\nGenerating accessibility tests...\n");
|
||||
idle = waitForIdle();
|
||||
await session.send({ prompt: testGenerationPrompt });
|
||||
await idle;
|
||||
|
||||
console.log("\n\n=== Tests Generated ===");
|
||||
}
|
||||
|
||||
rl.close();
|
||||
await session.destroy();
|
||||
await client.stop();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -8,7 +8,7 @@
|
||||
"name": "copilot-sdk-cookbook-recipes",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "file:../../src"
|
||||
"@github/copilot-sdk": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.19.7",
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"multiple-sessions": "tsx multiple-sessions.ts",
|
||||
"managing-local-files": "tsx managing-local-files.ts",
|
||||
"pr-visualization": "tsx pr-visualization.ts",
|
||||
"persisting-sessions": "tsx persisting-sessions.ts"
|
||||
"persisting-sessions": "tsx persisting-sessions.ts",
|
||||
"accessibility-report": "tsx accessibility-report.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "*"
|
||||
|
||||
78
cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts
Normal file
78
cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { readFile } from "fs/promises";
|
||||
import { CopilotClient } from "@github/copilot-sdk";
|
||||
|
||||
/**
|
||||
* Ralph loop: autonomous AI task loop with fresh context per iteration.
|
||||
*
|
||||
* Two modes:
|
||||
* - "plan": reads PROMPT_plan.md, generates/updates IMPLEMENTATION_PLAN.md
|
||||
* - "build": reads PROMPT_build.md, implements tasks, runs tests, commits
|
||||
*
|
||||
* Each iteration creates a fresh session so the agent always operates in
|
||||
* the "smart zone" of its context window. State is shared between
|
||||
* iterations via files on disk (IMPLEMENTATION_PLAN.md, AGENTS.md, specs/*).
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx ralph-loop.ts # build mode, 50 iterations
|
||||
* npx tsx ralph-loop.ts plan # planning mode
|
||||
* npx tsx ralph-loop.ts 20 # build mode, 20 iterations
|
||||
* npx tsx ralph-loop.ts plan 5 # planning mode, 5 iterations
|
||||
*/
|
||||
|
||||
type Mode = "plan" | "build";
|
||||
|
||||
async function ralphLoop(mode: Mode, maxIterations: number) {
|
||||
const promptFile = mode === "plan" ? "PROMPT_plan.md" : "PROMPT_build.md";
|
||||
|
||||
const client = new CopilotClient();
|
||||
await client.start();
|
||||
|
||||
console.log("━".repeat(40));
|
||||
console.log(`Mode: ${mode}`);
|
||||
console.log(`Prompt: ${promptFile}`);
|
||||
console.log(`Max: ${maxIterations} iterations`);
|
||||
console.log("━".repeat(40));
|
||||
|
||||
try {
|
||||
const prompt = await readFile(promptFile, "utf-8");
|
||||
|
||||
for (let i = 1; i <= maxIterations; i++) {
|
||||
console.log(`\n=== Iteration ${i}/${maxIterations} ===`);
|
||||
|
||||
const session = await client.createSession({
|
||||
model: "gpt-5.1-codex-mini",
|
||||
// Pin the agent to the project directory
|
||||
workingDirectory: process.cwd(),
|
||||
// Auto-approve tool calls for unattended operation
|
||||
onPermissionRequest: async () => ({ allow: true }),
|
||||
});
|
||||
|
||||
// Log tool usage for visibility
|
||||
session.on((event) => {
|
||||
if (event.type === "tool.execution_start") {
|
||||
console.log(` ⚙ ${event.data.toolName}`);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await session.sendAndWait({ prompt }, 600_000);
|
||||
} finally {
|
||||
await session.destroy();
|
||||
}
|
||||
|
||||
console.log(`\nIteration ${i} complete.`);
|
||||
}
|
||||
|
||||
console.log(`\nReached max iterations: ${maxIterations}`);
|
||||
} finally {
|
||||
await client.stop();
|
||||
}
|
||||
}
|
||||
|
||||
// Parse CLI args
|
||||
const args = process.argv.slice(2);
|
||||
const mode: Mode = args.includes("plan") ? "plan" : "build";
|
||||
const maxArg = args.find((a) => /^\d+$/.test(a));
|
||||
const maxIterations = maxArg ? parseInt(maxArg) : 50;
|
||||
|
||||
ralphLoop(mode, maxIterations).catch(console.error);
|
||||
253
cookbook/copilot-sdk/python/accessibility-report.md
Normal file
253
cookbook/copilot-sdk/python/accessibility-report.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# Generating Accessibility Reports
|
||||
|
||||
Build a CLI tool that analyzes web page accessibility using the Playwright MCP server and generates detailed WCAG-compliant reports with optional test generation.
|
||||
|
||||
> **Runnable example:** [recipe/accessibility_report.py](recipe/accessibility_report.py)
|
||||
>
|
||||
> ```bash
|
||||
> cd recipe && pip install -r requirements.txt
|
||||
> python accessibility_report.py
|
||||
> ```
|
||||
|
||||
## Example scenario
|
||||
|
||||
You want to audit a website's accessibility compliance. This tool navigates to a URL using Playwright, captures an accessibility snapshot, and produces a structured report covering WCAG criteria like landmarks, heading hierarchy, focus management, and touch targets. It can also generate Playwright test files to automate future accessibility checks.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
pip install github-copilot-sdk
|
||||
```
|
||||
|
||||
You also need `npx` available (Node.js installed) for the Playwright MCP server.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
python accessibility_report.py
|
||||
# Enter a URL when prompted
|
||||
```
|
||||
|
||||
## Full example: accessibility_report.py
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
from copilot import (
|
||||
CopilotClient, SessionConfig, MessageOptions,
|
||||
SessionEvent, SessionEventType,
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Main Application
|
||||
# ============================================================================
|
||||
|
||||
async def main():
|
||||
print("=== Accessibility Report Generator ===\n")
|
||||
|
||||
url = input("Enter URL to analyze: ").strip()
|
||||
|
||||
if not url:
|
||||
print("No URL provided. Exiting.")
|
||||
return
|
||||
|
||||
# Ensure URL has a scheme
|
||||
if not url.startswith("http://") and not url.startswith("https://"):
|
||||
url = "https://" + url
|
||||
|
||||
print(f"\nAnalyzing: {url}")
|
||||
print("Please wait...\n")
|
||||
|
||||
# Create Copilot client with Playwright MCP server
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
session = await client.create_session(SessionConfig(
|
||||
model="claude-opus-4.6",
|
||||
streaming=True,
|
||||
mcp_servers={
|
||||
"playwright": {
|
||||
"type": "local",
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest"],
|
||||
"tools": ["*"],
|
||||
}
|
||||
},
|
||||
))
|
||||
|
||||
done = asyncio.Event()
|
||||
|
||||
# Set up streaming event handling
|
||||
def handle_event(event: SessionEvent):
|
||||
if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA:
|
||||
print(event.data.delta_content or "", end="", flush=True)
|
||||
elif event.type.value == "session.idle":
|
||||
done.set()
|
||||
elif event.type.value == "session.error":
|
||||
print(f"\nError: {event.data.message}")
|
||||
done.set()
|
||||
|
||||
session.on(handle_event)
|
||||
|
||||
prompt = f"""
|
||||
Use the Playwright MCP server to analyze the accessibility of this webpage: {url}
|
||||
|
||||
Please:
|
||||
1. Navigate to the URL using playwright-browser_navigate
|
||||
2. Take an accessibility snapshot using playwright-browser_snapshot
|
||||
3. Analyze the snapshot and provide a detailed accessibility report
|
||||
|
||||
Format the report with emoji indicators:
|
||||
- 📊 Accessibility Report header
|
||||
- ✅ What's Working Well (table with Category, Status, Details)
|
||||
- ⚠️ Issues Found (table with Severity, Issue, WCAG Criterion, Recommendation)
|
||||
- 📋 Stats Summary (links, headings, focusable elements, landmarks)
|
||||
- ⚙️ Priority Recommendations
|
||||
|
||||
Use ✅ for pass, 🔴 for high severity issues, 🟡 for medium severity, ❌ for missing items.
|
||||
Include actual findings from the page analysis.
|
||||
"""
|
||||
|
||||
await session.send(MessageOptions(prompt=prompt))
|
||||
await done.wait()
|
||||
|
||||
print("\n\n=== Report Complete ===\n")
|
||||
|
||||
# Prompt user for test generation
|
||||
generate_tests = input(
|
||||
"Would you like to generate Playwright accessibility tests? (y/n): "
|
||||
).strip().lower()
|
||||
|
||||
if generate_tests in ("y", "yes"):
|
||||
done.clear()
|
||||
|
||||
detect_language_prompt = """
|
||||
Analyze the current working directory to detect the primary programming language.
|
||||
Respond with ONLY the detected language name and a brief explanation.
|
||||
If no project is detected, suggest "TypeScript" as the default.
|
||||
"""
|
||||
|
||||
print("\nDetecting project language...\n")
|
||||
await session.send(MessageOptions(prompt=detect_language_prompt))
|
||||
await done.wait()
|
||||
|
||||
language = input(
|
||||
"\n\nConfirm language for tests (or enter a different one): "
|
||||
).strip()
|
||||
if not language:
|
||||
language = "TypeScript"
|
||||
|
||||
done.clear()
|
||||
|
||||
test_generation_prompt = f"""
|
||||
Based on the accessibility report you just generated for {url},
|
||||
create Playwright accessibility tests in {language}.
|
||||
|
||||
Include tests for: lang attribute, title, heading hierarchy, alt text,
|
||||
landmarks, skip navigation, focus indicators, and touch targets.
|
||||
Use Playwright's accessibility testing features with helpful comments.
|
||||
Output the complete test file.
|
||||
"""
|
||||
|
||||
print("\nGenerating accessibility tests...\n")
|
||||
await session.send(MessageOptions(prompt=test_generation_prompt))
|
||||
await done.wait()
|
||||
|
||||
print("\n\n=== Tests Generated ===")
|
||||
|
||||
await session.destroy()
|
||||
await client.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
1. **Playwright MCP server**: Configures a local MCP server running `@playwright/mcp` to provide browser automation tools
|
||||
2. **Streaming output**: Uses `streaming=True` and `ASSISTANT_MESSAGE_DELTA` events for real-time token-by-token output
|
||||
3. **Accessibility snapshot**: Playwright's `browser_snapshot` tool captures the full accessibility tree of the page
|
||||
4. **Structured report**: The prompt engineers a consistent WCAG-aligned report format with emoji severity indicators
|
||||
5. **Test generation**: Optionally detects the project language and generates Playwright accessibility tests
|
||||
|
||||
## Key concepts
|
||||
|
||||
### MCP server configuration
|
||||
|
||||
The recipe configures a local MCP server that runs alongside the session:
|
||||
|
||||
```python
|
||||
session = await client.create_session(SessionConfig(
|
||||
mcp_servers={
|
||||
"playwright": {
|
||||
"type": "local",
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest"],
|
||||
"tools": ["*"],
|
||||
}
|
||||
},
|
||||
))
|
||||
```
|
||||
|
||||
This gives the model access to Playwright browser tools like `browser_navigate`, `browser_snapshot`, and `browser_click`.
|
||||
|
||||
### Streaming with events
|
||||
|
||||
Unlike `send_and_wait`, this recipe uses streaming for real-time output:
|
||||
|
||||
```python
|
||||
def handle_event(event: SessionEvent):
|
||||
if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA:
|
||||
print(event.data.delta_content or "", end="", flush=True)
|
||||
elif event.type.value == "session.idle":
|
||||
done.set()
|
||||
|
||||
session.on(handle_event)
|
||||
```
|
||||
|
||||
## Sample interaction
|
||||
|
||||
```
|
||||
=== Accessibility Report Generator ===
|
||||
|
||||
Enter URL to analyze: github.com
|
||||
|
||||
Analyzing: https://github.com
|
||||
Please wait...
|
||||
|
||||
📊 Accessibility Report: GitHub (github.com)
|
||||
|
||||
✅ What's Working Well
|
||||
| Category | Status | Details |
|
||||
|----------|--------|---------|
|
||||
| Language | ✅ Pass | lang="en" properly set |
|
||||
| Page Title | ✅ Pass | "GitHub" is recognizable |
|
||||
| Heading Hierarchy | ✅ Pass | Proper H1/H2 structure |
|
||||
| Images | ✅ Pass | All images have alt text |
|
||||
|
||||
⚠️ Issues Found
|
||||
| Severity | Issue | WCAG Criterion | Recommendation |
|
||||
|----------|-------|----------------|----------------|
|
||||
| 🟡 Medium | Some links lack descriptive text | 2.4.4 | Add aria-label to icon-only links |
|
||||
|
||||
📋 Stats Summary
|
||||
- Total Links: 47
|
||||
- Total Headings: 8 (1× H1, proper hierarchy)
|
||||
- Focusable Elements: 52
|
||||
- Landmarks Found: banner ✅, navigation ✅, main ✅, footer ✅
|
||||
|
||||
=== Report Complete ===
|
||||
|
||||
Would you like to generate Playwright accessibility tests? (y/n): y
|
||||
|
||||
Detecting project language...
|
||||
TypeScript detected (package.json found)
|
||||
|
||||
Confirm language for tests (or enter a different one):
|
||||
|
||||
Generating accessibility tests...
|
||||
[Generated test file output...]
|
||||
|
||||
=== Tests Generated ===
|
||||
```
|
||||
@@ -16,41 +16,36 @@ You need to handle various error conditions like connection failures, timeouts,
|
||||
## Basic try-except
|
||||
|
||||
```python
|
||||
from copilot import CopilotClient
|
||||
import asyncio
|
||||
from copilot import CopilotClient, SessionConfig, MessageOptions
|
||||
|
||||
client = CopilotClient()
|
||||
async def main():
|
||||
client = CopilotClient()
|
||||
|
||||
try:
|
||||
client.start()
|
||||
session = client.create_session(model="gpt-5")
|
||||
try:
|
||||
await client.start()
|
||||
session = await client.create_session(SessionConfig(model="gpt-5"))
|
||||
|
||||
response = None
|
||||
def handle_message(event):
|
||||
nonlocal response
|
||||
if event["type"] == "assistant.message":
|
||||
response = event["data"]["content"]
|
||||
response = await session.send_and_wait(MessageOptions(prompt="Hello!"))
|
||||
|
||||
session.on(handle_message)
|
||||
session.send(prompt="Hello!")
|
||||
session.wait_for_idle()
|
||||
if response:
|
||||
print(response.data.content)
|
||||
|
||||
if response:
|
||||
print(response)
|
||||
await session.destroy()
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
await client.stop()
|
||||
|
||||
session.destroy()
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
client.stop()
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Handling specific error types
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
client.start()
|
||||
await client.start()
|
||||
except FileNotFoundError:
|
||||
print("Copilot CLI not found. Please install it first.")
|
||||
except ConnectionError:
|
||||
@@ -62,31 +57,14 @@ except Exception as e:
|
||||
## Timeout handling
|
||||
|
||||
```python
|
||||
import signal
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def timeout(seconds):
|
||||
def timeout_handler(signum, frame):
|
||||
raise TimeoutError("Request timed out")
|
||||
|
||||
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
|
||||
signal.alarm(seconds)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
signal.signal(signal.SIGALRM, old_handler)
|
||||
|
||||
session = client.create_session(model="gpt-5")
|
||||
session = await client.create_session(SessionConfig(model="gpt-5"))
|
||||
|
||||
try:
|
||||
session.send(prompt="Complex question...")
|
||||
|
||||
# Wait with timeout (30 seconds)
|
||||
with timeout(30):
|
||||
session.wait_for_idle()
|
||||
|
||||
# send_and_wait accepts an optional timeout in seconds
|
||||
response = await session.send_and_wait(
|
||||
MessageOptions(prompt="Complex question..."),
|
||||
timeout=30.0
|
||||
)
|
||||
print("Response received")
|
||||
except TimeoutError:
|
||||
print("Request timed out")
|
||||
@@ -95,21 +73,15 @@ except TimeoutError:
|
||||
## Aborting a request
|
||||
|
||||
```python
|
||||
import threading
|
||||
session = await client.create_session(SessionConfig(model="gpt-5"))
|
||||
|
||||
session = client.create_session(model="gpt-5")
|
||||
|
||||
# Start a request
|
||||
session.send(prompt="Write a very long story...")
|
||||
# Start a request (non-blocking send)
|
||||
await session.send(MessageOptions(prompt="Write a very long story..."))
|
||||
|
||||
# Abort it after some condition
|
||||
def abort_later():
|
||||
import time
|
||||
time.sleep(5)
|
||||
session.abort()
|
||||
print("Request aborted")
|
||||
|
||||
threading.Thread(target=abort_later).start()
|
||||
await asyncio.sleep(5)
|
||||
await session.abort()
|
||||
print("Request aborted")
|
||||
```
|
||||
|
||||
## Graceful shutdown
|
||||
@@ -120,31 +92,19 @@ import sys
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print("\nShutting down...")
|
||||
errors = client.stop()
|
||||
if errors:
|
||||
print(f"Cleanup errors: {errors}")
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.create_task(client.stop())
|
||||
except RuntimeError:
|
||||
asyncio.run(client.stop())
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
```
|
||||
|
||||
## Context manager for automatic cleanup
|
||||
|
||||
```python
|
||||
from copilot import CopilotClient
|
||||
|
||||
with CopilotClient() as client:
|
||||
client.start()
|
||||
session = client.create_session(model="gpt-5")
|
||||
|
||||
# ... do work ...
|
||||
|
||||
# client.stop() is automatically called when exiting context
|
||||
```
|
||||
|
||||
## Best practices
|
||||
|
||||
1. **Always clean up**: Use try-finally or context managers to ensure `stop()` is called
|
||||
1. **Always clean up**: Use try-finally to ensure `await client.stop()` is called
|
||||
2. **Handle connection errors**: The CLI might not be installed or running
|
||||
3. **Set appropriate timeouts**: Long-running requests should have timeouts
|
||||
3. **Set appropriate timeouts**: Use the `timeout` parameter on `send_and_wait()`
|
||||
4. **Log errors**: Capture error details for debugging
|
||||
|
||||
@@ -16,31 +16,40 @@ You have a folder with many files and want to organize them into subfolders base
|
||||
## Example code
|
||||
|
||||
```python
|
||||
from copilot import CopilotClient
|
||||
import asyncio
|
||||
import os
|
||||
from copilot import (
|
||||
CopilotClient, SessionConfig, MessageOptions,
|
||||
SessionEvent, SessionEventType,
|
||||
)
|
||||
|
||||
# Create and start client
|
||||
client = CopilotClient()
|
||||
client.start()
|
||||
async def main():
|
||||
# Create and start client
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
# Create session
|
||||
session = client.create_session(model="gpt-5")
|
||||
# Create session
|
||||
session = await client.create_session(SessionConfig(model="gpt-5"))
|
||||
|
||||
# Event handler
|
||||
def handle_event(event):
|
||||
if event["type"] == "assistant.message":
|
||||
print(f"\nCopilot: {event['data']['content']}")
|
||||
elif event["type"] == "tool.execution_start":
|
||||
print(f" → Running: {event['data']['toolName']}")
|
||||
elif event["type"] == "tool.execution_complete":
|
||||
print(f" ✓ Completed: {event['data']['toolCallId']}")
|
||||
done = asyncio.Event()
|
||||
|
||||
session.on(handle_event)
|
||||
# Event handler
|
||||
def handle_event(event: SessionEvent):
|
||||
if event.type == SessionEventType.ASSISTANT_MESSAGE:
|
||||
print(f"\nCopilot: {event.data.content}")
|
||||
elif event.type == SessionEventType.TOOL_EXECUTION_START:
|
||||
print(f" → Running: {event.data.tool_name}")
|
||||
elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE:
|
||||
print(f" ✓ Completed: {event.data.tool_call_id}")
|
||||
elif event.type.value == "session.idle":
|
||||
done.set()
|
||||
|
||||
# Ask Copilot to organize files
|
||||
target_folder = os.path.expanduser("~/Downloads")
|
||||
session.on(handle_event)
|
||||
|
||||
session.send(prompt=f"""
|
||||
# Ask Copilot to organize files
|
||||
target_folder = os.path.expanduser("~/Downloads")
|
||||
|
||||
await session.send(MessageOptions(prompt=f"""
|
||||
Analyze the files in "{target_folder}" and organize them into subfolders.
|
||||
|
||||
1. First, list all files and their metadata
|
||||
@@ -49,11 +58,15 @@ Analyze the files in "{target_folder}" and organize them into subfolders.
|
||||
4. Move each file to its appropriate subfolder
|
||||
|
||||
Please confirm before moving any files.
|
||||
""")
|
||||
"""))
|
||||
|
||||
session.wait_for_idle()
|
||||
await done.wait()
|
||||
|
||||
client.stop()
|
||||
await session.destroy()
|
||||
await client.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Grouping strategies
|
||||
@@ -90,10 +103,10 @@ client.stop()
|
||||
For safety, you can ask Copilot to only preview changes:
|
||||
|
||||
```python
|
||||
session.send(prompt=f"""
|
||||
await session.send(MessageOptions(prompt=f"""
|
||||
Analyze files in "{target_folder}" and show me how you would organize them
|
||||
by file type. DO NOT move any files - just show me the plan.
|
||||
""")
|
||||
"""))
|
||||
```
|
||||
|
||||
## Custom grouping with AI analysis
|
||||
@@ -101,7 +114,7 @@ by file type. DO NOT move any files - just show me the plan.
|
||||
Let Copilot determine the best grouping based on file content:
|
||||
|
||||
```python
|
||||
session.send(prompt=f"""
|
||||
await session.send(MessageOptions(prompt=f"""
|
||||
Look at the files in "{target_folder}" and suggest a logical organization.
|
||||
Consider:
|
||||
- File names and what they might contain
|
||||
@@ -109,7 +122,7 @@ Consider:
|
||||
- Date patterns that might indicate projects or events
|
||||
|
||||
Propose folder names that are descriptive and useful.
|
||||
""")
|
||||
"""))
|
||||
```
|
||||
|
||||
## Safety considerations
|
||||
|
||||
@@ -16,31 +16,36 @@ You need to run multiple conversations in parallel, each with its own context an
|
||||
## Python
|
||||
|
||||
```python
|
||||
from copilot import CopilotClient
|
||||
import asyncio
|
||||
from copilot import CopilotClient, SessionConfig, MessageOptions
|
||||
|
||||
client = CopilotClient()
|
||||
client.start()
|
||||
async def main():
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
# Create multiple independent sessions
|
||||
session1 = client.create_session(model="gpt-5")
|
||||
session2 = client.create_session(model="gpt-5")
|
||||
session3 = client.create_session(model="claude-sonnet-4.5")
|
||||
# Create multiple independent sessions
|
||||
session1 = await client.create_session(SessionConfig(model="gpt-5"))
|
||||
session2 = await client.create_session(SessionConfig(model="gpt-5"))
|
||||
session3 = await client.create_session(SessionConfig(model="claude-sonnet-4.5"))
|
||||
|
||||
# Each session maintains its own conversation history
|
||||
session1.send(prompt="You are helping with a Python project")
|
||||
session2.send(prompt="You are helping with a TypeScript project")
|
||||
session3.send(prompt="You are helping with a Go project")
|
||||
# Each session maintains its own conversation history
|
||||
await session1.send(MessageOptions(prompt="You are helping with a Python project"))
|
||||
await session2.send(MessageOptions(prompt="You are helping with a TypeScript project"))
|
||||
await session3.send(MessageOptions(prompt="You are helping with a Go project"))
|
||||
|
||||
# Follow-up messages stay in their respective contexts
|
||||
session1.send(prompt="How do I create a virtual environment?")
|
||||
session2.send(prompt="How do I set up tsconfig?")
|
||||
session3.send(prompt="How do I initialize a module?")
|
||||
# Follow-up messages stay in their respective contexts
|
||||
await session1.send(MessageOptions(prompt="How do I create a virtual environment?"))
|
||||
await session2.send(MessageOptions(prompt="How do I set up tsconfig?"))
|
||||
await session3.send(MessageOptions(prompt="How do I initialize a module?"))
|
||||
|
||||
# Clean up all sessions
|
||||
session1.destroy()
|
||||
session2.destroy()
|
||||
session3.destroy()
|
||||
client.stop()
|
||||
# Clean up all sessions
|
||||
await session1.destroy()
|
||||
await session2.destroy()
|
||||
await session3.destroy()
|
||||
await client.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Custom session IDs
|
||||
@@ -48,10 +53,10 @@ client.stop()
|
||||
Use custom IDs for easier tracking:
|
||||
|
||||
```python
|
||||
session = client.create_session(
|
||||
session = await client.create_session(SessionConfig(
|
||||
session_id="user-123-chat",
|
||||
model="gpt-5"
|
||||
)
|
||||
))
|
||||
|
||||
print(session.session_id) # "user-123-chat"
|
||||
```
|
||||
@@ -59,16 +64,16 @@ print(session.session_id) # "user-123-chat"
|
||||
## Listing sessions
|
||||
|
||||
```python
|
||||
sessions = client.list_sessions()
|
||||
sessions = await client.list_sessions()
|
||||
for session_info in sessions:
|
||||
print(f"Session: {session_info['sessionId']}")
|
||||
print(f"Session: {session_info.session_id}")
|
||||
```
|
||||
|
||||
## Deleting sessions
|
||||
|
||||
```python
|
||||
# Delete a specific session
|
||||
client.delete_session("user-123-chat")
|
||||
await client.delete_session("user-123-chat")
|
||||
```
|
||||
|
||||
## Use cases
|
||||
|
||||
@@ -16,64 +16,69 @@ You want users to be able to continue a conversation even after closing and reop
|
||||
### Creating a session with a custom ID
|
||||
|
||||
```python
|
||||
from copilot import CopilotClient
|
||||
import asyncio
|
||||
from copilot import CopilotClient, SessionConfig, MessageOptions
|
||||
|
||||
client = CopilotClient()
|
||||
client.start()
|
||||
async def main():
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
# Create session with a memorable ID
|
||||
session = client.create_session(
|
||||
session_id="user-123-conversation",
|
||||
model="gpt-5",
|
||||
)
|
||||
# Create session with a memorable ID
|
||||
session = await client.create_session(SessionConfig(
|
||||
session_id="user-123-conversation",
|
||||
model="gpt-5",
|
||||
))
|
||||
|
||||
session.send(prompt="Let's discuss TypeScript generics")
|
||||
await session.send_and_wait(MessageOptions(prompt="Let's discuss TypeScript generics"))
|
||||
|
||||
# Session ID is preserved
|
||||
print(session.session_id) # "user-123-conversation"
|
||||
# Session ID is preserved
|
||||
print(session.session_id) # "user-123-conversation"
|
||||
|
||||
# Destroy session but keep data on disk
|
||||
session.destroy()
|
||||
client.stop()
|
||||
# Destroy session but keep data on disk
|
||||
await session.destroy()
|
||||
await client.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### Resuming a session
|
||||
|
||||
```python
|
||||
client = CopilotClient()
|
||||
client.start()
|
||||
await client.start()
|
||||
|
||||
# Resume the previous session
|
||||
session = client.resume_session("user-123-conversation")
|
||||
session = await client.resume_session("user-123-conversation")
|
||||
|
||||
# Previous context is restored
|
||||
session.send(prompt="What were we discussing?")
|
||||
await session.send_and_wait(MessageOptions(prompt="What were we discussing?"))
|
||||
|
||||
session.destroy()
|
||||
client.stop()
|
||||
await session.destroy()
|
||||
await client.stop()
|
||||
```
|
||||
|
||||
### Listing available sessions
|
||||
|
||||
```python
|
||||
sessions = client.list_sessions()
|
||||
sessions = await client.list_sessions()
|
||||
for s in sessions:
|
||||
print("Session:", s["sessionId"])
|
||||
print("Session:", s.session_id)
|
||||
```
|
||||
|
||||
### Deleting a session permanently
|
||||
|
||||
```python
|
||||
# Remove session and all its data from disk
|
||||
client.delete_session("user-123-conversation")
|
||||
await client.delete_session("user-123-conversation")
|
||||
```
|
||||
|
||||
### Getting session history
|
||||
|
||||
```python
|
||||
messages = session.get_messages()
|
||||
messages = await session.get_messages()
|
||||
for msg in messages:
|
||||
print(f"[{msg['type']}] {msg['data']}")
|
||||
print(f"[{msg.type}] {msg.data.content}")
|
||||
```
|
||||
|
||||
## Best practices
|
||||
|
||||
@@ -38,10 +38,15 @@ python pr_visualization.py --repo github/copilot-sdk
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
from copilot import CopilotClient
|
||||
import re
|
||||
from copilot import (
|
||||
CopilotClient, SessionConfig, MessageOptions,
|
||||
SessionEvent, SessionEventType,
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Git & GitHub Detection
|
||||
@@ -69,7 +74,6 @@ def get_github_remote():
|
||||
remote_url = result.stdout.strip()
|
||||
|
||||
# Handle SSH: git@github.com:owner/repo.git
|
||||
import re
|
||||
ssh_match = re.search(r"git@github\.com:(.+/.+?)(?:\.git)?$", remote_url)
|
||||
if ssh_match:
|
||||
return ssh_match.group(1)
|
||||
@@ -98,7 +102,7 @@ def prompt_for_repo():
|
||||
# Main Application
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
async def main():
|
||||
print("🔍 PR Age Chart Generator\n")
|
||||
|
||||
# Determine the repository
|
||||
@@ -126,11 +130,11 @@ def main():
|
||||
|
||||
owner, repo_name = repo.split("/", 1)
|
||||
|
||||
# Create Copilot client - no custom tools needed!
|
||||
client = CopilotClient(log_level="error")
|
||||
client.start()
|
||||
# Create Copilot client
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
session = client.create_session(
|
||||
session = await client.create_session(SessionConfig(
|
||||
model="gpt-5",
|
||||
system_message={
|
||||
"content": f"""
|
||||
@@ -147,30 +151,34 @@ The current working directory is: {os.getcwd()}
|
||||
</instructions>
|
||||
"""
|
||||
}
|
||||
)
|
||||
))
|
||||
|
||||
done = asyncio.Event()
|
||||
|
||||
# Set up event handling
|
||||
def handle_event(event):
|
||||
if event["type"] == "assistant.message":
|
||||
print(f"\n🤖 {event['data']['content']}\n")
|
||||
elif event["type"] == "tool.execution_start":
|
||||
print(f" ⚙️ {event['data']['toolName']}")
|
||||
def handle_event(event: SessionEvent):
|
||||
if event.type == SessionEventType.ASSISTANT_MESSAGE:
|
||||
print(f"\n🤖 {event.data.content}\n")
|
||||
elif event.type == SessionEventType.TOOL_EXECUTION_START:
|
||||
print(f" ⚙️ {event.data.tool_name}")
|
||||
elif event.type.value == "session.idle":
|
||||
done.set()
|
||||
|
||||
session.on(handle_event)
|
||||
|
||||
# Initial prompt - let Copilot figure out the details
|
||||
print("\n📊 Starting analysis...\n")
|
||||
|
||||
session.send(prompt=f"""
|
||||
await session.send(MessageOptions(prompt=f"""
|
||||
Fetch the open pull requests for {owner}/{repo_name} from the last week.
|
||||
Calculate the age of each PR in days.
|
||||
Then generate a bar chart image showing the distribution of PR ages
|
||||
(group them into sensible buckets like <1 day, 1-3 days, etc.).
|
||||
Save the chart as "pr-age-chart.png" in the current directory.
|
||||
Finally, summarize the PR health - average age, oldest PR, and how many might be considered stale.
|
||||
""")
|
||||
"""))
|
||||
|
||||
session.wait_for_idle()
|
||||
await done.wait()
|
||||
|
||||
# Interactive loop
|
||||
print("\n💡 Ask follow-up questions or type \"exit\" to quit.\n")
|
||||
@@ -189,14 +197,15 @@ The current working directory is: {os.getcwd()}
|
||||
break
|
||||
|
||||
if user_input:
|
||||
session.send(prompt=user_input)
|
||||
session.wait_for_idle()
|
||||
done.clear()
|
||||
await session.send(MessageOptions(prompt=user_input))
|
||||
await done.wait()
|
||||
|
||||
session.destroy()
|
||||
client.stop()
|
||||
await session.destroy()
|
||||
await client.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
252
cookbook/copilot-sdk/python/ralph-loop.md
Normal file
252
cookbook/copilot-sdk/python/ralph-loop.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# Ralph Loop: Autonomous AI Task Loops
|
||||
|
||||
Build autonomous coding loops where an AI agent picks tasks, implements them, validates against backpressure (tests, builds), commits, and repeats — each iteration in a fresh context window.
|
||||
|
||||
> **Runnable example:** [recipe/ralph_loop.py](recipe/ralph_loop.py)
|
||||
>
|
||||
> From the repository root, install dependencies and run:
|
||||
>
|
||||
> ```bash
|
||||
> pip install -r cookbook/copilot-sdk/python/recipe/requirements.txt
|
||||
> python cookbook/copilot-sdk/python/recipe/ralph_loop.py
|
||||
> ```
|
||||
>
|
||||
> Make sure `PROMPT_build.md` and `PROMPT_plan.md` exist in your current working directory before running the loop.
|
||||
|
||||
## What is a Ralph Loop?
|
||||
|
||||
A [Ralph loop](https://ghuntley.com/ralph/) is an autonomous development workflow where an AI agent iterates through tasks in isolated context windows. The key insight: **state lives on disk, not in the model's context**. Each iteration starts fresh, reads the current state from files, does one task, writes results back to disk, and exits.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ loop.sh │
|
||||
│ while true: │
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ Fresh session (isolated context) │ │
|
||||
│ │ │ │
|
||||
│ │ 1. Read PROMPT.md + AGENTS.md │ │
|
||||
│ │ 2. Study specs/* and code │ │
|
||||
│ │ 3. Pick next task from plan │ │
|
||||
│ │ 4. Implement + run tests │ │
|
||||
│ │ 5. Update plan, commit, exit │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
│ ↻ next iteration (fresh context) │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Core principles:**
|
||||
|
||||
- **Fresh context per iteration**: Each loop creates a new session — no context accumulation, always in the "smart zone"
|
||||
- **Disk as shared state**: `IMPLEMENTATION_PLAN.md` persists between iterations and acts as the coordination mechanism
|
||||
- **Backpressure steers quality**: Tests, builds, and lints reject bad work — the agent must fix issues before committing
|
||||
- **Two modes**: PLANNING (gap analysis → generate plan) and BUILDING (implement from plan)
|
||||
|
||||
## Simple Version
|
||||
|
||||
The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | copilot ; done`:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from copilot import CopilotClient, MessageOptions, SessionConfig
|
||||
|
||||
|
||||
async def ralph_loop(prompt_file: str, max_iterations: int = 50):
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
try:
|
||||
prompt = Path(prompt_file).read_text()
|
||||
|
||||
for i in range(1, max_iterations + 1):
|
||||
print(f"\n=== Iteration {i}/{max_iterations} ===")
|
||||
|
||||
# Fresh session each iteration — context isolation is the point
|
||||
session = await client.create_session(
|
||||
SessionConfig(model="gpt-5.1-codex-mini")
|
||||
)
|
||||
try:
|
||||
await session.send_and_wait(
|
||||
MessageOptions(prompt=prompt), timeout=600
|
||||
)
|
||||
finally:
|
||||
await session.destroy()
|
||||
|
||||
print(f"Iteration {i} complete.")
|
||||
finally:
|
||||
await client.stop()
|
||||
|
||||
|
||||
# Usage: point at your PROMPT.md
|
||||
asyncio.run(ralph_loop("PROMPT.md", 20))
|
||||
```
|
||||
|
||||
This is all you need to get started. The prompt file tells the agent what to do; the agent reads project files, does work, commits, and exits. The loop restarts with a clean slate.
|
||||
|
||||
## Ideal Version
|
||||
|
||||
The full Ralph pattern with planning and building modes, matching the [Ralph Playbook](https://github.com/ClaytonFarr/ralph-playbook) architecture:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from copilot import CopilotClient, MessageOptions, SessionConfig
|
||||
|
||||
|
||||
async def ralph_loop(mode: str = "build", max_iterations: int = 50):
|
||||
prompt_file = "PROMPT_plan.md" if mode == "plan" else "PROMPT_build.md"
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
print("━" * 40)
|
||||
print(f"Mode: {mode}")
|
||||
print(f"Prompt: {prompt_file}")
|
||||
print(f"Max: {max_iterations} iterations")
|
||||
print("━" * 40)
|
||||
|
||||
try:
|
||||
prompt = Path(prompt_file).read_text()
|
||||
|
||||
for i in range(1, max_iterations + 1):
|
||||
print(f"\n=== Iteration {i}/{max_iterations} ===")
|
||||
|
||||
session = await client.create_session(SessionConfig(
|
||||
model="gpt-5.1-codex-mini",
|
||||
# Pin the agent to the project directory
|
||||
working_directory=str(Path.cwd()),
|
||||
# Auto-approve tool calls for unattended operation
|
||||
on_permission_request=lambda _req, _ctx: {
|
||||
"kind": "approved", "rules": []
|
||||
},
|
||||
))
|
||||
|
||||
# Log tool usage for visibility
|
||||
def log_tool_event(event):
|
||||
if event.type.value == "tool.execution_start":
|
||||
print(f" ⚙ {event.data.tool_name}")
|
||||
|
||||
session.on(log_tool_event)
|
||||
|
||||
try:
|
||||
await session.send_and_wait(
|
||||
MessageOptions(prompt=prompt), timeout=600
|
||||
)
|
||||
finally:
|
||||
await session.destroy()
|
||||
|
||||
print(f"\nIteration {i} complete.")
|
||||
|
||||
print(f"\nReached max iterations: {max_iterations}")
|
||||
finally:
|
||||
await client.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = sys.argv[1:]
|
||||
mode = "plan" if "plan" in args else "build"
|
||||
max_iter = next((int(a) for a in args if a.isdigit()), 50)
|
||||
asyncio.run(ralph_loop(mode, max_iter))
|
||||
```
|
||||
|
||||
### Required Project Files
|
||||
|
||||
The ideal version expects this file structure in your project:
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── PROMPT_plan.md # Planning mode instructions
|
||||
├── PROMPT_build.md # Building mode instructions
|
||||
├── AGENTS.md # Operational guide (build/test commands)
|
||||
├── IMPLEMENTATION_PLAN.md # Task list (generated by planning mode)
|
||||
├── specs/ # Requirement specs (one per topic)
|
||||
│ ├── auth.md
|
||||
│ └── data-pipeline.md
|
||||
└── src/ # Your source code
|
||||
```
|
||||
|
||||
### Example `PROMPT_plan.md`
|
||||
|
||||
```markdown
|
||||
0a. Study `specs/*` to learn the application specifications.
|
||||
0b. Study IMPLEMENTATION_PLAN.md (if present) to understand the plan so far.
|
||||
0c. Study `src/` to understand existing code and shared utilities.
|
||||
|
||||
1. Compare specs against code (gap analysis). Create or update
|
||||
IMPLEMENTATION_PLAN.md as a prioritized bullet-point list of tasks
|
||||
yet to be implemented. Do NOT implement anything.
|
||||
|
||||
IMPORTANT: Do NOT assume functionality is missing — search the
|
||||
codebase first to confirm. Prefer updating existing utilities over
|
||||
creating ad-hoc copies.
|
||||
```
|
||||
|
||||
### Example `PROMPT_build.md`
|
||||
|
||||
```markdown
|
||||
0a. Study `specs/*` to learn the application specifications.
|
||||
0b. Study IMPLEMENTATION_PLAN.md.
|
||||
0c. Study `src/` for reference.
|
||||
|
||||
1. Choose the most important item from IMPLEMENTATION_PLAN.md. Before
|
||||
making changes, search the codebase (don't assume not implemented).
|
||||
2. After implementing, run the tests. If functionality is missing, add it.
|
||||
3. When you discover issues, update IMPLEMENTATION_PLAN.md immediately.
|
||||
4. When tests pass, update IMPLEMENTATION_PLAN.md, then `git add -A`
|
||||
then `git commit` with a descriptive message.
|
||||
|
||||
5. When authoring documentation, capture the why.
|
||||
6. Implement completely. No placeholders or stubs.
|
||||
7. Keep IMPLEMENTATION_PLAN.md current — future iterations depend on it.
|
||||
```
|
||||
|
||||
### Example `AGENTS.md`
|
||||
|
||||
Keep this brief (~60 lines). It's loaded every iteration, so bloat wastes context.
|
||||
|
||||
```markdown
|
||||
## Build & Run
|
||||
|
||||
python -m pytest
|
||||
|
||||
## Validation
|
||||
|
||||
- Tests: `pytest`
|
||||
- Typecheck: `mypy src/`
|
||||
- Lint: `ruff check src/`
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Fresh context per iteration**: Never accumulate context across iterations — that's the whole point
|
||||
2. **Disk is your database**: `IMPLEMENTATION_PLAN.md` is shared state between isolated sessions
|
||||
3. **Backpressure is essential**: Tests, builds, lints in `AGENTS.md` — the agent must pass them before committing
|
||||
4. **Start with PLANNING mode**: Generate the plan first, then switch to BUILDING
|
||||
5. **Observe and tune**: Watch early iterations, add guardrails to prompts when the agent fails in specific ways
|
||||
6. **The plan is disposable**: If the agent goes off track, delete `IMPLEMENTATION_PLAN.md` and re-plan
|
||||
7. **Keep `AGENTS.md` brief**: It's loaded every iteration — operational info only, no progress notes
|
||||
8. **Use a sandbox**: The agent runs autonomously with full tool access — isolate it
|
||||
9. **Set `working_directory`**: Pin the session to your project root so tool operations resolve paths correctly
|
||||
10. **Auto-approve permissions**: Use `on_permission_request` to allow tool calls without interrupting the loop
|
||||
|
||||
## When to Use a Ralph Loop
|
||||
|
||||
**Good for:**
|
||||
|
||||
- Implementing features from specs with test-driven validation
|
||||
- Large refactors broken into many small tasks
|
||||
- Unattended, long-running development with clear requirements
|
||||
- Any work where backpressure (tests/builds) can verify correctness
|
||||
|
||||
**Not good for:**
|
||||
|
||||
- Tasks requiring human judgment mid-loop
|
||||
- One-shot operations that don't benefit from iteration
|
||||
- Vague requirements without testable acceptance criteria
|
||||
- Exploratory prototyping where direction isn't clear
|
||||
|
||||
## See Also
|
||||
|
||||
- [Error Handling](error-handling.md) — timeout patterns and graceful shutdown for long-running sessions
|
||||
- [Persisting Sessions](persisting-sessions.md) — save and resume sessions across restarts
|
||||
171
cookbook/copilot-sdk/python/recipe/accessibility_report.py
Normal file
171
cookbook/copilot-sdk/python/recipe/accessibility_report.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
from copilot import (
|
||||
CopilotClient, SessionConfig, MessageOptions,
|
||||
SessionEvent, SessionEventType,
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Main Application
|
||||
# ============================================================================
|
||||
|
||||
async def main():
|
||||
print("=== Accessibility Report Generator ===\n")
|
||||
|
||||
url = input("Enter URL to analyze: ").strip()
|
||||
|
||||
if not url:
|
||||
print("No URL provided. Exiting.")
|
||||
return
|
||||
|
||||
# Ensure URL has a scheme
|
||||
if not url.startswith("http://") and not url.startswith("https://"):
|
||||
url = "https://" + url
|
||||
|
||||
print(f"\nAnalyzing: {url}")
|
||||
print("Please wait...\n")
|
||||
|
||||
# Create Copilot client with Playwright MCP server
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
session = await client.create_session(SessionConfig(
|
||||
model="claude-opus-4.6",
|
||||
streaming=True,
|
||||
mcp_servers={
|
||||
"playwright": {
|
||||
"type": "local",
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest"],
|
||||
"tools": ["*"],
|
||||
}
|
||||
},
|
||||
))
|
||||
|
||||
done = asyncio.Event()
|
||||
|
||||
# Set up streaming event handling
|
||||
def handle_event(event: SessionEvent):
|
||||
if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA:
|
||||
print(event.data.delta_content or "", end="", flush=True)
|
||||
elif event.type.value == "session.idle":
|
||||
done.set()
|
||||
elif event.type.value == "session.error":
|
||||
print(f"\nError: {event.data.message}")
|
||||
done.set()
|
||||
|
||||
session.on(handle_event)
|
||||
|
||||
prompt = f"""
|
||||
Use the Playwright MCP server to analyze the accessibility of this webpage: {url}
|
||||
|
||||
Please:
|
||||
1. Navigate to the URL using playwright-browser_navigate
|
||||
2. Take an accessibility snapshot using playwright-browser_snapshot
|
||||
3. Analyze the snapshot and provide a detailed accessibility report
|
||||
|
||||
Format the report EXACTLY like this structure with emoji indicators:
|
||||
|
||||
📊 Accessibility Report: [Page Title] (domain.com)
|
||||
|
||||
✅ What's Working Well
|
||||
| Category | Status | Details |
|
||||
|----------|--------|---------|
|
||||
| Language | ✅ Pass | lang="en-US" properly set |
|
||||
| Page Title | ✅ Pass | "[Title]" is descriptive |
|
||||
| Heading Hierarchy | ✅ Pass | Single H1, proper H2/H3 structure |
|
||||
| Images | ✅ Pass | All X images have alt text |
|
||||
| Viewport | ✅ Pass | Allows pinch-zoom (no user-scalable=no) |
|
||||
| Links | ✅ Pass | No ambiguous "click here" links |
|
||||
| Reduced Motion | ✅ Pass | Supports prefers-reduced-motion |
|
||||
| Autoplay Media | ✅ Pass | No autoplay audio/video |
|
||||
|
||||
⚠️ Issues Found
|
||||
| Severity | Issue | WCAG Criterion | Recommendation |
|
||||
|----------|-------|----------------|----------------|
|
||||
| 🔴 High | No <main> landmark | 1.3.1, 2.4.1 | Wrap main content in <main> element |
|
||||
| 🔴 High | No skip navigation link | 2.4.1 | Add "Skip to content" link at top |
|
||||
| 🟡 Medium | Focus outlines disabled | 2.4.7 | Default outline is none - ensure visible :focus styles exist |
|
||||
| 🟡 Medium | Small touch targets | 2.5.8 | Navigation links are 37px tall (below 44px minimum) |
|
||||
|
||||
📋 Stats Summary
|
||||
- Total Links: X
|
||||
- Total Headings: X (1× H1, proper hierarchy)
|
||||
- Focusable Elements: X
|
||||
- Landmarks Found: banner ✅, navigation ✅, main ❌, footer ✅
|
||||
|
||||
⚙️ Priority Recommendations
|
||||
- Add <main> landmark - Wrap page content in <main role="main"> for screen reader navigation
|
||||
- Add skip link - Hidden link at start: <a href="#main-content" class="skip-link">Skip to content</a>
|
||||
- Increase touch targets - Add padding to nav links and tags to meet 44×44px minimum
|
||||
- Verify focus styles - Test keyboard navigation; add visible :focus or :focus-visible outlines
|
||||
|
||||
Use ✅ for pass, 🔴 for high severity issues, 🟡 for medium severity, ❌ for missing items.
|
||||
Include actual findings from the page analysis - don't just copy the example.
|
||||
"""
|
||||
|
||||
await session.send(MessageOptions(prompt=prompt))
|
||||
await done.wait()
|
||||
|
||||
print("\n\n=== Report Complete ===\n")
|
||||
|
||||
# Prompt user for test generation
|
||||
generate_tests = input("Would you like to generate Playwright accessibility tests? (y/n): ").strip().lower()
|
||||
|
||||
if generate_tests in ("y", "yes"):
|
||||
done.clear()
|
||||
|
||||
detect_language_prompt = """
|
||||
Analyze the current working directory to detect the primary programming language used in this project.
|
||||
Look for project files like package.json, *.csproj, pom.xml, requirements.txt, go.mod, etc.
|
||||
|
||||
Respond with ONLY the detected language name (e.g., "TypeScript", "JavaScript", "C#", "Python", "Java")
|
||||
and a brief explanation of why you detected it.
|
||||
If no project is detected, suggest "TypeScript" as the default for Playwright tests.
|
||||
"""
|
||||
|
||||
print("\nDetecting project language...\n")
|
||||
await session.send(MessageOptions(prompt=detect_language_prompt))
|
||||
await done.wait()
|
||||
|
||||
language = input("\n\nConfirm language for tests (or enter a different one): ").strip()
|
||||
if not language:
|
||||
language = "TypeScript"
|
||||
|
||||
done.clear()
|
||||
|
||||
test_generation_prompt = f"""
|
||||
Based on the accessibility report you just generated for {url}, create Playwright accessibility tests in {language}.
|
||||
|
||||
The tests should:
|
||||
1. Verify all the accessibility checks from the report
|
||||
2. Test for the issues that were found (to ensure they get fixed)
|
||||
3. Include tests for:
|
||||
- Page has proper lang attribute
|
||||
- Page has descriptive title
|
||||
- Heading hierarchy is correct (single H1, proper nesting)
|
||||
- All images have alt text
|
||||
- No autoplay media
|
||||
- Landmark regions exist (banner, nav, main, footer)
|
||||
- Skip navigation link exists and works
|
||||
- Focus indicators are visible
|
||||
- Touch targets meet minimum size requirements
|
||||
4. Use Playwright's accessibility testing features
|
||||
5. Include helpful comments explaining each test
|
||||
|
||||
Output the complete test file that can be saved and run.
|
||||
Use the Playwright MCP server tools if you need to verify any page details.
|
||||
"""
|
||||
|
||||
print("\nGenerating accessibility tests...\n")
|
||||
await session.send(MessageOptions(prompt=test_generation_prompt))
|
||||
await done.wait()
|
||||
|
||||
print("\n\n=== Tests Generated ===")
|
||||
|
||||
await session.destroy()
|
||||
await client.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,28 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from copilot import CopilotClient
|
||||
import asyncio
|
||||
from copilot import CopilotClient, SessionConfig, MessageOptions
|
||||
|
||||
client = CopilotClient()
|
||||
async def main():
|
||||
client = CopilotClient()
|
||||
|
||||
try:
|
||||
client.start()
|
||||
session = client.create_session(model="gpt-5")
|
||||
try:
|
||||
await client.start()
|
||||
session = await client.create_session(SessionConfig(model="gpt-5"))
|
||||
|
||||
response = None
|
||||
def handle_message(event):
|
||||
nonlocal response
|
||||
if event["type"] == "assistant.message":
|
||||
response = event["data"]["content"]
|
||||
response = await session.send_and_wait(MessageOptions(prompt="Hello!"))
|
||||
|
||||
session.on(handle_message)
|
||||
session.send(prompt="Hello!")
|
||||
session.wait_for_idle()
|
||||
if response:
|
||||
print(response.data.content)
|
||||
|
||||
if response:
|
||||
print(response)
|
||||
await session.destroy()
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
await client.stop()
|
||||
|
||||
session.destroy()
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
client.stop()
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -1,31 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from copilot import CopilotClient
|
||||
import asyncio
|
||||
import os
|
||||
from copilot import (
|
||||
CopilotClient, SessionConfig, MessageOptions,
|
||||
SessionEvent, SessionEventType,
|
||||
)
|
||||
|
||||
# Create and start client
|
||||
client = CopilotClient()
|
||||
client.start()
|
||||
async def main():
|
||||
# Create and start client
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
# Create session
|
||||
session = client.create_session(model="gpt-5")
|
||||
# Create session
|
||||
session = await client.create_session(SessionConfig(model="gpt-5"))
|
||||
|
||||
# Event handler
|
||||
def handle_event(event):
|
||||
if event["type"] == "assistant.message":
|
||||
print(f"\nCopilot: {event['data']['content']}")
|
||||
elif event["type"] == "tool.execution_start":
|
||||
print(f" → Running: {event['data']['toolName']}")
|
||||
elif event["type"] == "tool.execution_complete":
|
||||
print(f" ✓ Completed: {event['data']['toolCallId']}")
|
||||
done = asyncio.Event()
|
||||
|
||||
session.on(handle_event)
|
||||
# Event handler
|
||||
def handle_event(event: SessionEvent):
|
||||
if event.type == SessionEventType.ASSISTANT_MESSAGE:
|
||||
print(f"\nCopilot: {event.data.content}")
|
||||
elif event.type == SessionEventType.TOOL_EXECUTION_START:
|
||||
print(f" → Running: {event.data.tool_name}")
|
||||
elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE:
|
||||
print(f" ✓ Completed: {event.data.tool_call_id}")
|
||||
elif event.type.value == "session.idle":
|
||||
done.set()
|
||||
|
||||
# Ask Copilot to organize files
|
||||
# Change this to your target folder
|
||||
target_folder = os.path.expanduser("~/Downloads")
|
||||
session.on(handle_event)
|
||||
|
||||
session.send(prompt=f"""
|
||||
# Ask Copilot to organize files
|
||||
# Change this to your target folder
|
||||
target_folder = os.path.expanduser("~/Downloads")
|
||||
|
||||
await session.send(MessageOptions(prompt=f"""
|
||||
Analyze the files in "{target_folder}" and organize them into subfolders.
|
||||
|
||||
1. First, list all files and their metadata
|
||||
@@ -34,9 +43,12 @@ Analyze the files in "{target_folder}" and organize them into subfolders.
|
||||
4. Move each file to its appropriate subfolder
|
||||
|
||||
Please confirm before moving any files.
|
||||
""")
|
||||
"""))
|
||||
|
||||
session.wait_for_idle()
|
||||
await done.wait()
|
||||
|
||||
session.destroy()
|
||||
client.stop()
|
||||
await session.destroy()
|
||||
await client.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from copilot import CopilotClient
|
||||
import asyncio
|
||||
from copilot import CopilotClient, SessionConfig, MessageOptions
|
||||
|
||||
client = CopilotClient()
|
||||
client.start()
|
||||
async def main():
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
# Create multiple independent sessions
|
||||
session1 = client.create_session(model="gpt-5")
|
||||
session2 = client.create_session(model="gpt-5")
|
||||
session3 = client.create_session(model="claude-sonnet-4.5")
|
||||
# Create multiple independent sessions
|
||||
session1 = await client.create_session(SessionConfig(model="gpt-5"))
|
||||
session2 = await client.create_session(SessionConfig(model="gpt-5"))
|
||||
session3 = await client.create_session(SessionConfig(model="claude-sonnet-4.5"))
|
||||
|
||||
print("Created 3 independent sessions")
|
||||
print("Created 3 independent sessions")
|
||||
|
||||
# Each session maintains its own conversation history
|
||||
session1.send(prompt="You are helping with a Python project")
|
||||
session2.send(prompt="You are helping with a TypeScript project")
|
||||
session3.send(prompt="You are helping with a Go project")
|
||||
# Each session maintains its own conversation history
|
||||
await session1.send(MessageOptions(prompt="You are helping with a Python project"))
|
||||
await session2.send(MessageOptions(prompt="You are helping with a TypeScript project"))
|
||||
await session3.send(MessageOptions(prompt="You are helping with a Go project"))
|
||||
|
||||
print("Sent initial context to all sessions")
|
||||
print("Sent initial context to all sessions")
|
||||
|
||||
# Follow-up messages stay in their respective contexts
|
||||
session1.send(prompt="How do I create a virtual environment?")
|
||||
session2.send(prompt="How do I set up tsconfig?")
|
||||
session3.send(prompt="How do I initialize a module?")
|
||||
# Follow-up messages stay in their respective contexts
|
||||
await session1.send(MessageOptions(prompt="How do I create a virtual environment?"))
|
||||
await session2.send(MessageOptions(prompt="How do I set up tsconfig?"))
|
||||
await session3.send(MessageOptions(prompt="How do I initialize a module?"))
|
||||
|
||||
print("Sent follow-up questions to each session")
|
||||
print("Sent follow-up questions to each session")
|
||||
|
||||
# Clean up all sessions
|
||||
session1.destroy()
|
||||
session2.destroy()
|
||||
session3.destroy()
|
||||
client.stop()
|
||||
# Clean up all sessions
|
||||
await session1.destroy()
|
||||
await session2.destroy()
|
||||
await session3.destroy()
|
||||
await client.stop()
|
||||
|
||||
print("All sessions destroyed successfully")
|
||||
print("All sessions destroyed successfully")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from copilot import CopilotClient
|
||||
import asyncio
|
||||
from copilot import CopilotClient, SessionConfig, MessageOptions
|
||||
|
||||
client = CopilotClient()
|
||||
client.start()
|
||||
async def main():
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
# Create session with a memorable ID
|
||||
session = client.create_session(
|
||||
session_id="user-123-conversation",
|
||||
model="gpt-5",
|
||||
)
|
||||
# Create session with a memorable ID
|
||||
session = await client.create_session(SessionConfig(
|
||||
session_id="user-123-conversation",
|
||||
model="gpt-5",
|
||||
))
|
||||
|
||||
session.send(prompt="Let's discuss TypeScript generics")
|
||||
print(f"Session created: {session.session_id}")
|
||||
await session.send_and_wait(MessageOptions(prompt="Let's discuss TypeScript generics"))
|
||||
print(f"Session created: {session.session_id}")
|
||||
|
||||
# Destroy session but keep data on disk
|
||||
session.destroy()
|
||||
print("Session destroyed (state persisted)")
|
||||
# Destroy session but keep data on disk
|
||||
await session.destroy()
|
||||
print("Session destroyed (state persisted)")
|
||||
|
||||
# Resume the previous session
|
||||
resumed = client.resume_session("user-123-conversation")
|
||||
print(f"Resumed: {resumed.session_id}")
|
||||
# Resume the previous session
|
||||
resumed = await client.resume_session("user-123-conversation")
|
||||
print(f"Resumed: {resumed.session_id}")
|
||||
|
||||
resumed.send(prompt="What were we discussing?")
|
||||
await resumed.send_and_wait(MessageOptions(prompt="What were we discussing?"))
|
||||
|
||||
# List sessions
|
||||
sessions = client.list_sessions()
|
||||
print("Sessions:", [s["sessionId"] for s in sessions])
|
||||
# List sessions
|
||||
sessions = await client.list_sessions()
|
||||
print("Sessions:", [s.session_id for s in sessions])
|
||||
|
||||
# Delete session permanently
|
||||
client.delete_session("user-123-conversation")
|
||||
print("Session deleted")
|
||||
# Delete session permanently
|
||||
await client.delete_session("user-123-conversation")
|
||||
print("Session deleted")
|
||||
|
||||
resumed.destroy()
|
||||
client.stop()
|
||||
await resumed.destroy()
|
||||
await client.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from copilot import CopilotClient
|
||||
from copilot import (
|
||||
CopilotClient, SessionConfig, MessageOptions,
|
||||
SessionEvent, SessionEventType,
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Git & GitHub Detection
|
||||
@@ -60,7 +64,7 @@ def prompt_for_repo():
|
||||
# Main Application
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
async def main():
|
||||
print("🔍 PR Age Chart Generator\n")
|
||||
|
||||
# Determine the repository
|
||||
@@ -88,11 +92,11 @@ def main():
|
||||
|
||||
owner, repo_name = repo.split("/", 1)
|
||||
|
||||
# Create Copilot client - no custom tools needed!
|
||||
client = CopilotClient(log_level="error")
|
||||
client.start()
|
||||
# Create Copilot client
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
session = client.create_session(
|
||||
session = await client.create_session(SessionConfig(
|
||||
model="gpt-5",
|
||||
system_message={
|
||||
"content": f"""
|
||||
@@ -109,30 +113,34 @@ The current working directory is: {os.getcwd()}
|
||||
</instructions>
|
||||
"""
|
||||
}
|
||||
)
|
||||
))
|
||||
|
||||
done = asyncio.Event()
|
||||
|
||||
# Set up event handling
|
||||
def handle_event(event):
|
||||
if event["type"] == "assistant.message":
|
||||
print(f"\n🤖 {event['data']['content']}\n")
|
||||
elif event["type"] == "tool.execution_start":
|
||||
print(f" ⚙️ {event['data']['toolName']}")
|
||||
def handle_event(event: SessionEvent):
|
||||
if event.type == SessionEventType.ASSISTANT_MESSAGE:
|
||||
print(f"\n🤖 {event.data.content}\n")
|
||||
elif event.type == SessionEventType.TOOL_EXECUTION_START:
|
||||
print(f" ⚙️ {event.data.tool_name}")
|
||||
elif event.type.value == "session.idle":
|
||||
done.set()
|
||||
|
||||
session.on(handle_event)
|
||||
|
||||
# Initial prompt - let Copilot figure out the details
|
||||
print("\n📊 Starting analysis...\n")
|
||||
|
||||
session.send(prompt=f"""
|
||||
await session.send(MessageOptions(prompt=f"""
|
||||
Fetch the open pull requests for {owner}/{repo_name} from the last week.
|
||||
Calculate the age of each PR in days.
|
||||
Then generate a bar chart image showing the distribution of PR ages
|
||||
(group them into sensible buckets like <1 day, 1-3 days, etc.).
|
||||
Save the chart as "pr-age-chart.png" in the current directory.
|
||||
Finally, summarize the PR health - average age, oldest PR, and how many might be considered stale.
|
||||
""")
|
||||
"""))
|
||||
|
||||
session.wait_for_idle()
|
||||
await done.wait()
|
||||
|
||||
# Interactive loop
|
||||
print("\n💡 Ask follow-up questions or type \"exit\" to quit.\n")
|
||||
@@ -151,11 +159,12 @@ The current working directory is: {os.getcwd()}
|
||||
break
|
||||
|
||||
if user_input:
|
||||
session.send(prompt=user_input)
|
||||
session.wait_for_idle()
|
||||
done.clear()
|
||||
await session.send(MessageOptions(prompt=user_input))
|
||||
await done.wait()
|
||||
|
||||
session.destroy()
|
||||
client.stop()
|
||||
await session.destroy()
|
||||
await client.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
asyncio.run(main())
|
||||
|
||||
81
cookbook/copilot-sdk/python/recipe/ralph_loop.py
Normal file
81
cookbook/copilot-sdk/python/recipe/ralph_loop.py
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Ralph loop: autonomous AI task loop with fresh context per iteration.
|
||||
|
||||
Two modes:
|
||||
- "plan": reads PROMPT_plan.md, generates/updates IMPLEMENTATION_PLAN.md
|
||||
- "build": reads PROMPT_build.md, implements tasks, runs tests, commits
|
||||
|
||||
Each iteration creates a fresh session so the agent always operates in
|
||||
the "smart zone" of its context window. State is shared between
|
||||
iterations via files on disk (IMPLEMENTATION_PLAN.md, AGENTS.md, specs/*).
|
||||
|
||||
Usage:
|
||||
python ralph_loop.py # build mode, 50 iterations
|
||||
python ralph_loop.py plan # planning mode
|
||||
python ralph_loop.py 20 # build mode, 20 iterations
|
||||
python ralph_loop.py plan 5 # planning mode, 5 iterations
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from copilot import CopilotClient, MessageOptions, SessionConfig
|
||||
|
||||
|
||||
async def ralph_loop(mode: str = "build", max_iterations: int = 50):
|
||||
prompt_file = "PROMPT_plan.md" if mode == "plan" else "PROMPT_build.md"
|
||||
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
print("━" * 40)
|
||||
print(f"Mode: {mode}")
|
||||
print(f"Prompt: {prompt_file}")
|
||||
print(f"Max: {max_iterations} iterations")
|
||||
print("━" * 40)
|
||||
|
||||
try:
|
||||
prompt = Path(prompt_file).read_text()
|
||||
|
||||
for i in range(1, max_iterations + 1):
|
||||
print(f"\n=== Iteration {i}/{max_iterations} ===")
|
||||
|
||||
session = await client.create_session(SessionConfig(
|
||||
model="gpt-5.1-codex-mini",
|
||||
# Pin the agent to the project directory
|
||||
working_directory=str(Path.cwd()),
|
||||
# Auto-approve tool calls for unattended operation
|
||||
on_permission_request=lambda _req, _ctx: {
|
||||
"kind": "approved",
|
||||
"rules": [],
|
||||
},
|
||||
))
|
||||
|
||||
# Log tool usage for visibility
|
||||
def log_tool_event(event):
|
||||
if event.type.value == "tool.execution_start":
|
||||
print(f" ⚙ {event.data.tool_name}")
|
||||
|
||||
session.on(log_tool_event)
|
||||
try:
|
||||
await session.send_and_wait(
|
||||
MessageOptions(prompt=prompt), timeout=600
|
||||
)
|
||||
finally:
|
||||
await session.destroy()
|
||||
|
||||
print(f"\nIteration {i} complete.")
|
||||
|
||||
print(f"\nReached max iterations: {max_iterations}")
|
||||
finally:
|
||||
await client.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = sys.argv[1:]
|
||||
mode = "plan" if "plan" in args else "build"
|
||||
max_iter = next((int(a) for a in args if a.isdigit()), 50)
|
||||
asyncio.run(ralph_loop(mode, max_iter))
|
||||
@@ -131,6 +131,7 @@ Custom agents for GitHub Copilot, making it easy for users and organizations to
|
||||
| [Prompt Builder](../agents/prompt-builder.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fprompt-builder.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fprompt-builder.agent.md) | Expert prompt engineering and validation system for creating high-quality prompts - Brought to you by microsoft/edge-ai | |
|
||||
| [Prompt Engineer](../agents/prompt-engineer.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fprompt-engineer.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fprompt-engineer.agent.md) | A specialized chat mode for analyzing and improving prompts. Every user input is treated as a prompt to be improved. It first provides a detailed analysis of the original prompt within a <reasoning> tag, evaluating it against a systematic framework based on OpenAI's prompt engineering best practices. Following the analysis, it generates a new, improved prompt. | |
|
||||
| [Python MCP Server Expert](../agents/python-mcp-expert.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fpython-mcp-expert.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fpython-mcp-expert.agent.md) | Expert assistant for developing Model Context Protocol (MCP) servers in Python | |
|
||||
| [Reepl Linkedin](../agents/reepl-linkedin.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Freepl-linkedin.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Freepl-linkedin.agent.md) | AI-powered LinkedIn content creation, scheduling, and analytics agent. Create posts, carousels, and manage your LinkedIn presence with GitHub Copilot. | |
|
||||
| [Refine Requirement or Issue](../agents/refine-issue.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Frefine-issue.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Frefine-issue.agent.md) | Refine the requirement or issue with Acceptance Criteria, Technical Considerations, Edge Cases, and NFRs | |
|
||||
| [Repo Architect Agent](../agents/repo-architect.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Frepo-architect.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Frepo-architect.agent.md) | Bootstraps and validates agentic project structures for GitHub Copilot (VS Code) and OpenCode CLI workflows. Run after `opencode /init` or VS Code Copilot initialization to scaffold proper folder hierarchies, instructions, agents, skills, and prompts. | |
|
||||
| [Ruby MCP Expert](../agents/ruby-mcp-expert.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fruby-mcp-expert.agent.md)<br />[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fruby-mcp-expert.agent.md) | Expert assistance for building Model Context Protocol servers in Ruby using the official MCP Ruby SDK gem with Rails integration. | |
|
||||
|
||||
@@ -45,6 +45,7 @@ Skills differ from other primitives by supporting bundled assets (scripts, code
|
||||
| [meeting-minutes](../skills/meeting-minutes/SKILL.md) | Generate concise, actionable meeting minutes for internal meetings. Includes metadata, attendees, agenda, decisions, action items (owner + due date), and follow-up steps. | None |
|
||||
| [microsoft-code-reference](../skills/microsoft-code-reference/SKILL.md) | Look up Microsoft API references, find working code samples, and verify SDK code is correct. Use when working with Azure SDKs, .NET libraries, or Microsoft APIs—to find the right method, check parameters, get working examples, or troubleshoot errors. Catches hallucinated methods, wrong signatures, and deprecated patterns by querying official docs. | None |
|
||||
| [microsoft-docs](../skills/microsoft-docs/SKILL.md) | Query official Microsoft documentation to find concepts, tutorials, and code examples across Azure, .NET, Agent Framework, Aspire, VS Code, GitHub, and more. Uses Microsoft Learn MCP as the default, with Context7 and Aspire MCP for content that lives outside learn.microsoft.com. | None |
|
||||
| [microsoft-skill-creator](../skills/microsoft-skill-creator/SKILL.md) | Create agent skills for Microsoft technologies using Learn MCP tools. Use when users want to create a skill that teaches agents about any Microsoft technology, library, framework, or service (Azure, .NET, M365, VS Code, Bicep, etc.). Investigates topics deeply, then generates a hybrid skill storing essential knowledge locally while enabling dynamic deeper investigation. | `references/skill-templates.md` |
|
||||
| [nano-banana-pro-openrouter](../skills/nano-banana-pro-openrouter/SKILL.md) | Generate or edit images via OpenRouter with the Gemini 3 Pro Image model. Use for prompt-only image generation, image edits, and multi-image compositing; supports 1K/2K/4K output. | `assets/SYSTEM_TEMPLATE`<br />`scripts/generate_image.py` |
|
||||
| [nuget-manager](../skills/nuget-manager/SKILL.md) | Manage NuGet packages in .NET projects/solutions. Use this skill when adding, removing, or updating NuGet package versions. It enforces using `dotnet` CLI for package management and provides strict procedures for direct file edits only when updating versions. | None |
|
||||
| [penpot-uiux-design](../skills/penpot-uiux-design/SKILL.md) | Comprehensive guide for creating professional UI/UX designs in Penpot using MCP tools. Use this skill when: (1) Creating new UI/UX designs for web, mobile, or desktop applications, (2) Building design systems with components and tokens, (3) Designing dashboards, forms, navigation, or landing pages, (4) Applying accessibility standards and best practices, (5) Following platform guidelines (iOS, Android, Material Design), (6) Reviewing or improving existing Penpot designs for usability. Triggers: "design a UI", "create interface", "build layout", "design dashboard", "create form", "design landing page", "make it accessible", "design system", "component library". | `references/accessibility.md`<br />`references/component-patterns.md`<br />`references/platform-guidelines.md`<br />`references/setup-troubleshooting.md` |
|
||||
|
||||
@@ -9,9 +9,9 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
|
||||
## Project Context
|
||||
|
||||
- **Power Apps Code Apps (Preview)**: Code-first web app development with Power Platform integration
|
||||
- **Power Apps Code Apps**: Code-first web app development with Power Platform integration
|
||||
- **TypeScript + React**: Recommended frontend stack with Vite bundler
|
||||
- **Power Platform SDK**: @microsoft/power-apps (current version ^0.3.1) for connector integration
|
||||
- **Power Platform SDK**: @microsoft/power-apps (current version ^1.0.3) for connector integration
|
||||
- **PAC CLI**: Power Platform CLI for project management and deployment
|
||||
- **Port 3000**: Required for local development with Power Platform SDK
|
||||
- **Power Apps Premium**: End-user licensing requirement for production use
|
||||
@@ -25,14 +25,15 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
src/
|
||||
├── components/ # Reusable UI components
|
||||
├── hooks/ # Custom React hooks for Power Platform
|
||||
├── services/ # Generated connector services (PAC CLI)
|
||||
├── models/ # Generated TypeScript models (PAC CLI)
|
||||
├── generated/
|
||||
│ ├── services/ # Generated connector services (PAC CLI)
|
||||
│ └── models/ # Generated TypeScript models (PAC CLI)
|
||||
├── utils/ # Utility functions and helpers
|
||||
├── types/ # TypeScript type definitions
|
||||
├── PowerProvider.tsx # Power Platform initialization
|
||||
├── PowerProvider.tsx # Power Platform context wrapper
|
||||
└── main.tsx # Application entry point
|
||||
```
|
||||
- Keep generated files (`services/`, `models/`) separate from custom code
|
||||
- Keep generated files (`generated/services/`, `generated/models/`) separate from custom code
|
||||
- Use consistent naming conventions (kebab-case for files, PascalCase for components)
|
||||
|
||||
### TypeScript Configuration
|
||||
@@ -77,7 +78,7 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
```typescript
|
||||
// Example: Using custom PCF control for data visualization
|
||||
import { PCFControlWrapper } from './components/PCFControlWrapper';
|
||||
|
||||
|
||||
const MyComponent = () => {
|
||||
return (
|
||||
<PCFControlWrapper
|
||||
@@ -95,7 +96,7 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
- **Embed Power BI reports**: Integrate interactive dashboards and reports
|
||||
```typescript
|
||||
import { PowerBIEmbed } from 'powerbi-client-react';
|
||||
|
||||
|
||||
const DashboardComponent = () => {
|
||||
return (
|
||||
<PowerBIEmbed
|
||||
@@ -123,12 +124,12 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
const processDocument = async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
|
||||
const result = await AIBuilderService.ProcessDocument({
|
||||
modelId: 'document-processing-model-id',
|
||||
document: formData
|
||||
});
|
||||
|
||||
|
||||
return result.extractedFields;
|
||||
};
|
||||
```
|
||||
@@ -141,12 +142,12 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
```typescript
|
||||
import { DirectLine } from 'botframework-directlinejs';
|
||||
import { WebChat } from 'botframework-webchat';
|
||||
|
||||
|
||||
const ChatbotComponent = () => {
|
||||
const directLine = new DirectLine({
|
||||
token: chatbotToken
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ height: '400px', width: '100%' }}>
|
||||
<WebChat directLine={directLine} />
|
||||
@@ -159,23 +160,11 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
- Use generated TypeScript services from PAC CLI for connector operations
|
||||
- Implement proper authentication flows with Microsoft Entra ID
|
||||
- Handle connector consent dialogs and permission management
|
||||
- PowerProvider implementation pattern:
|
||||
- PowerProvider implementation pattern (no SDK initialization required in v1.0):
|
||||
```typescript
|
||||
import { initialize } from "@microsoft/power-apps/app";
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export default function PowerProvider({ children }: { children: ReactNode }) {
|
||||
useEffect(() => {
|
||||
const initApp = async () => {
|
||||
try {
|
||||
await initialize();
|
||||
console.log('Power Platform SDK initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Power Platform SDK:', error);
|
||||
}
|
||||
};
|
||||
initApp();
|
||||
}, []);
|
||||
return <>{children}</>;
|
||||
}
|
||||
```
|
||||
@@ -221,7 +210,7 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
// Handle polymorphic customer lookup (Account or Contact)
|
||||
const customerType = record.customerType; // 'account' or 'contact'
|
||||
const customerId = record.customerId;
|
||||
const customer = customerType === 'account'
|
||||
const customer = customerType === 'account'
|
||||
? await AccountService.get(customerId)
|
||||
: await ContactService.get(customerId);
|
||||
```
|
||||
@@ -257,7 +246,7 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
const transaction = db.transaction(['data'], 'readwrite');
|
||||
transaction.objectStore('data').put({ id: key, data, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
|
||||
async loadData(key: string) {
|
||||
const db = await this.openDB();
|
||||
const transaction = db.transaction(['data'], 'readonly');
|
||||
@@ -397,9 +386,9 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
variant, size, disabled, onClick, children
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
variant, size, disabled, onClick, children
|
||||
}) => {
|
||||
const classes = `btn btn-${variant} btn-${size} ${disabled ? 'btn-disabled' : ''}`;
|
||||
return <button className={classes} onClick={onClick} disabled={disabled}>{children}</button>;
|
||||
@@ -416,14 +405,14 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
theme: 'light',
|
||||
toggleTheme: () => {}
|
||||
});
|
||||
|
||||
|
||||
export const ThemeProvider: React.FC<{children: ReactNode}> = ({ children }) => {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
||||
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prev => prev === 'light' ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
<div className={`theme-${theme}`}>{children}</div>
|
||||
@@ -441,7 +430,7 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
.card-container {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
|
||||
@container (min-width: 400px) {
|
||||
.card {
|
||||
display: grid;
|
||||
@@ -456,7 +445,7 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
- **Framer Motion integration**: Smooth animations and transitions
|
||||
```typescript
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
|
||||
const AnimatedCard = () => {
|
||||
return (
|
||||
<motion.div
|
||||
@@ -480,8 +469,8 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
- **ARIA implementation**: Proper semantic markup and ARIA attributes
|
||||
```typescript
|
||||
// Example: Accessible modal component
|
||||
const Modal: React.FC<{isOpen: boolean, onClose: () => void, children: ReactNode}> = ({
|
||||
isOpen, onClose, children
|
||||
const Modal: React.FC<{isOpen: boolean, onClose: () => void, children: ReactNode}> = ({
|
||||
isOpen, onClose, children
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@@ -491,11 +480,11 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
}
|
||||
return () => { document.body.style.overflow = 'unset'; };
|
||||
}, [isOpen]);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
className={isOpen ? 'modal-open' : 'modal-hidden'}
|
||||
>
|
||||
@@ -512,10 +501,10 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
- **React-intl integration**: Multi-language support
|
||||
```typescript
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
|
||||
const WelcomeMessage = ({ userName }: { userName: string }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
|
||||
return (
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
@@ -539,8 +528,8 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
- Content Security Policy (CSP) not yet supported
|
||||
- Storage SAS IP restrictions not supported
|
||||
- No Power Platform Git integration
|
||||
- No Dataverse solutions support
|
||||
- No native Azure Application Insights integration
|
||||
- Dataverse solutions supported, but solution packager and source code integration are limited
|
||||
- Application Insights supported through SDK logger configuration (no built-in native integration)
|
||||
|
||||
### Workarounds
|
||||
|
||||
@@ -567,7 +556,7 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
- **Package installation failures**: Clear npm cache with `npm cache clean --force` and reinstall
|
||||
- **TypeScript compilation errors**: Check verbatimModuleSyntax setting and SDK compatibility
|
||||
- **Connector permission errors**: Ensure proper consent flow and admin permissions
|
||||
- **PowerProvider initialization errors**: Check console for SDK initialization failures
|
||||
- **PowerProvider issues**: Ensure v1.0 apps do not wait on SDK initialization
|
||||
- **Vite dev server issues**: Ensure host and port configuration match requirements
|
||||
|
||||
### Deployment Issues
|
||||
@@ -577,11 +566,11 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript,
|
||||
- **Connector unavailable**: Verify connector setup in Power Platform and connection status
|
||||
- **Performance issues**: Optimize bundle size with `npm run build --report` and implement caching
|
||||
- **Environment mismatch**: Confirm correct environment selection with `pac env list`
|
||||
- **App timeout errors**: Check PowerProvider.tsx implementation and network connectivity
|
||||
- **App timeout errors**: Check build output and network connectivity
|
||||
|
||||
### Runtime Issues
|
||||
|
||||
- **"App timed out" errors**: Verify npm run build was executed and PowerProvider is error-free
|
||||
- **"App timed out" errors**: Verify npm run build was executed and the deployment output is valid
|
||||
- **Connector authentication prompts**: Ensure proper consent flow implementation
|
||||
- **Data loading failures**: Check network requests and connector permissions
|
||||
- **UI rendering issues**: Verify Fluent UI compatibility and responsive design implementation
|
||||
|
||||
@@ -7,7 +7,6 @@ tools: ['execute/runInTerminal', 'execute/getTerminalOutput']
|
||||
|
||||
```xml
|
||||
<description>This file contains a prompt template for generating conventional commit messages. It provides instructions, examples, and formatting guidelines to help users write standardized, descriptive commit messages in accordance with the Conventional Commits specification.</description>
|
||||
<note>
|
||||
```
|
||||
|
||||
### Workflow
|
||||
|
||||
217
skills/microsoft-skill-creator/SKILL.md
Normal file
217
skills/microsoft-skill-creator/SKILL.md
Normal file
@@ -0,0 +1,217 @@
|
||||
---
|
||||
name: microsoft-skill-creator
|
||||
description: Create agent skills for Microsoft technologies using Learn MCP tools. Use when users want to create a skill that teaches agents about any Microsoft technology, library, framework, or service (Azure, .NET, M365, VS Code, Bicep, etc.). Investigates topics deeply, then generates a hybrid skill storing essential knowledge locally while enabling dynamic deeper investigation.
|
||||
context: fork
|
||||
compatibility: Requires Microsoft Learn MCP Server (https://learn.microsoft.com/api/mcp)
|
||||
---
|
||||
|
||||
# Microsoft Skill Creator
|
||||
|
||||
Create hybrid skills for Microsoft technologies that store essential knowledge locally while enabling dynamic Learn MCP lookups for deeper details.
|
||||
|
||||
## About Skills
|
||||
|
||||
Skills are modular packages that extend agent capabilities with specialized knowledge and workflows. A skill transforms a general-purpose agent into a specialized one for a specific domain.
|
||||
|
||||
### Skill Structure
|
||||
|
||||
```
|
||||
skill-name/
|
||||
├── SKILL.md (required) # Frontmatter (name, description) + instructions
|
||||
├── references/ # Documentation loaded into context as needed
|
||||
├── sample_codes/ # Working code examples
|
||||
└── assets/ # Files used in output (templates, etc.)
|
||||
```
|
||||
|
||||
### Key Principles
|
||||
|
||||
- **Frontmatter is critical**: `name` and `description` determine when the skill triggers—be clear and comprehensive
|
||||
- **Concise is key**: Only include what agents don't already know; context window is shared
|
||||
- **No duplication**: Information lives in SKILL.md OR reference files, not both
|
||||
|
||||
## Learn MCP Tools
|
||||
|
||||
| Tool | Purpose | When to Use |
|
||||
|------|---------|-------------|
|
||||
| `microsoft_docs_search` | Search official docs | First pass discovery, finding topics |
|
||||
| `microsoft_docs_fetch` | Get full page content | Deep dive into important pages |
|
||||
| `microsoft_code_sample_search` | Find code examples | Get implementation patterns |
|
||||
|
||||
## Creation Process
|
||||
|
||||
### Step 1: Investigate the Topic
|
||||
|
||||
Build deep understanding using Learn MCP tools in three phases:
|
||||
|
||||
**Phase 1 - Scope Discovery:**
|
||||
```
|
||||
microsoft_docs_search(query="{technology} overview what is")
|
||||
microsoft_docs_search(query="{technology} concepts architecture")
|
||||
microsoft_docs_search(query="{technology} getting started tutorial")
|
||||
```
|
||||
|
||||
**Phase 2 - Core Content:**
|
||||
```
|
||||
microsoft_docs_fetch(url="...") # Fetch pages from Phase 1
|
||||
microsoft_code_sample_search(query="{technology}", language="{lang}")
|
||||
```
|
||||
|
||||
**Phase 3 - Depth:**
|
||||
```
|
||||
microsoft_docs_search(query="{technology} best practices")
|
||||
microsoft_docs_search(query="{technology} troubleshooting errors")
|
||||
```
|
||||
|
||||
#### Investigation Checklist
|
||||
|
||||
After investigating, verify:
|
||||
- [ ] Can explain what the technology does in one paragraph
|
||||
- [ ] Identified 3-5 key concepts
|
||||
- [ ] Have working code for basic usage
|
||||
- [ ] Know the most common API patterns
|
||||
- [ ] Have search queries for deeper topics
|
||||
|
||||
### Step 2: Clarify with User
|
||||
|
||||
Present findings and ask:
|
||||
1. "I found these key areas: [list]. Which are most important?"
|
||||
2. "What tasks will agents primarily perform with this skill?"
|
||||
3. "Which programming language should code samples prioritize?"
|
||||
|
||||
### Step 3: Generate the Skill
|
||||
|
||||
Use the appropriate template from [skill-templates.md](references/skill-templates.md):
|
||||
|
||||
| Technology Type | Template |
|
||||
|-----------------|----------|
|
||||
| Client library, NuGet/npm package | SDK/Library |
|
||||
| Azure resource | Azure Service |
|
||||
| App development framework | Framework/Platform |
|
||||
| REST API, protocol | API/Protocol |
|
||||
|
||||
#### Generated Skill Structure
|
||||
|
||||
```
|
||||
{skill-name}/
|
||||
├── SKILL.md # Core knowledge + Learn MCP guidance
|
||||
├── references/ # Detailed local documentation (if needed)
|
||||
└── sample_codes/ # Working code examples
|
||||
├── getting-started/
|
||||
└── common-patterns/
|
||||
```
|
||||
|
||||
### Step 4: Balance Local vs Dynamic Content
|
||||
|
||||
**Store locally when:**
|
||||
- Foundational (needed for any task)
|
||||
- Frequently accessed
|
||||
- Stable (won't change)
|
||||
- Hard to find via search
|
||||
|
||||
**Keep dynamic when:**
|
||||
- Exhaustive reference (too large)
|
||||
- Version-specific
|
||||
- Situational (specific tasks only)
|
||||
- Well-indexed (easy to search)
|
||||
|
||||
#### Content Guidelines
|
||||
|
||||
| Content Type | Local | Dynamic |
|
||||
|--------------|-------|---------|
|
||||
| Core concepts (3-5) | ✅ Full | |
|
||||
| Hello world code | ✅ Full | |
|
||||
| Common patterns (3-5) | ✅ Full | |
|
||||
| Top API methods | Signature + example | Full docs via fetch |
|
||||
| Best practices | Top 5 bullets | Search for more |
|
||||
| Troubleshooting | | Search queries |
|
||||
| Full API reference | | Doc links |
|
||||
|
||||
### Step 5: Validate
|
||||
|
||||
1. Review: Is local content sufficient for common tasks?
|
||||
2. Test: Do suggested search queries return useful results?
|
||||
3. Verify: Do code samples run without errors?
|
||||
|
||||
## Common Investigation Patterns
|
||||
|
||||
### For SDKs/Libraries
|
||||
```
|
||||
"{name} overview" → purpose, architecture
|
||||
"{name} getting started quickstart" → setup steps
|
||||
"{name} API reference" → core classes/methods
|
||||
"{name} samples examples" → code patterns
|
||||
"{name} best practices performance" → optimization
|
||||
```
|
||||
|
||||
### For Azure Services
|
||||
```
|
||||
"{service} overview features" → capabilities
|
||||
"{service} quickstart {language}" → setup code
|
||||
"{service} REST API reference" → endpoints
|
||||
"{service} SDK {language}" → client library
|
||||
"{service} pricing limits quotas" → constraints
|
||||
```
|
||||
|
||||
### For Frameworks/Platforms
|
||||
```
|
||||
"{framework} architecture concepts" → mental model
|
||||
"{framework} project structure" → conventions
|
||||
"{framework} tutorial walkthrough" → end-to-end flow
|
||||
"{framework} configuration options" → customization
|
||||
```
|
||||
|
||||
## Example: Creating a "Semantic Kernel" Skill
|
||||
|
||||
### Investigation
|
||||
|
||||
```
|
||||
microsoft_docs_search(query="semantic kernel overview")
|
||||
microsoft_docs_search(query="semantic kernel plugins functions")
|
||||
microsoft_code_sample_search(query="semantic kernel", language="csharp")
|
||||
microsoft_docs_fetch(url="https://learn.microsoft.com/semantic-kernel/overview/")
|
||||
```
|
||||
|
||||
### Generated Skill
|
||||
|
||||
```
|
||||
semantic-kernel/
|
||||
├── SKILL.md
|
||||
└── sample_codes/
|
||||
├── getting-started/
|
||||
│ └── hello-kernel.cs
|
||||
└── common-patterns/
|
||||
├── chat-completion.cs
|
||||
└── function-calling.cs
|
||||
```
|
||||
|
||||
### Generated SKILL.md
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: semantic-kernel
|
||||
description: Build AI agents with Microsoft Semantic Kernel. Use for LLM-powered apps with plugins, planners, and memory in .NET or Python.
|
||||
---
|
||||
|
||||
# Semantic Kernel
|
||||
|
||||
Orchestration SDK for integrating LLMs into applications with plugins, planners, and memory.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Kernel**: Central orchestrator managing AI services and plugins
|
||||
- **Plugins**: Collections of functions the AI can call
|
||||
- **Planner**: Sequences plugin functions to achieve goals
|
||||
- **Memory**: Vector store integration for RAG patterns
|
||||
|
||||
## Quick Start
|
||||
|
||||
See [getting-started/hello-kernel.cs](sample_codes/getting-started/hello-kernel.cs)
|
||||
|
||||
## Learn More
|
||||
|
||||
| Topic | How to Find |
|
||||
|-------|-------------|
|
||||
| Plugin development | `microsoft_docs_search(query="semantic kernel plugins custom functions")` |
|
||||
| Planners | `microsoft_docs_search(query="semantic kernel planner")` |
|
||||
| Memory | `microsoft_docs_fetch(url="https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent/agent-memory")` |
|
||||
```
|
||||
333
skills/microsoft-skill-creator/references/skill-templates.md
Normal file
333
skills/microsoft-skill-creator/references/skill-templates.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# Skill Templates
|
||||
|
||||
Ready-to-use templates for different types of Microsoft technologies.
|
||||
|
||||
## Template 1: SDK/Library Skill
|
||||
|
||||
For client libraries, SDKs, and programming frameworks.
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: {sdk-name}
|
||||
description: {What it does}. Use when agents need to {primary task} with {technology context}. Supports {languages/platforms}.
|
||||
---
|
||||
|
||||
# {SDK Name}
|
||||
|
||||
{One paragraph: what it is, why it exists, when to use it}
|
||||
|
||||
## Installation
|
||||
|
||||
{Package manager commands for supported languages}
|
||||
|
||||
## Key Concepts
|
||||
|
||||
{3-5 essential concepts, one paragraph each max}
|
||||
|
||||
### {Concept 1}
|
||||
{Brief explanation}
|
||||
|
||||
### {Concept 2}
|
||||
{Brief explanation}
|
||||
|
||||
## Quick Start
|
||||
|
||||
{Minimal working example - inline if <30 lines, otherwise reference sample_codes/}
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### {Pattern 1: e.g., "Basic CRUD"}
|
||||
```{language}
|
||||
{code}
|
||||
```
|
||||
|
||||
### {Pattern 2: e.g., "Error Handling"}
|
||||
```{language}
|
||||
{code}
|
||||
```
|
||||
|
||||
## API Quick Reference
|
||||
|
||||
| Class/Method | Purpose | Example |
|
||||
|--------------|---------|---------|
|
||||
| {name} | {what it does} | `{usage}` |
|
||||
|
||||
For full API documentation:
|
||||
- `microsoft_docs_search(query="{sdk} {class} API reference")`
|
||||
- `microsoft_docs_fetch(url="{url}")`
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Do**: {recommendation}
|
||||
- **Do**: {recommendation}
|
||||
- **Avoid**: {anti-pattern}
|
||||
|
||||
See [best-practices.md](references/best-practices.md) for detailed guidance.
|
||||
|
||||
## Learn More
|
||||
|
||||
| Topic | How to Find |
|
||||
|-------|-------------|
|
||||
| {Advanced topic 1} | `microsoft_docs_search(query="{sdk} {topic}")` |
|
||||
| {Advanced topic 2} | `microsoft_docs_fetch(url="{url}")` |
|
||||
| {Code examples} | `microsoft_code_sample_search(query="{sdk} {scenario}", language="{lang}")` |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 2: Azure Service Skill
|
||||
|
||||
For Azure services and cloud resources.
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: {service-name}
|
||||
description: Work with {Azure Service}. Use when agents need to {primary capabilities}. Covers provisioning, configuration, and SDK usage.
|
||||
---
|
||||
|
||||
# {Azure Service Name}
|
||||
|
||||
{One paragraph: what the service does, primary use cases}
|
||||
|
||||
## Overview
|
||||
|
||||
- **Category**: {Compute/Storage/AI/Networking/etc.}
|
||||
- **Key capability**: {main value proposition}
|
||||
- **When to use**: {scenarios}
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- Azure subscription
|
||||
- {Other requirements}
|
||||
|
||||
### Provisioning
|
||||
{CLI/Portal/Bicep snippet for creating the resource}
|
||||
|
||||
## SDK Usage ({Language})
|
||||
|
||||
### Installation
|
||||
```
|
||||
{package install command}
|
||||
```
|
||||
|
||||
### Authentication
|
||||
```{language}
|
||||
{auth code pattern}
|
||||
```
|
||||
|
||||
### Basic Operations
|
||||
```{language}
|
||||
{CRUD or primary operations}
|
||||
```
|
||||
|
||||
## Key Configurations
|
||||
|
||||
| Setting | Purpose | Default |
|
||||
|---------|---------|---------|
|
||||
| {setting} | {what it controls} | {value} |
|
||||
|
||||
## Pricing & Limits
|
||||
|
||||
- **Pricing model**: {consumption/tier-based/etc.}
|
||||
- **Key limits**: {important quotas}
|
||||
|
||||
For current pricing: `microsoft_docs_search(query="{service} pricing")`
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### {Pattern 1}
|
||||
{Code or configuration}
|
||||
|
||||
### {Pattern 2}
|
||||
{Code or configuration}
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| {Common error} | {Fix} |
|
||||
|
||||
For more issues: `microsoft_docs_search(query="{service} troubleshoot {symptom}")`
|
||||
|
||||
## Learn More
|
||||
|
||||
| Topic | How to Find |
|
||||
|-------|-------------|
|
||||
| REST API | `microsoft_docs_fetch(url="{url}")` |
|
||||
| ARM/Bicep | `microsoft_docs_search(query="{service} bicep template")` |
|
||||
| Security | `microsoft_docs_search(query="{service} security best practices")` |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 3: Framework/Platform Skill
|
||||
|
||||
For development frameworks and platforms (e.g., ASP.NET, MAUI, Blazor).
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: {framework-name}
|
||||
description: Build {type of apps} with {Framework}. Use when agents need to create, modify, or debug {framework} applications.
|
||||
---
|
||||
|
||||
# {Framework Name}
|
||||
|
||||
{One paragraph: what it is, what you build with it, why choose it}
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
{typical-project}/
|
||||
├── {folder}/ # {purpose}
|
||||
├── {file} # {purpose}
|
||||
└── {file} # {purpose}
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Create New Project
|
||||
```bash
|
||||
{CLI command to scaffold}
|
||||
```
|
||||
|
||||
### Project Configuration
|
||||
{Key files to configure and what they control}
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### {Concept 1: e.g., "Components"}
|
||||
{Explanation with minimal code example}
|
||||
|
||||
### {Concept 2: e.g., "Routing"}
|
||||
{Explanation with minimal code example}
|
||||
|
||||
### {Concept 3: e.g., "State Management"}
|
||||
{Explanation with minimal code example}
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### {Pattern 1}
|
||||
```{language}
|
||||
{code}
|
||||
```
|
||||
|
||||
### {Pattern 2}
|
||||
```{language}
|
||||
{code}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
| Setting | File | Purpose |
|
||||
|---------|------|---------|
|
||||
| {setting} | {file} | {what it does} |
|
||||
|
||||
## Deployment
|
||||
|
||||
{Brief deployment guidance or reference}
|
||||
|
||||
For detailed deployment: `microsoft_docs_search(query="{framework} deploy {target}")`
|
||||
|
||||
## Learn More
|
||||
|
||||
| Topic | How to Find |
|
||||
|-------|-------------|
|
||||
| {Advanced feature} | `microsoft_docs_search(query="{framework} {feature}")` |
|
||||
| {Integration} | `microsoft_docs_fetch(url="{url}")` |
|
||||
| {Samples} | `microsoft_code_sample_search(query="{framework} {scenario}")` |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 4: API/Protocol Skill
|
||||
|
||||
For APIs, protocols, and specifications (e.g., Microsoft Graph, OOXML).
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: {api-name}
|
||||
description: Interact with {API/Protocol}. Use when agents need to {primary operations}. Covers authentication, endpoints, and common operations.
|
||||
---
|
||||
|
||||
# {API/Protocol Name}
|
||||
|
||||
{One paragraph: what it provides access to, primary use cases}
|
||||
|
||||
## Authentication
|
||||
|
||||
{Auth method and code pattern}
|
||||
|
||||
## Base Configuration
|
||||
|
||||
- **Base URL**: `{url}`
|
||||
- **Version**: `{version}`
|
||||
- **Format**: {JSON/XML/etc.}
|
||||
|
||||
## Common Endpoints/Operations
|
||||
|
||||
### {Operation 1: e.g., "List Items"}
|
||||
```
|
||||
{HTTP method} {endpoint}
|
||||
```
|
||||
```{language}
|
||||
{SDK code}
|
||||
```
|
||||
|
||||
### {Operation 2: e.g., "Create Item"}
|
||||
```
|
||||
{HTTP method} {endpoint}
|
||||
```
|
||||
```{language}
|
||||
{SDK code}
|
||||
```
|
||||
|
||||
## Request/Response Patterns
|
||||
|
||||
### Pagination
|
||||
{How to handle pagination}
|
||||
|
||||
### Error Handling
|
||||
{Error format and common codes}
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Operation | Endpoint/Method | Notes |
|
||||
|-----------|-----------------|-------|
|
||||
| {op} | `{endpoint}` | {note} |
|
||||
|
||||
## Permissions/Scopes
|
||||
|
||||
| Operation | Required Permission |
|
||||
|-----------|---------------------|
|
||||
| {op} | `{permission}` |
|
||||
|
||||
## Learn More
|
||||
|
||||
| Topic | How to Find |
|
||||
|-------|-------------|
|
||||
| Full endpoint reference | `microsoft_docs_fetch(url="{url}")` |
|
||||
| Permissions | `microsoft_docs_search(query="{api} permissions {resource}")` |
|
||||
| SDKs | `microsoft_docs_search(query="{api} SDK {language}")` |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Choosing a Template
|
||||
|
||||
| Technology Type | Template | Examples |
|
||||
|-----------------|----------|----------|
|
||||
| Client library, NuGet/npm package | SDK/Library | Semantic Kernel, Azure SDK, MSAL |
|
||||
| Azure resource | Azure Service | Cosmos DB, Azure Functions, App Service |
|
||||
| App development framework | Framework/Platform | ASP.NET Core, Blazor, MAUI |
|
||||
| REST API, protocol, specification | API/Protocol | Microsoft Graph, OOXML, FHIR |
|
||||
|
||||
## Customization Guidelines
|
||||
|
||||
Templates are starting points. Customize by:
|
||||
|
||||
1. **Adding sections** for unique aspects of the technology
|
||||
2. **Removing sections** that don't apply
|
||||
3. **Adjusting depth** based on complexity (more concepts for complex tech)
|
||||
4. **Adding reference files** for detailed content that doesn't fit in SKILL.md
|
||||
5. **Adding sample_codes/** for working examples beyond inline snippets
|
||||
@@ -5,6 +5,7 @@
|
||||
import { FuzzySearch, type SearchableItem } from "../search";
|
||||
import { fetchData, escapeHtml } from "../utils";
|
||||
import { createChoices, getChoicesValues, type Choices } from "../choices";
|
||||
import { setupModal } from "../modal";
|
||||
|
||||
// Types
|
||||
interface Language {
|
||||
@@ -82,6 +83,7 @@ export async function initSamplesPage(): Promise<void> {
|
||||
search = new FuzzySearch(allRecipes);
|
||||
|
||||
// Setup UI
|
||||
setupModal();
|
||||
setupFilters();
|
||||
setupSearch();
|
||||
renderCookbooks();
|
||||
|
||||
Reference in New Issue
Block a user