From 6b1ac50007e5bc96819fc9217d42682baa6a0cb1 Mon Sep 17 00:00:00 2001 From: Tianqi Zhang Date: Tue, 10 Feb 2026 14:03:06 +0800 Subject: [PATCH 01/21] Add microsoft-skill-creator skill --- docs/README.skills.md | 1 + skills/microsoft-skill-creator/SKILL.md | 217 ++++++++++++ .../references/skill-templates.md | 333 ++++++++++++++++++ 3 files changed, 551 insertions(+) create mode 100644 skills/microsoft-skill-creator/SKILL.md create mode 100644 skills/microsoft-skill-creator/references/skill-templates.md diff --git a/docs/README.skills.md b/docs/README.skills.md index 60525266..a7d407ec 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -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` | | [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`
`references/component-patterns.md`
`references/platform-guidelines.md`
`references/setup-troubleshooting.md` | | [plantuml-ascii](../skills/plantuml-ascii/SKILL.md) | Generate ASCII art diagrams using PlantUML text mode. Use when user asks to create ASCII diagrams, text-based diagrams, terminal-friendly diagrams, or mentions plantuml ascii, text diagram, ascii art diagram. Supports: Converting PlantUML diagrams to ASCII art, Creating sequence diagrams, class diagrams, flowcharts in ASCII format, Generating Unicode-enhanced ASCII art with -utxt flag | None | diff --git a/skills/microsoft-skill-creator/SKILL.md b/skills/microsoft-skill-creator/SKILL.md new file mode 100644 index 00000000..545c40bd --- /dev/null +++ b/skills/microsoft-skill-creator/SKILL.md @@ -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")` | +``` diff --git a/skills/microsoft-skill-creator/references/skill-templates.md b/skills/microsoft-skill-creator/references/skill-templates.md new file mode 100644 index 00000000..dc234300 --- /dev/null +++ b/skills/microsoft-skill-creator/references/skill-templates.md @@ -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 From 5ba0ed2f196bc13b75ae250b09317f5a38615cdf Mon Sep 17 00:00:00 2001 From: abhibavishi Date: Wed, 11 Feb 2026 15:48:12 +0530 Subject: [PATCH 02/21] Add Reepl LinkedIn content agent Add an agent for AI-powered LinkedIn content creation, scheduling, and analytics through Reepl (https://reepl.io). Enables creating posts, carousels, and managing LinkedIn presence directly from GitHub Copilot. Co-Authored-By: Claude Opus 4.6 --- agents/reepl-linkedin.agent.md | 42 ++++++++++++++++++++++++++++++++++ docs/README.agents.md | 1 + 2 files changed, 43 insertions(+) create mode 100644 agents/reepl-linkedin.agent.md diff --git a/agents/reepl-linkedin.agent.md b/agents/reepl-linkedin.agent.md new file mode 100644 index 00000000..df414840 --- /dev/null +++ b/agents/reepl-linkedin.agent.md @@ -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. diff --git a/docs/README.agents.md b/docs/README.agents.md index f394dd2a..bf44c448 100644 --- a/docs/README.agents.md +++ b/docs/README.agents.md @@ -123,6 +123,7 @@ Custom agents for GitHub Copilot, making it easy for users and organizations to | [Prompt Builder](../agents/prompt-builder.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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 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)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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. | | From d8fc473383fe1b52ce26f1a600acd61db2ced013 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 04:59:20 -0800 Subject: [PATCH 03/21] Add RALPH-loop recipe to Copilot SDK cookbook Add iterative RALPH-loop (Read, Act, Log, Persist, Halt) pattern implementations for all four supported languages: - C#/.NET: ralph-loop.cs with documentation - Node.js/TypeScript: ralph-loop.ts with documentation - Python: ralph_loop.py with documentation (async API) - Go: ralph-loop.go with documentation Each recipe demonstrates: - Self-referential iteration where AI reviews its own output - Completion promise detection to halt the loop - Max iteration safety limits - File persistence between iterations Verified against real Copilot SDK APIs: - Python: fully verified end-to-end with github-copilot-sdk - Node.js: fully verified end-to-end with @github/copilot-sdk - C#: compiles and runs successfully with GitHub.Copilot.SDK - Go: compiles against github.com/github/copilot-sdk/go v0.1.23 --- cookbook/copilot-sdk/README.md | 6 +- cookbook/copilot-sdk/dotnet/ralph-loop.md | 238 +++++++++++++++++ .../copilot-sdk/dotnet/recipe/ralph-loop.cs | 132 ++++++++++ cookbook/copilot-sdk/go/ralph-loop.md | 240 ++++++++++++++++++ cookbook/copilot-sdk/go/recipe/ralph-loop.go | 130 ++++++++++ cookbook/copilot-sdk/nodejs/ralph-loop.md | 209 +++++++++++++++ .../copilot-sdk/nodejs/recipe/ralph-loop.ts | 121 +++++++++ cookbook/copilot-sdk/python/ralph-loop.md | 205 +++++++++++++++ .../copilot-sdk/python/recipe/ralph_loop.py | 123 +++++++++ 9 files changed, 1403 insertions(+), 1 deletion(-) create mode 100644 cookbook/copilot-sdk/dotnet/ralph-loop.md create mode 100644 cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs create mode 100644 cookbook/copilot-sdk/go/ralph-loop.md create mode 100644 cookbook/copilot-sdk/go/recipe/ralph-loop.go create mode 100644 cookbook/copilot-sdk/nodejs/ralph-loop.md create mode 100644 cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts create mode 100644 cookbook/copilot-sdk/python/ralph-loop.md create mode 100644 cookbook/copilot-sdk/python/recipe/ralph_loop.py diff --git a/cookbook/copilot-sdk/README.md b/cookbook/copilot-sdk/README.md index 53c91a70..c4892687 100644 --- a/cookbook/copilot-sdk/README.md +++ b/cookbook/copilot-sdk/README.md @@ -6,6 +6,7 @@ This cookbook collects small, focused recipes showing how to accomplish common t ### .NET (C#) +- [RALPH-loop](dotnet/ralph-loop.md): Implement iterative self-referential AI loops for task completion with automatic retries. - [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. @@ -14,6 +15,7 @@ This cookbook collects small, focused recipes showing how to accomplish common t ### Node.js / TypeScript +- [RALPH-loop](nodejs/ralph-loop.md): Implement iterative self-referential AI loops for task completion with automatic retries. - [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. @@ -22,6 +24,7 @@ This cookbook collects small, focused recipes showing how to accomplish common t ### Python +- [RALPH-loop](python/ralph-loop.md): Implement iterative self-referential AI loops for task completion with automatic retries. - [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. @@ -30,6 +33,7 @@ This cookbook collects small, focused recipes showing how to accomplish common t ### Go +- [RALPH-loop](go/ralph-loop.md): Implement iterative self-referential AI loops for task completion with automatic retries. - [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. @@ -83,4 +87,4 @@ go run .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 5 recipes across all 4 supported languages. Each recipe includes both markdown documentation and runnable examples. The RALPH-loop recipe demonstrates iterative self-referential AI loops for autonomous task completion. diff --git a/cookbook/copilot-sdk/dotnet/ralph-loop.md b/cookbook/copilot-sdk/dotnet/ralph-loop.md new file mode 100644 index 00000000..43d22e72 --- /dev/null +++ b/cookbook/copilot-sdk/dotnet/ralph-loop.md @@ -0,0 +1,238 @@ +# RALPH-loop: Iterative Self-Referential AI Loops + +Implement self-referential feedback loops where an AI agent iteratively improves work by reading its own previous output. + +> **Runnable example:** [recipe/ralph-loop.cs](recipe/ralph-loop.cs) +> +> ```bash +> cd dotnet/recipe +> dotnet run ralph-loop.cs +> ``` + +## What is RALPH-loop? + +RALPH-loop is a development methodology for iterative AI-powered task completion. Named after the Ralph Wiggum technique, it embodies the philosophy of persistent iteration: + +- **One prompt, multiple iterations**: The same prompt is processed repeatedly +- **Self-referential feedback**: The AI reads its own previous work (file changes, git history) +- **Completion detection**: Loop exits when a completion promise is detected in output +- **Safety limits**: Always include a maximum iteration count to prevent infinite loops + +## Example Scenario + +You need to iteratively improve code until all tests pass. Instead of asking Claude to "write perfect code," you use RALPH-loop to: + +1. Send the initial prompt with clear success criteria +2. Claude writes code and tests +3. Claude runs tests and sees failures +4. Loop automatically re-sends the prompt +5. Claude reads test output and previous code, fixes issues +6. Repeat until all tests pass and completion promise is output + +## Basic Implementation + +```csharp +using GitHub.Copilot.SDK; + +public class RalphLoop +{ + private readonly CopilotClient _client; + private int _iteration = 0; + private readonly int _maxIterations; + private readonly string _completionPromise; + private string? _lastResponse; + + public RalphLoop(int maxIterations = 10, string completionPromise = "COMPLETE") + { + _client = new CopilotClient(); + _maxIterations = maxIterations; + _completionPromise = completionPromise; + } + + public async Task RunAsync(string prompt) + { + await _client.StartAsync(); + var session = await _client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" }); + + try + { + while (_iteration < _maxIterations) + { + _iteration++; + Console.WriteLine($"\n--- Iteration {_iteration} ---"); + + var done = new TaskCompletionSource(); + session.On(evt => + { + if (evt is AssistantMessageEvent msg) + { + _lastResponse = msg.Data.Content; + done.SetResult(msg.Data.Content); + } + }); + + // Send prompt (on first iteration) or continuation + var messagePrompt = _iteration == 1 + ? prompt + : $"{prompt}\n\nPrevious attempt:\n{_lastResponse}\n\nContinue iterating..."; + + await session.SendAsync(new MessageOptions { Prompt = messagePrompt }); + var response = await done.Task; + + // Check for completion promise + if (response.Contains(_completionPromise)) + { + Console.WriteLine($"✓ Completion promise detected: {_completionPromise}"); + return response; + } + + Console.WriteLine($"Iteration {_iteration} complete. Continuing..."); + } + + throw new InvalidOperationException( + $"Max iterations ({_maxIterations}) reached without completion promise"); + } + finally + { + await session.DisposeAsync(); + await _client.StopAsync(); + } + } +} + +// Usage +var loop = new RalphLoop(maxIterations: 5, completionPromise: "DONE"); +var result = await loop.RunAsync("Your task here"); +Console.WriteLine(result); +``` + +## With File Persistence + +For tasks involving code generation, persist state to files so the AI can see changes: + +```csharp +public class PersistentRalphLoop +{ + private readonly string _workDir; + private readonly CopilotClient _client; + private int _iteration = 0; + + public PersistentRalphLoop(string workDir, int maxIterations = 10) + { + _workDir = workDir; + Directory.CreateDirectory(_workDir); + _client = new CopilotClient(); + } + + public async Task RunAsync(string prompt) + { + await _client.StartAsync(); + var session = await _client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" }); + + try + { + // Store initial prompt + var promptFile = Path.Combine(_workDir, "prompt.md"); + await File.WriteAllTextAsync(promptFile, prompt); + + while (_iteration < 10) + { + _iteration++; + Console.WriteLine($"\n--- Iteration {_iteration} ---"); + + // Build context including previous work + var contextBuilder = new StringBuilder(prompt); + var previousOutput = Path.Combine(_workDir, $"output-{_iteration - 1}.txt"); + if (File.Exists(previousOutput)) + { + contextBuilder.AppendLine($"\nPrevious iteration output:\n{await File.ReadAllTextAsync(previousOutput)}"); + } + + var done = new TaskCompletionSource(); + string response = ""; + session.On(evt => + { + if (evt is AssistantMessageEvent msg) + { + response = msg.Data.Content; + done.SetResult(msg.Data.Content); + } + }); + + await session.SendAsync(new MessageOptions { Prompt = contextBuilder.ToString() }); + await done.Task; + + // Persist output + await File.WriteAllTextAsync( + Path.Combine(_workDir, $"output-{_iteration}.txt"), + response); + + if (response.Contains("COMPLETE")) + { + return response; + } + } + + throw new InvalidOperationException("Max iterations reached"); + } + finally + { + await session.DisposeAsync(); + await _client.StopAsync(); + } + } +} +``` + +## Best Practices + +1. **Write clear completion criteria**: Include exactly what "done" looks like +2. **Use output markers**: Include `COMPLETE` or similar in completion condition +3. **Always set max iterations**: Prevents infinite loops on impossible tasks +4. **Persist state**: Save files so AI can see what changed between iterations +5. **Include context**: Feed previous iteration output back as context +6. **Monitor progress**: Log each iteration to track what's happening + +## Example: Iterative Code Generation + +```csharp +var prompt = @"Write a function that: +1. Parses CSV data +2. Validates required fields +3. Returns parsed records or error +4. Has unit tests +5. Output COMPLETE when done"; + +var loop = new RalphLoop(maxIterations: 10, completionPromise: "COMPLETE"); +var result = await loop.RunAsync(prompt); +``` + +## Handling Failures + +```csharp +try +{ + var result = await loop.RunAsync(prompt); + Console.WriteLine("Task completed successfully!"); +} +catch (InvalidOperationException ex) when (ex.Message.Contains("Max iterations")) +{ + Console.WriteLine("Task did not complete within iteration limit."); + Console.WriteLine($"Last response: {loop.LastResponse}"); + // Document what was attempted and suggest alternatives +} +``` + +## When to Use RALPH-loop + +**Good for:** +- Code generation with automatic verification (tests, linters) +- Tasks with clear success criteria +- Iterative refinement where each attempt learns from previous failures +- Unattended long-running improvements + +**Not good for:** +- Tasks requiring human judgment or design input +- One-shot operations +- Tasks with vague success criteria +- Real-time interactive debugging diff --git a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs new file mode 100644 index 00000000..a1297656 --- /dev/null +++ b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs @@ -0,0 +1,132 @@ +#:package GitHub.Copilot.SDK@* +#:property PublishAot=false + +using GitHub.Copilot.SDK; +using System.Text; + +// RALPH-loop: Iterative self-referential AI loops. +// The same prompt is sent repeatedly, with AI reading its own previous output. +// Loop continues until completion promise is detected in the response. + +var prompt = @"You are iteratively building a small library. Follow these phases IN ORDER. +Do NOT skip ahead — only do the current phase, then stop and wait for the next iteration. + +Phase 1: Design a DataValidator class that validates records against a schema. + - Schema defines field names, types (string, int, float, bool), and whether required. + - Return a list of validation errors per record. + - Show the class code only. Do NOT output COMPLETE. + +Phase 2: Write at least 4 unit tests covering: missing required field, wrong type, + valid record, and empty input. Show test code only. Do NOT output COMPLETE. + +Phase 3: Review the code from phases 1 and 2. Fix any bugs, add docstrings, and add + an extra edge-case test. Show the final consolidated code with all fixes. + When this phase is fully done, output the exact text: COMPLETE"; + +var loop = new RalphLoop(maxIterations: 5, completionPromise: "COMPLETE"); + +try +{ + var result = await loop.RunAsync(prompt); + Console.WriteLine("\n=== FINAL RESULT ==="); + Console.WriteLine(result); +} +catch (InvalidOperationException ex) +{ + Console.WriteLine($"\nTask did not complete: {ex.Message}"); + if (loop.LastResponse != null) + { + Console.WriteLine($"\nLast attempt:\n{loop.LastResponse}"); + } +} + +// --- RalphLoop class definition --- + +public class RalphLoop +{ + private readonly CopilotClient _client; + private int _iteration = 0; + private readonly int _maxIterations; + private readonly string _completionPromise; + private string? _lastResponse; + + public RalphLoop(int maxIterations = 10, string completionPromise = "COMPLETE") + { + _client = new CopilotClient(); + _maxIterations = maxIterations; + _completionPromise = completionPromise; + } + + public string? LastResponse => _lastResponse; + + public async Task RunAsync(string initialPrompt) + { + await _client.StartAsync(); + var session = await _client.CreateSessionAsync(new SessionConfig + { + Model = "gpt-5.1-codex-mini" + }); + + try + { + while (_iteration < _maxIterations) + { + _iteration++; + Console.WriteLine($"\n=== Iteration {_iteration}/{_maxIterations} ==="); + + var done = new TaskCompletionSource(); + session.On(evt => + { + if (evt is AssistantMessageEvent msg) + { + _lastResponse = msg.Data.Content; + done.SetResult(msg.Data.Content); + } + }); + + var currentPrompt = BuildIterationPrompt(initialPrompt); + Console.WriteLine($"Sending prompt (length: {currentPrompt.Length})..."); + + await session.SendAsync(new MessageOptions { Prompt = currentPrompt }); + var response = await done.Task; + + var summary = response.Length > 200 + ? response.Substring(0, 200) + "..." + : response; + Console.WriteLine($"Response: {summary}"); + + if (response.Contains(_completionPromise)) + { + Console.WriteLine($"\n✓ Completion promise detected: '{_completionPromise}'"); + return response; + } + + Console.WriteLine($"Iteration {_iteration} complete. Continuing..."); + } + + throw new InvalidOperationException( + $"Max iterations ({_maxIterations}) reached without completion promise: '{_completionPromise}'"); + } + finally + { + await session.DisposeAsync(); + await _client.StopAsync(); + } + } + + private string BuildIterationPrompt(string initialPrompt) + { + if (_iteration == 1) + return initialPrompt; + + var sb = new StringBuilder(); + sb.AppendLine(initialPrompt); + sb.AppendLine(); + sb.AppendLine("=== CONTEXT FROM PREVIOUS ITERATION ==="); + sb.AppendLine(_lastResponse); + sb.AppendLine("=== END CONTEXT ==="); + sb.AppendLine(); + sb.AppendLine("Continue working on this task. Review the previous attempt and improve upon it."); + return sb.ToString(); + } +} diff --git a/cookbook/copilot-sdk/go/ralph-loop.md b/cookbook/copilot-sdk/go/ralph-loop.md new file mode 100644 index 00000000..37879c0b --- /dev/null +++ b/cookbook/copilot-sdk/go/ralph-loop.md @@ -0,0 +1,240 @@ +# RALPH-loop: Iterative Self-Referential AI Loops + +Implement self-referential feedback loops where an AI agent iteratively improves work by reading its own previous output. + +> **Runnable example:** [recipe/ralph-loop.go](recipe/ralph-loop.go) +> +> ```bash +> cd go/recipe +> go run ralph-loop.go +> ``` + +## What is RALPH-loop? + +RALPH-loop is a development methodology for iterative AI-powered task completion. Named after the Ralph Wiggum technique, it embodies the philosophy of persistent iteration: + +- **One prompt, multiple iterations**: The same prompt is processed repeatedly +- **Self-referential feedback**: The AI reads its own previous work (file changes, git history) +- **Completion detection**: Loop exits when a completion promise is detected in output +- **Safety limits**: Always include a maximum iteration count to prevent infinite loops + +## Example Scenario + +You need to iteratively improve code until all tests pass. Instead of asking Claude to "write perfect code," you use RALPH-loop to: + +1. Send the initial prompt with clear success criteria +2. Claude writes code and tests +3. Claude runs tests and sees failures +4. Loop automatically re-sends the prompt +5. Claude reads test output and previous code, fixes issues +6. Repeat until all tests pass and completion promise is output + +## Basic Implementation + +```go +package main + +import ( + "context" + "fmt" + "log" + "strings" + + copilot "github.com/github/copilot-sdk/go" +) + +type RalphLoop struct { + client *copilot.Client + iteration int + maxIterations int + completionPromise string + LastResponse string +} + +func NewRalphLoop(maxIterations int, completionPromise string) *RalphLoop { + return &RalphLoop{ + client: copilot.NewClient(nil), + maxIterations: maxIterations, + completionPromise: completionPromise, + } +} + +func (r *RalphLoop) Run(ctx context.Context, initialPrompt string) (string, error) { + if err := r.client.Start(ctx); err != nil { + return "", err + } + defer r.client.Stop() + + session, err := r.client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-5.1-codex-mini", + }) + if err != nil { + return "", err + } + defer session.Destroy() + + for r.iteration < r.maxIterations { + r.iteration++ + fmt.Printf("\n--- Iteration %d/%d ---\n", r.iteration, r.maxIterations) + + prompt := r.buildIterationPrompt(initialPrompt) + + result, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt}) + if err != nil { + return "", err + } + + if result != nil && result.Data.Content != nil { + r.LastResponse = *result.Data.Content + } + + if strings.Contains(r.LastResponse, r.completionPromise) { + fmt.Printf("✓ Completion promise detected: %s\n", r.completionPromise) + return r.LastResponse, nil + } + } + + return "", fmt.Errorf("max iterations (%d) reached without completion promise", + r.maxIterations) +} + +// Usage +func main() { + ctx := context.Background() + loop := NewRalphLoop(5, "COMPLETE") + result, err := loop.Run(ctx, "Your task here") + if err != nil { + log.Fatal(err) + } + fmt.Println(result) +} +``` + +## With File Persistence + +For tasks involving code generation, persist state to files so the AI can see changes: + +```go +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + copilot "github.com/github/copilot-sdk/go" +) + +type PersistentRalphLoop struct { + client *copilot.Client + workDir string + iteration int + maxIterations int +} + +func NewPersistentRalphLoop(workDir string, maxIterations int) *PersistentRalphLoop { + os.MkdirAll(workDir, 0755) + return &PersistentRalphLoop{ + client: copilot.NewClient(nil), + workDir: workDir, + maxIterations: maxIterations, + } +} + +func (p *PersistentRalphLoop) Run(ctx context.Context, initialPrompt string) (string, error) { + if err := p.client.Start(ctx); err != nil { + return "", err + } + defer p.client.Stop() + + os.WriteFile(filepath.Join(p.workDir, "prompt.md"), []byte(initialPrompt), 0644) + + session, err := p.client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-5.1-codex-mini", + }) + if err != nil { + return "", err + } + defer session.Destroy() + + for p.iteration < p.maxIterations { + p.iteration++ + + prompt := initialPrompt + prevFile := filepath.Join(p.workDir, fmt.Sprintf("output-%d.txt", p.iteration-1)) + if data, err := os.ReadFile(prevFile); err == nil { + prompt = fmt.Sprintf("%s\n\nPrevious iteration:\n%s", initialPrompt, string(data)) + } + + result, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt}) + if err != nil { + return "", err + } + + response := "" + if result != nil && result.Data.Content != nil { + response = *result.Data.Content + } + + os.WriteFile(filepath.Join(p.workDir, fmt.Sprintf("output-%d.txt", p.iteration)), + []byte(response), 0644) + + if strings.Contains(response, "COMPLETE") { + return response, nil + } + } + + return "", fmt.Errorf("max iterations reached") +} +``` + +## Best Practices + +1. **Write clear completion criteria**: Include exactly what "done" looks like +2. **Use output markers**: Include `COMPLETE` or similar in completion condition +3. **Always set max iterations**: Prevents infinite loops on impossible tasks +4. **Persist state**: Save files so AI can see what changed between iterations +5. **Include context**: Feed previous iteration output back as context +6. **Monitor progress**: Log each iteration to track what's happening + +## Example: Iterative Code Generation + +```go +prompt := `Write a function that: +1. Parses CSV data +2. Validates required fields +3. Returns parsed records or error +4. Has unit tests +5. Output COMPLETE when done` + +loop := NewRalphLoop(10, "COMPLETE") +result, err := loop.Run(context.Background(), prompt) +``` + +## Handling Failures + +```go +ctx := context.Background() +loop := NewRalphLoop(5, "COMPLETE") +result, err := loop.Run(ctx, prompt) +if err != nil { + log.Printf("Task failed: %v", err) + log.Printf("Last attempt: %s", loop.LastResponse) +} +``` + +## When to Use RALPH-loop + +**Good for:** +- Code generation with automatic verification (tests, linters) +- Tasks with clear success criteria +- Iterative refinement where each attempt learns from previous failures +- Unattended long-running improvements + +**Not good for:** +- Tasks requiring human judgment or design input +- One-shot operations +- Tasks with vague success criteria +- Real-time interactive debugging diff --git a/cookbook/copilot-sdk/go/recipe/ralph-loop.go b/cookbook/copilot-sdk/go/recipe/ralph-loop.go new file mode 100644 index 00000000..b99fe54d --- /dev/null +++ b/cookbook/copilot-sdk/go/recipe/ralph-loop.go @@ -0,0 +1,130 @@ +package main + +import ( + "context" + "fmt" + "log" + "strings" + + copilot "github.com/github/copilot-sdk/go" +) + +// RalphLoop implements iterative self-referential feedback loops. +// The same prompt is sent repeatedly, with AI reading its own previous output. +// Loop continues until completion promise is detected in the response. +type RalphLoop struct { + client *copilot.Client + iteration int + maxIterations int + completionPromise string + LastResponse string +} + +// NewRalphLoop creates a new RALPH-loop instance. +func NewRalphLoop(maxIterations int, completionPromise string) *RalphLoop { + return &RalphLoop{ + client: copilot.NewClient(nil), + maxIterations: maxIterations, + completionPromise: completionPromise, + } +} + +// Run executes the RALPH-loop until completion promise is detected or max iterations reached. +func (r *RalphLoop) Run(ctx context.Context, initialPrompt string) (string, error) { + if err := r.client.Start(ctx); err != nil { + return "", fmt.Errorf("failed to start client: %w", err) + } + defer r.client.Stop() + + session, err := r.client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-5.1-codex-mini", + }) + if err != nil { + return "", fmt.Errorf("failed to create session: %w", err) + } + defer session.Destroy() + + for r.iteration < r.maxIterations { + r.iteration++ + fmt.Printf("\n=== Iteration %d/%d ===\n", r.iteration, r.maxIterations) + + currentPrompt := r.buildIterationPrompt(initialPrompt) + fmt.Printf("Sending prompt (length: %d)...\n", len(currentPrompt)) + + result, err := session.SendAndWait(ctx, copilot.MessageOptions{ + Prompt: currentPrompt, + }) + if err != nil { + return "", fmt.Errorf("send failed on iteration %d: %w", r.iteration, err) + } + + if result != nil && result.Data.Content != nil { + r.LastResponse = *result.Data.Content + } else { + r.LastResponse = "" + } + + // Display response summary + summary := r.LastResponse + if len(summary) > 200 { + summary = summary[:200] + "..." + } + fmt.Printf("Response: %s\n", summary) + + // Check for completion promise + if strings.Contains(r.LastResponse, r.completionPromise) { + fmt.Printf("\n✓ Success! Completion promise detected: '%s'\n", r.completionPromise) + return r.LastResponse, nil + } + + fmt.Printf("Iteration %d complete. Continuing...\n", r.iteration) + } + + return "", fmt.Errorf("maximum iterations (%d) reached without detecting completion promise: '%s'", + r.maxIterations, r.completionPromise) +} + +func (r *RalphLoop) buildIterationPrompt(initialPrompt string) string { + if r.iteration == 1 { + return initialPrompt + } + + return fmt.Sprintf(`%s + +=== CONTEXT FROM PREVIOUS ITERATION === +%s +=== END CONTEXT === + +Continue working on this task. Review the previous attempt and improve upon it.`, + initialPrompt, r.LastResponse) +} + +func main() { + prompt := `You are iteratively building a small library. Follow these phases IN ORDER. +Do NOT skip ahead — only do the current phase, then stop and wait for the next iteration. + +Phase 1: Design a DataValidator struct that validates records against a schema. + - Schema defines field names, types (string, int, float, bool), and whether required. + - Return a slice of validation errors per record. + - Show the struct and method code only. Do NOT output COMPLETE. + +Phase 2: Write at least 4 unit tests covering: missing required field, wrong type, + valid record, and empty input. Show test code only. Do NOT output COMPLETE. + +Phase 3: Review the code from phases 1 and 2. Fix any bugs, add doc comments, and add + an extra edge-case test. Show the final consolidated code with all fixes. + When this phase is fully done, output the exact text: COMPLETE` + + ctx := context.Background() + loop := NewRalphLoop(5, "COMPLETE") + + result, err := loop.Run(ctx, prompt) + if err != nil { + log.Printf("Task did not complete: %v", err) + log.Printf("Last attempt: %s", loop.LastResponse) + return + } + + fmt.Println("\n=== FINAL RESULT ===") + fmt.Println(result) +} diff --git a/cookbook/copilot-sdk/nodejs/ralph-loop.md b/cookbook/copilot-sdk/nodejs/ralph-loop.md new file mode 100644 index 00000000..1ad70708 --- /dev/null +++ b/cookbook/copilot-sdk/nodejs/ralph-loop.md @@ -0,0 +1,209 @@ +# RALPH-loop: Iterative Self-Referential AI Loops + +Implement self-referential feedback loops where an AI agent iteratively improves work by reading its own previous output. + +> **Runnable example:** [recipe/ralph-loop.ts](recipe/ralph-loop.ts) +> +> ```bash +> cd nodejs/recipe +> npm install +> npx tsx ralph-loop.ts +> ``` + +## What is RALPH-loop? + +RALPH-loop is a development methodology for iterative AI-powered task completion. Named after the Ralph Wiggum technique, it embodies the philosophy of persistent iteration: + +- **One prompt, multiple iterations**: The same prompt is processed repeatedly +- **Self-referential feedback**: The AI reads its own previous work (file changes, git history) +- **Completion detection**: Loop exits when a completion promise is detected in output +- **Safety limits**: Always include a maximum iteration count to prevent infinite loops + +## Example Scenario + +You need to iteratively improve code until all tests pass. Instead of asking Claude to "write perfect code," you use RALPH-loop to: + +1. Send the initial prompt with clear success criteria +2. Claude writes code and tests +3. Claude runs tests and sees failures +4. Loop automatically re-sends the prompt +5. Claude reads test output and previous code, fixes issues +6. Repeat until all tests pass and completion promise is output + +## Basic Implementation + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +class RalphLoop { + private client: CopilotClient; + private iteration: number = 0; + private maxIterations: number; + private completionPromise: string; + private lastResponse: string | null = null; + + constructor(maxIterations: number = 10, completionPromise: string = "COMPLETE") { + this.client = new CopilotClient(); + this.maxIterations = maxIterations; + this.completionPromise = completionPromise; + } + + async run(initialPrompt: string): Promise { + await this.client.start(); + const session = await this.client.createSession({ model: "gpt-5" }); + + try { + while (this.iteration < this.maxIterations) { + this.iteration++; + console.log(`\n--- Iteration ${this.iteration}/${this.maxIterations} ---`); + + // Build prompt including previous response as context + const prompt = this.iteration === 1 + ? initialPrompt + : `${initialPrompt}\n\nPrevious attempt:\n${this.lastResponse}\n\nContinue improving...`; + + const response = await session.sendAndWait({ prompt }); + this.lastResponse = response?.data.content || ""; + + console.log(`Response (${this.lastResponse.length} chars)`); + + // Check for completion promise + if (this.lastResponse.includes(this.completionPromise)) { + console.log(`✓ Completion promise detected: ${this.completionPromise}`); + return this.lastResponse; + } + + console.log(`Continuing to iteration ${this.iteration + 1}...`); + } + + throw new Error( + `Max iterations (${this.maxIterations}) reached without completion promise` + ); + } finally { + await session.destroy(); + await this.client.stop(); + } + } +} + +// Usage +const loop = new RalphLoop(5, "COMPLETE"); +const result = await loop.run("Your task here"); +console.log(result); +``` + +## With File Persistence + +For tasks involving code generation, persist state to files so the AI can see changes: + +```typescript +import fs from "fs/promises"; +import path from "path"; +import { CopilotClient } from "@github/copilot-sdk"; + +class PersistentRalphLoop { + private client: CopilotClient; + private workDir: string; + private iteration: number = 0; + private maxIterations: number; + + constructor(workDir: string, maxIterations: number = 10) { + this.client = new CopilotClient(); + this.workDir = workDir; + this.maxIterations = maxIterations; + } + + async run(initialPrompt: string): Promise { + await fs.mkdir(this.workDir, { recursive: true }); + await this.client.start(); + const session = await this.client.createSession({ model: "gpt-5" }); + + try { + // Store initial prompt + await fs.writeFile(path.join(this.workDir, "prompt.md"), initialPrompt); + + while (this.iteration < this.maxIterations) { + this.iteration++; + console.log(`\n--- Iteration ${this.iteration} ---`); + + // Build context from previous outputs + let context = initialPrompt; + const prevOutputFile = path.join(this.workDir, `output-${this.iteration - 1}.txt`); + try { + const prevOutput = await fs.readFile(prevOutputFile, "utf-8"); + context += `\n\nPrevious iteration:\n${prevOutput}`; + } catch { + // No previous output yet + } + + const response = await session.sendAndWait({ prompt: context }); + const output = response?.data.content || ""; + + // Persist output + await fs.writeFile( + path.join(this.workDir, `output-${this.iteration}.txt`), + output + ); + + if (output.includes("COMPLETE")) { + return output; + } + } + + throw new Error("Max iterations reached"); + } finally { + await session.destroy(); + await this.client.stop(); + } + } +} +``` + +## Best Practices + +1. **Write clear completion criteria**: Include exactly what "done" looks like +2. **Use output markers**: Include `COMPLETE` or similar in completion condition +3. **Always set max iterations**: Prevents infinite loops on impossible tasks +4. **Persist state**: Save files so AI can see what changed between iterations +5. **Include context**: Feed previous iteration output back as context +6. **Monitor progress**: Log each iteration to track what's happening + +## Example: Iterative Code Generation + +```typescript +const prompt = `Write a function that: +1. Parses CSV data +2. Validates required fields +3. Returns parsed records or error +4. Has unit tests +5. Output COMPLETE when done`; + +const loop = new RalphLoop(10, "COMPLETE"); +const result = await loop.run(prompt); +``` + +## Handling Failures + +```typescript +try { + const result = await loop.run(prompt); + console.log("Task completed successfully!"); +} catch (error) { + console.error("Task failed:", error.message); + // Analyze what was attempted and suggest alternatives +} +``` + +## When to Use RALPH-loop + +**Good for:** +- Code generation with automatic verification (tests, linters) +- Tasks with clear success criteria +- Iterative refinement where each attempt learns from previous failures +- Unattended long-running improvements + +**Not good for:** +- Tasks requiring human judgment or design input +- One-shot operations +- Tasks with vague success criteria +- Real-time interactive debugging diff --git a/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts new file mode 100644 index 00000000..19b16b06 --- /dev/null +++ b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts @@ -0,0 +1,121 @@ +import { CopilotClient } from "@github/copilot-sdk"; + +/** + * RALPH-loop implementation: Iterative self-referential AI loops. + * The same prompt is sent repeatedly, with AI reading its own previous output. + * Loop continues until completion promise is detected in the response. + */ +class RalphLoop { + private client: CopilotClient; + private iteration: number = 0; + private readonly maxIterations: number; + private readonly completionPromise: string; + public lastResponse: string | null = null; + + constructor(maxIterations: number = 10, completionPromise: string = "COMPLETE") { + this.client = new CopilotClient(); + this.maxIterations = maxIterations; + this.completionPromise = completionPromise; + } + + /** + * Run the RALPH-loop until completion promise is detected or max iterations reached. + */ + async run(initialPrompt: string): Promise { + await this.client.start(); + const session = await this.client.createSession({ + model: "gpt-5.1-codex-mini" + }); + + try { + while (this.iteration < this.maxIterations) { + this.iteration++; + console.log(`\n=== Iteration ${this.iteration}/${this.maxIterations} ===`); + + // Build the prompt for this iteration + const currentPrompt = this.buildIterationPrompt(initialPrompt); + console.log(`Sending prompt (length: ${currentPrompt.length})...`); + + const response = await session.sendAndWait({ prompt: currentPrompt }, 300_000); + this.lastResponse = response?.data.content || ""; + + // Display response summary + const summary = this.lastResponse.length > 200 + ? this.lastResponse.substring(0, 200) + "..." + : this.lastResponse; + console.log(`Response: ${summary}`); + + // Check for completion promise + if (this.lastResponse.includes(this.completionPromise)) { + console.log(`\n✓ Success! Completion promise detected: '${this.completionPromise}'`); + return this.lastResponse; + } + + console.log(`Iteration ${this.iteration} complete. Checking for next iteration...`); + } + + // Max iterations reached without completion + throw new Error( + `Maximum iterations (${this.maxIterations}) reached without detecting completion promise: '${this.completionPromise}'` + ); + } catch (error) { + console.error(`\nError during RALPH-loop: ${error instanceof Error ? error.message : String(error)}`); + throw error; + } finally { + await session.destroy(); + await this.client.stop(); + } + } + + /** + * Build the prompt for the current iteration, including previous output as context. + */ + private buildIterationPrompt(initialPrompt: string): string { + if (this.iteration === 1) { + // First iteration: just the initial prompt + return initialPrompt; + } + + // Subsequent iterations: include previous output as context + return `${initialPrompt} + +=== CONTEXT FROM PREVIOUS ITERATION === +${this.lastResponse} +=== END CONTEXT === + +Continue working on this task. Review the previous attempt and improve upon it.`; + } +} + +// Example usage demonstrating RALPH-loop +async function main() { + const prompt = `You are iteratively building a small library. Follow these phases IN ORDER. +Do NOT skip ahead — only do the current phase, then stop and wait for the next iteration. + +Phase 1: Design a DataValidator class that validates records against a schema. + - Schema defines field names, types (str, int, float, bool), and whether required. + - Return a list of validation errors per record. + - Show the class code only. Do NOT output COMPLETE. + +Phase 2: Write at least 4 unit tests covering: missing required field, wrong type, + valid record, and empty input. Show test code only. Do NOT output COMPLETE. + +Phase 3: Review the code from phases 1 and 2. Fix any bugs, add docstrings, and add + an extra edge-case test. Show the final consolidated code with all fixes. + When this phase is fully done, output the exact text: COMPLETE`; + + const loop = new RalphLoop(5, "COMPLETE"); + + try { + const result = await loop.run(prompt); + console.log("\n=== FINAL RESULT ==="); + console.log(result); + } catch (error) { + console.error(`\nTask did not complete: ${error instanceof Error ? error.message : String(error)}`); + if (loop.lastResponse) { + console.log(`\nLast attempt:\n${loop.lastResponse}`); + } + } +} + +main().catch(console.error); diff --git a/cookbook/copilot-sdk/python/ralph-loop.md b/cookbook/copilot-sdk/python/ralph-loop.md new file mode 100644 index 00000000..5b208200 --- /dev/null +++ b/cookbook/copilot-sdk/python/ralph-loop.md @@ -0,0 +1,205 @@ +# RALPH-loop: Iterative Self-Referential AI Loops + +Implement self-referential feedback loops where an AI agent iteratively improves work by reading its own previous output. + +> **Runnable example:** [recipe/ralph_loop.py](recipe/ralph_loop.py) +> +> ```bash +> cd python/recipe +> pip install -r requirements.txt +> python ralph_loop.py +> ``` + +## What is RALPH-loop? + +RALPH-loop is a development methodology for iterative AI-powered task completion. Named after the Ralph Wiggum technique, it embodies the philosophy of persistent iteration: + +- **One prompt, multiple iterations**: The same prompt is processed repeatedly +- **Self-referential feedback**: The AI reads its own previous work (file changes, git history) +- **Completion detection**: Loop exits when a completion promise is detected in output +- **Safety limits**: Always include a maximum iteration count to prevent infinite loops + +## Example Scenario + +You need to iteratively improve code until all tests pass. Instead of asking Claude to "write perfect code," you use RALPH-loop to: + +1. Send the initial prompt with clear success criteria +2. Claude writes code and tests +3. Claude runs tests and sees failures +4. Loop automatically re-sends the prompt +5. Claude reads test output and previous code, fixes issues +6. Repeat until all tests pass and completion promise is output + +## Basic Implementation + +```python +import asyncio +from copilot import CopilotClient, MessageOptions, SessionConfig + +class RalphLoop: + """Iterative self-referential feedback loop using Copilot.""" + + def __init__(self, max_iterations=10, completion_promise="COMPLETE"): + self.client = CopilotClient() + self.iteration = 0 + self.max_iterations = max_iterations + self.completion_promise = completion_promise + self.last_response = None + + async def run(self, initial_prompt): + """Run the RALPH-loop until completion promise detected or max iterations reached.""" + await self.client.start() + session = await self.client.create_session( + SessionConfig(model="gpt-5.1-codex-mini") + ) + + try: + while self.iteration < self.max_iterations: + self.iteration += 1 + print(f"\n--- Iteration {self.iteration}/{self.max_iterations} ---") + + # Build prompt including previous response as context + if self.iteration == 1: + prompt = initial_prompt + else: + prompt = f"{initial_prompt}\n\nPrevious attempt:\n{self.last_response}\n\nContinue improving..." + + result = await session.send_and_wait( + MessageOptions(prompt=prompt), timeout=300 + ) + + self.last_response = result.data.content if result else "" + print(f"Response ({len(self.last_response)} chars)") + + # Check for completion promise + if self.completion_promise in self.last_response: + print(f"✓ Completion promise detected: {self.completion_promise}") + return self.last_response + + print(f"Continuing to iteration {self.iteration + 1}...") + + raise RuntimeError( + f"Max iterations ({self.max_iterations}) reached without completion promise" + ) + finally: + await session.destroy() + await self.client.stop() + +# Usage +async def main(): + loop = RalphLoop(5, "COMPLETE") + result = await loop.run("Your task here") + print(result) + +asyncio.run(main()) +``` + +## With File Persistence + +For tasks involving code generation, persist state to files so the AI can see changes: + +```python +import asyncio +from pathlib import Path +from copilot import CopilotClient, MessageOptions, SessionConfig + +class PersistentRalphLoop: + """RALPH-loop with file-based state persistence.""" + + def __init__(self, work_dir, max_iterations=10): + self.client = CopilotClient() + self.work_dir = Path(work_dir) + self.work_dir.mkdir(parents=True, exist_ok=True) + self.iteration = 0 + self.max_iterations = max_iterations + + async def run(self, initial_prompt): + """Run the loop with persistent state.""" + await self.client.start() + session = await self.client.create_session( + SessionConfig(model="gpt-5.1-codex-mini") + ) + + try: + # Store initial prompt + (self.work_dir / "prompt.md").write_text(initial_prompt) + + while self.iteration < self.max_iterations: + self.iteration += 1 + print(f"\n--- Iteration {self.iteration} ---") + + # Build context from previous outputs + context = initial_prompt + prev_output = self.work_dir / f"output-{self.iteration - 1}.txt" + if prev_output.exists(): + context += f"\n\nPrevious iteration:\n{prev_output.read_text()}" + + result = await session.send_and_wait( + MessageOptions(prompt=context), timeout=300 + ) + response = result.data.content if result else "" + + # Persist output + output_file = self.work_dir / f"output-{self.iteration}.txt" + output_file.write_text(response) + + if "COMPLETE" in response: + return response + + raise RuntimeError("Max iterations reached") + finally: + await session.destroy() + await self.client.stop() +``` + +## Best Practices + +1. **Write clear completion criteria**: Include exactly what "done" looks like +2. **Use output markers**: Include `COMPLETE` or similar in completion condition +3. **Always set max iterations**: Prevents infinite loops on impossible tasks +4. **Persist state**: Save files so AI can see what changed between iterations +5. **Include context**: Feed previous iteration output back as context +6. **Monitor progress**: Log each iteration to track what's happening + +## Example: Iterative Code Generation + +```python +prompt = """Write a function that: +1. Parses CSV data +2. Validates required fields +3. Returns parsed records or error +4. Has unit tests +5. Output COMPLETE when done""" + +async def main(): + loop = RalphLoop(10, "COMPLETE") + result = await loop.run(prompt) + +asyncio.run(main()) +``` + +## Handling Failures + +```python +try: + result = await loop.run(prompt) + print("Task completed successfully!") +except RuntimeError as e: + print(f"Task failed: {e}") + if loop.last_response: + print(f"\nLast attempt:\n{loop.last_response}") +``` + +## When to Use RALPH-loop + +**Good for:** +- Code generation with automatic verification (tests, linters) +- Tasks with clear success criteria +- Iterative refinement where each attempt learns from previous failures +- Unattended long-running improvements + +**Not good for:** +- Tasks requiring human judgment or design input +- One-shot operations +- Tasks with vague success criteria +- Real-time interactive debugging diff --git a/cookbook/copilot-sdk/python/recipe/ralph_loop.py b/cookbook/copilot-sdk/python/recipe/ralph_loop.py new file mode 100644 index 00000000..a8c228a2 --- /dev/null +++ b/cookbook/copilot-sdk/python/recipe/ralph_loop.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 + +import asyncio + +from copilot import CopilotClient, MessageOptions, SessionConfig + + +class RalphLoop: + """ + RALPH-loop implementation: Iterative self-referential AI loops. + + The same prompt is sent repeatedly, with AI reading its own previous output. + Loop continues until completion promise is detected in the response. + """ + + def __init__(self, max_iterations=10, completion_promise="COMPLETE"): + """Initialize RALPH-loop with iteration limits and completion detection.""" + self.client = CopilotClient() + self.iteration = 0 + self.max_iterations = max_iterations + self.completion_promise = completion_promise + self.last_response = None + + async def run(self, initial_prompt): + """ + Run the RALPH-loop until completion promise is detected or max iterations reached. + """ + await self.client.start() + session = await self.client.create_session( + SessionConfig(model="gpt-5.1-codex-mini") + ) + + try: + while self.iteration < self.max_iterations: + self.iteration += 1 + print(f"\n=== Iteration {self.iteration}/{self.max_iterations} ===") + + current_prompt = self._build_iteration_prompt(initial_prompt) + print(f"Sending prompt (length: {len(current_prompt)})...") + + result = await session.send_and_wait( + MessageOptions(prompt=current_prompt), + timeout=300, + ) + + self.last_response = result.data.content if result else "" + + # Display response summary + summary = ( + self.last_response[:200] + "..." + if len(self.last_response) > 200 + else self.last_response + ) + print(f"Response: {summary}") + + # Check for completion promise + if self.completion_promise in self.last_response: + print( + f"\n✓ Success! Completion promise detected: '{self.completion_promise}'" + ) + return self.last_response + + print( + f"Iteration {self.iteration} complete. Checking for next iteration..." + ) + + raise RuntimeError( + f"Maximum iterations ({self.max_iterations}) reached without " + f"detecting completion promise: '{self.completion_promise}'" + ) + + except Exception as e: + print(f"\nError during RALPH-loop: {e}") + raise + finally: + await session.destroy() + await self.client.stop() + + def _build_iteration_prompt(self, initial_prompt): + """Build the prompt for the current iteration, including previous output as context.""" + if self.iteration == 1: + return initial_prompt + + return f"""{initial_prompt} + +=== CONTEXT FROM PREVIOUS ITERATION === +{self.last_response} +=== END CONTEXT === + +Continue working on this task. Review the previous attempt and improve upon it.""" + + +async def main(): + """Example usage demonstrating RALPH-loop.""" + prompt = """You are iteratively building a small library. Follow these phases IN ORDER. +Do NOT skip ahead — only do the current phase, then stop and wait for the next iteration. + +Phase 1: Design a DataValidator class that validates records against a schema. + - Schema defines field names, types (str, int, float, bool), and whether required. + - Return a list of validation errors per record. + - Show the class code only. Do NOT output COMPLETE. + +Phase 2: Write at least 4 unit tests covering: missing required field, wrong type, + valid record, and empty input. Show test code only. Do NOT output COMPLETE. + +Phase 3: Review the code from phases 1 and 2. Fix any bugs, add docstrings, and add + an extra edge-case test. Show the final consolidated code with all fixes. + When this phase is fully done, output the exact text: COMPLETE""" + + loop = RalphLoop(max_iterations=5, completion_promise="COMPLETE") + + try: + result = await loop.run(prompt) + print("\n=== FINAL RESULT ===") + print(result) + except RuntimeError as e: + print(f"\nTask did not complete: {e}") + if loop.last_response: + print(f"\nLast attempt:\n{loop.last_response}") + + +if __name__ == "__main__": + asyncio.run(main()) From 7e39d55028cc048a20281eeacf83e36bea47a223 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 05:48:44 -0800 Subject: [PATCH 04/21] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cookbook/copilot-sdk/README.md | 2 +- cookbook/copilot-sdk/dotnet/ralph-loop.md | 4 +- cookbook/copilot-sdk/go/ralph-loop.md | 12 +-- cookbook/copilot-sdk/nodejs/ralph-loop.md | 11 +-- .../copilot-sdk/nodejs/recipe/ralph-loop.ts | 69 +++++++------- cookbook/copilot-sdk/python/ralph-loop.md | 12 +-- .../copilot-sdk/python/recipe/ralph_loop.py | 92 ++++++++++--------- 7 files changed, 105 insertions(+), 97 deletions(-) diff --git a/cookbook/copilot-sdk/README.md b/cookbook/copilot-sdk/README.md index c4892687..5a20ba0a 100644 --- a/cookbook/copilot-sdk/README.md +++ b/cookbook/copilot-sdk/README.md @@ -87,4 +87,4 @@ go run .go ## Status -Cookbook structure is complete with 5 recipes across all 4 supported languages. Each recipe includes both markdown documentation and runnable examples. The RALPH-loop recipe demonstrates iterative self-referential AI loops for autonomous task completion. +Cookbook structure is complete with 6 recipes across all 4 supported languages. Each recipe includes both markdown documentation and runnable examples. The RALPH-loop recipe demonstrates iterative self-referential AI loops for autonomous task completion. diff --git a/cookbook/copilot-sdk/dotnet/ralph-loop.md b/cookbook/copilot-sdk/dotnet/ralph-loop.md index 43d22e72..5580e0f4 100644 --- a/cookbook/copilot-sdk/dotnet/ralph-loop.md +++ b/cookbook/copilot-sdk/dotnet/ralph-loop.md @@ -5,8 +5,8 @@ Implement self-referential feedback loops where an AI agent iteratively improves > **Runnable example:** [recipe/ralph-loop.cs](recipe/ralph-loop.cs) > > ```bash -> cd dotnet/recipe -> dotnet run ralph-loop.cs +> cd dotnet +> dotnet run recipe/ralph-loop.cs > ``` ## What is RALPH-loop? diff --git a/cookbook/copilot-sdk/go/ralph-loop.md b/cookbook/copilot-sdk/go/ralph-loop.md index 37879c0b..2469c181 100644 --- a/cookbook/copilot-sdk/go/ralph-loop.md +++ b/cookbook/copilot-sdk/go/ralph-loop.md @@ -5,8 +5,8 @@ Implement self-referential feedback loops where an AI agent iteratively improves > **Runnable example:** [recipe/ralph-loop.go](recipe/ralph-loop.go) > > ```bash -> cd go/recipe -> go run ralph-loop.go +> cd go +> go run recipe/ralph-loop.go > ``` ## What is RALPH-loop? @@ -20,13 +20,13 @@ RALPH-loop is a development methodology for iterative AI-powered task completion ## Example Scenario -You need to iteratively improve code until all tests pass. Instead of asking Claude to "write perfect code," you use RALPH-loop to: +You need to iteratively improve code until all tests pass. Instead of asking Copilot to "write perfect code," you use RALPH-loop to: 1. Send the initial prompt with clear success criteria -2. Claude writes code and tests -3. Claude runs tests and sees failures +2. Copilot writes code and tests +3. Copilot runs tests and sees failures 4. Loop automatically re-sends the prompt -5. Claude reads test output and previous code, fixes issues +5. Copilot reads test output and previous code, fixes issues 6. Repeat until all tests pass and completion promise is output ## Basic Implementation diff --git a/cookbook/copilot-sdk/nodejs/ralph-loop.md b/cookbook/copilot-sdk/nodejs/ralph-loop.md index 1ad70708..bc4d1998 100644 --- a/cookbook/copilot-sdk/nodejs/ralph-loop.md +++ b/cookbook/copilot-sdk/nodejs/ralph-loop.md @@ -5,8 +5,7 @@ Implement self-referential feedback loops where an AI agent iteratively improves > **Runnable example:** [recipe/ralph-loop.ts](recipe/ralph-loop.ts) > > ```bash -> cd nodejs/recipe -> npm install +> cd recipe && npm install > npx tsx ralph-loop.ts > ``` @@ -21,13 +20,13 @@ RALPH-loop is a development methodology for iterative AI-powered task completion ## Example Scenario -You need to iteratively improve code until all tests pass. Instead of asking Claude to "write perfect code," you use RALPH-loop to: +You need to iteratively improve code until all tests pass. Instead of asking Copilot to "write perfect code," you use RALPH-loop to: 1. Send the initial prompt with clear success criteria -2. Claude writes code and tests -3. Claude runs tests and sees failures +2. Copilot writes code and tests +3. Copilot runs tests and sees failures 4. Loop automatically re-sends the prompt -5. Claude reads test output and previous code, fixes issues +5. Copilot reads test output and previous code, fixes issues 6. Repeat until all tests pass and completion promise is output ## Basic Implementation diff --git a/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts index 19b16b06..93a7ebb2 100644 --- a/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts +++ b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts @@ -22,47 +22,54 @@ class RalphLoop { * Run the RALPH-loop until completion promise is detected or max iterations reached. */ async run(initialPrompt: string): Promise { + let session: Awaited> | null = null; + await this.client.start(); - const session = await this.client.createSession({ - model: "gpt-5.1-codex-mini" - }); - try { - while (this.iteration < this.maxIterations) { - this.iteration++; - console.log(`\n=== Iteration ${this.iteration}/${this.maxIterations} ===`); + session = await this.client.createSession({ + model: "gpt-5.1-codex-mini" + }); - // Build the prompt for this iteration - const currentPrompt = this.buildIterationPrompt(initialPrompt); - console.log(`Sending prompt (length: ${currentPrompt.length})...`); + try { + while (this.iteration < this.maxIterations) { + this.iteration++; + console.log(`\n=== Iteration ${this.iteration}/${this.maxIterations} ===`); - const response = await session.sendAndWait({ prompt: currentPrompt }, 300_000); - this.lastResponse = response?.data.content || ""; + // Build the prompt for this iteration + const currentPrompt = this.buildIterationPrompt(initialPrompt); + console.log(`Sending prompt (length: ${currentPrompt.length})...`); - // Display response summary - const summary = this.lastResponse.length > 200 - ? this.lastResponse.substring(0, 200) + "..." - : this.lastResponse; - console.log(`Response: ${summary}`); + const response = await session.sendAndWait({ prompt: currentPrompt }, 300_000); + this.lastResponse = response?.data.content || ""; - // Check for completion promise - if (this.lastResponse.includes(this.completionPromise)) { - console.log(`\n✓ Success! Completion promise detected: '${this.completionPromise}'`); - return this.lastResponse; + // Display response summary + const summary = this.lastResponse.length > 200 + ? this.lastResponse.substring(0, 200) + "..." + : this.lastResponse; + console.log(`Response: ${summary}`); + + // Check for completion promise + if (this.lastResponse.includes(this.completionPromise)) { + console.log(`\n✓ Success! Completion promise detected: '${this.completionPromise}'`); + return this.lastResponse; + } + + console.log(`Iteration ${this.iteration} complete. Checking for next iteration...`); } - console.log(`Iteration ${this.iteration} complete. Checking for next iteration...`); + // Max iterations reached without completion + throw new Error( + `Maximum iterations (${this.maxIterations}) reached without detecting completion promise: '${this.completionPromise}'` + ); + } catch (error) { + console.error(`\nError during RALPH-loop: ${error instanceof Error ? error.message : String(error)}`); + throw error; + } finally { + if (session) { + await session.destroy(); + } } - - // Max iterations reached without completion - throw new Error( - `Maximum iterations (${this.maxIterations}) reached without detecting completion promise: '${this.completionPromise}'` - ); - } catch (error) { - console.error(`\nError during RALPH-loop: ${error instanceof Error ? error.message : String(error)}`); - throw error; } finally { - await session.destroy(); await this.client.stop(); } } diff --git a/cookbook/copilot-sdk/python/ralph-loop.md b/cookbook/copilot-sdk/python/ralph-loop.md index 5b208200..9a969ce6 100644 --- a/cookbook/copilot-sdk/python/ralph-loop.md +++ b/cookbook/copilot-sdk/python/ralph-loop.md @@ -5,11 +5,9 @@ Implement self-referential feedback loops where an AI agent iteratively improves > **Runnable example:** [recipe/ralph_loop.py](recipe/ralph_loop.py) > > ```bash -> cd python/recipe -> pip install -r requirements.txt +> cd recipe && pip install -r requirements.txt > python ralph_loop.py > ``` - ## What is RALPH-loop? RALPH-loop is a development methodology for iterative AI-powered task completion. Named after the Ralph Wiggum technique, it embodies the philosophy of persistent iteration: @@ -21,13 +19,13 @@ RALPH-loop is a development methodology for iterative AI-powered task completion ## Example Scenario -You need to iteratively improve code until all tests pass. Instead of asking Claude to "write perfect code," you use RALPH-loop to: +You need to iteratively improve code until all tests pass. Instead of asking Copilot to "write perfect code," you use RALPH-loop to: 1. Send the initial prompt with clear success criteria -2. Claude writes code and tests -3. Claude runs tests and sees failures +2. Copilot writes code and tests +3. Copilot runs tests and sees failures 4. Loop automatically re-sends the prompt -5. Claude reads test output and previous code, fixes issues +5. Copilot reads test output and previous code, fixes issues 6. Repeat until all tests pass and completion promise is output ## Basic Implementation diff --git a/cookbook/copilot-sdk/python/recipe/ralph_loop.py b/cookbook/copilot-sdk/python/recipe/ralph_loop.py index a8c228a2..00ecadcc 100644 --- a/cookbook/copilot-sdk/python/recipe/ralph_loop.py +++ b/cookbook/copilot-sdk/python/recipe/ralph_loop.py @@ -25,55 +25,59 @@ class RalphLoop: """ Run the RALPH-loop until completion promise is detected or max iterations reached. """ + session = None await self.client.start() - session = await self.client.create_session( - SessionConfig(model="gpt-5.1-codex-mini") - ) - try: - while self.iteration < self.max_iterations: - self.iteration += 1 - print(f"\n=== Iteration {self.iteration}/{self.max_iterations} ===") - - current_prompt = self._build_iteration_prompt(initial_prompt) - print(f"Sending prompt (length: {len(current_prompt)})...") - - result = await session.send_and_wait( - MessageOptions(prompt=current_prompt), - timeout=300, - ) - - self.last_response = result.data.content if result else "" - - # Display response summary - summary = ( - self.last_response[:200] + "..." - if len(self.last_response) > 200 - else self.last_response - ) - print(f"Response: {summary}") - - # Check for completion promise - if self.completion_promise in self.last_response: - print( - f"\n✓ Success! Completion promise detected: '{self.completion_promise}'" - ) - return self.last_response - - print( - f"Iteration {self.iteration} complete. Checking for next iteration..." - ) - - raise RuntimeError( - f"Maximum iterations ({self.max_iterations}) reached without " - f"detecting completion promise: '{self.completion_promise}'" + session = await self.client.create_session( + SessionConfig(model="gpt-5.1-codex-mini") ) - except Exception as e: - print(f"\nError during RALPH-loop: {e}") - raise + try: + while self.iteration < self.max_iterations: + self.iteration += 1 + print(f"\n=== Iteration {self.iteration}/{self.max_iterations} ===") + + current_prompt = self._build_iteration_prompt(initial_prompt) + print(f"Sending prompt (length: {len(current_prompt)})...") + + result = await session.send_and_wait( + MessageOptions(prompt=current_prompt), + timeout=300, + ) + + self.last_response = result.data.content if result else "" + + # Display response summary + summary = ( + self.last_response[:200] + "..." + if len(self.last_response) > 200 + else self.last_response + ) + print(f"Response: {summary}") + + # Check for completion promise + if self.completion_promise in self.last_response: + print( + f"\n✓ Success! Completion promise detected: '{self.completion_promise}'" + ) + return self.last_response + + print( + f"Iteration {self.iteration} complete. Checking for next iteration..." + ) + + raise RuntimeError( + f"Maximum iterations ({self.max_iterations}) reached without " + f"detecting completion promise: '{self.completion_promise}'" + ) + + except Exception as e: + print(f"\nError during RALPH-loop: {e}") + raise + finally: + if session is not None: + await session.destroy() finally: - await session.destroy() await self.client.stop() def _build_iteration_prompt(self, initial_prompt): From bb9f63a89920f443481d287b1d1fb04b79a89883 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 05:49:46 -0800 Subject: [PATCH 05/21] Update README to remove RALPH-loop reference Removed mention of the RALPH-loop recipe from the README. --- cookbook/copilot-sdk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/copilot-sdk/README.md b/cookbook/copilot-sdk/README.md index 5a20ba0a..6e364457 100644 --- a/cookbook/copilot-sdk/README.md +++ b/cookbook/copilot-sdk/README.md @@ -87,4 +87,4 @@ go run .go ## Status -Cookbook structure is complete with 6 recipes across all 4 supported languages. Each recipe includes both markdown documentation and runnable examples. The RALPH-loop recipe demonstrates iterative self-referential AI loops for autonomous task completion. +Cookbook structure is complete with 6 recipes across all 4 supported languages. Each recipe includes both markdown documentation and runnable examples. From c65e8ab0b5245edc498d52d6230db65c2e0c2f10 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 05:56:48 -0800 Subject: [PATCH 06/21] Fix Python cookbook recipes to use correct async SDK API All 5 Python recipes and their markdown docs used a synchronous, kwargs-based API that doesn't match the real github-copilot-sdk: - client.start() -> await client.start() (all methods are async) - create_session(model=...) -> create_session(SessionConfig(model=...)) - session.send(prompt=...) -> session.send(MessageOptions(prompt=...)) - session.wait_for_idle() -> session.send_and_wait() (wait_for_idle doesn't exist) - event['type']/event['data']['content'] -> event.type/event.data.content - All code wrapped in async def main() + asyncio.run(main()) Verified all imports resolve against github-copilot-sdk. --- cookbook/copilot-sdk/python/error-handling.md | 114 ++++++------------ .../python/managing-local-files.md | 63 ++++++---- .../copilot-sdk/python/multiple-sessions.md | 55 +++++---- .../copilot-sdk/python/persisting-sessions.md | 53 ++++---- .../copilot-sdk/python/pr-visualization.md | 51 ++++---- .../python/recipe/error_handling.py | 37 +++--- .../python/recipe/managing_local_files.py | 58 +++++---- .../python/recipe/multiple_sessions.py | 53 ++++---- .../python/recipe/persisting_sessions.py | 55 +++++---- .../python/recipe/pr_visualization.py | 49 +++++--- 10 files changed, 304 insertions(+), 284 deletions(-) diff --git a/cookbook/copilot-sdk/python/error-handling.md b/cookbook/copilot-sdk/python/error-handling.md index cdd73cbc..dfdd02b9 100644 --- a/cookbook/copilot-sdk/python/error-handling.md +++ b/cookbook/copilot-sdk/python/error-handling.md @@ -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 diff --git a/cookbook/copilot-sdk/python/managing-local-files.md b/cookbook/copilot-sdk/python/managing-local-files.md index c81a831e..a9e4e35f 100644 --- a/cookbook/copilot-sdk/python/managing-local-files.md +++ b/cookbook/copilot-sdk/python/managing-local-files.md @@ -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 diff --git a/cookbook/copilot-sdk/python/multiple-sessions.md b/cookbook/copilot-sdk/python/multiple-sessions.md index 4baa0f47..0efa3ed8 100644 --- a/cookbook/copilot-sdk/python/multiple-sessions.md +++ b/cookbook/copilot-sdk/python/multiple-sessions.md @@ -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 diff --git a/cookbook/copilot-sdk/python/persisting-sessions.md b/cookbook/copilot-sdk/python/persisting-sessions.md index 5d07a469..cc77407c 100644 --- a/cookbook/copilot-sdk/python/persisting-sessions.md +++ b/cookbook/copilot-sdk/python/persisting-sessions.md @@ -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 diff --git a/cookbook/copilot-sdk/python/pr-visualization.md b/cookbook/copilot-sdk/python/pr-visualization.md index 0419aed1..0b158e4a 100644 --- a/cookbook/copilot-sdk/python/pr-visualization.md +++ b/cookbook/copilot-sdk/python/pr-visualization.md @@ -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()} """ } - ) + )) + + 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 diff --git a/cookbook/copilot-sdk/python/recipe/error_handling.py b/cookbook/copilot-sdk/python/recipe/error_handling.py index b76b29ce..7933cbba 100644 --- a/cookbook/copilot-sdk/python/recipe/error_handling.py +++ b/cookbook/copilot-sdk/python/recipe/error_handling.py @@ -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()) diff --git a/cookbook/copilot-sdk/python/recipe/managing_local_files.py b/cookbook/copilot-sdk/python/recipe/managing_local_files.py index 8b9f94dc..c0381170 100644 --- a/cookbook/copilot-sdk/python/recipe/managing_local_files.py +++ b/cookbook/copilot-sdk/python/recipe/managing_local_files.py @@ -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()) diff --git a/cookbook/copilot-sdk/python/recipe/multiple_sessions.py b/cookbook/copilot-sdk/python/recipe/multiple_sessions.py index dd4f299c..8d7d35d1 100644 --- a/cookbook/copilot-sdk/python/recipe/multiple_sessions.py +++ b/cookbook/copilot-sdk/python/recipe/multiple_sessions.py @@ -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()) diff --git a/cookbook/copilot-sdk/python/recipe/persisting_sessions.py b/cookbook/copilot-sdk/python/recipe/persisting_sessions.py index b3d97f2f..da668070 100644 --- a/cookbook/copilot-sdk/python/recipe/persisting_sessions.py +++ b/cookbook/copilot-sdk/python/recipe/persisting_sessions.py @@ -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()) diff --git a/cookbook/copilot-sdk/python/recipe/pr_visualization.py b/cookbook/copilot-sdk/python/recipe/pr_visualization.py index 6be73dfd..9ece3f93 100644 --- a/cookbook/copilot-sdk/python/recipe/pr_visualization.py +++ b/cookbook/copilot-sdk/python/recipe/pr_visualization.py @@ -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()} """ } - ) + )) + + 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()) From 5eb7adb376cb8be40fc03b6d6595cdde7bd3b9d6 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 06:06:37 -0800 Subject: [PATCH 07/21] Fix Go cookbook recipes to use correct SDK API All 5 Go recipes and their markdown docs used incorrect API patterns that don't match the real github.com/github/copilot-sdk/go v0.1.23: - copilot.NewClient() -> copilot.NewClient(nil) (*ClientOptions param) - client.Start() -> client.Start(ctx) (context.Context required) - copilot.SessionConfig -> &copilot.SessionConfig (pointer required) - session.On(func(event copilot.Event)) -> session.On(func(event copilot.SessionEvent)) - Type assertions -> event.Type string check + *event.Data.Content deref - session.WaitForIdle() -> session.SendAndWait(ctx, ...) (WaitForIdle doesn't exist) - copilot.SystemMessage -> copilot.SystemMessageConfig All 5 recipes verified to compile against SDK v0.1.23. --- cookbook/copilot-sdk/go/error-handling.md | 98 ++++++++--------- .../copilot-sdk/go/managing-local-files.md | 42 ++++--- cookbook/copilot-sdk/go/multiple-sessions.md | 32 +++--- .../copilot-sdk/go/persisting-sessions.md | 31 +++--- cookbook/copilot-sdk/go/pr-visualization.md | 38 ++++--- .../copilot-sdk/go/recipe/error-handling.go | 31 ++---- .../go/recipe/managing-local-files.go | 38 ++++--- .../go/recipe/multiple-sessions.go | 26 +++-- .../go/recipe/persisting-sessions.go | 104 +++++++++--------- .../copilot-sdk/go/recipe/pr-visualization.go | 38 ++++--- 10 files changed, 246 insertions(+), 232 deletions(-) diff --git a/cookbook/copilot-sdk/go/error-handling.md b/cookbook/copilot-sdk/go/error-handling.md index 658613a9..462d2706 100644 --- a/cookbook/copilot-sdk/go/error-handling.md +++ b/cookbook/copilot-sdk/go/error-handling.md @@ -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) } diff --git a/cookbook/copilot-sdk/go/managing-local-files.md b/cookbook/copilot-sdk/go/managing-local-files.md index 1e5a2999..f86871a5 100644 --- a/cookbook/copilot-sdk/go/managing-local-files.md +++ b/cookbook/copilot-sdk/go/managing-local-files.md @@ -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 diff --git a/cookbook/copilot-sdk/go/multiple-sessions.md b/cookbook/copilot-sdk/go/multiple-sessions.md index 66261961..82d8bf50 100644 --- a/cookbook/copilot-sdk/go/multiple-sessions.md +++ b/cookbook/copilot-sdk/go/multiple-sessions.md @@ -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) } ``` diff --git a/cookbook/copilot-sdk/go/persisting-sessions.md b/cookbook/copilot-sdk/go/persisting-sessions.md index 8587b978..ea13b7ab 100644 --- a/cookbook/copilot-sdk/go/persisting-sessions.md +++ b/cookbook/copilot-sdk/go/persisting-sessions.md @@ -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) + } } ``` diff --git a/cookbook/copilot-sdk/go/pr-visualization.md b/cookbook/copilot-sdk/go/pr-visualization.md index e8046733..b0c7c0b3 100644 --- a/cookbook/copilot-sdk/go/pr-visualization.md +++ b/cookbook/copilot-sdk/go/pr-visualization.md @@ -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(` 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() } } ``` diff --git a/cookbook/copilot-sdk/go/recipe/error-handling.go b/cookbook/copilot-sdk/go/recipe/error-handling.go index 32edd9f9..3fc0fcdc 100644 --- a/cookbook/copilot-sdk/go/recipe/error-handling.go +++ b/cookbook/copilot-sdk/go/recipe/error-handling.go @@ -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) + } } diff --git a/cookbook/copilot-sdk/go/recipe/managing-local-files.go b/cookbook/copilot-sdk/go/recipe/managing-local-files.go index f1582669..dc3dfd84 100644 --- a/cookbook/copilot-sdk/go/recipe/managing-local-files.go +++ b/cookbook/copilot-sdk/go/recipe/managing-local-files.go @@ -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() } diff --git a/cookbook/copilot-sdk/go/recipe/multiple-sessions.go b/cookbook/copilot-sdk/go/recipe/multiple-sessions.go index 0fb3325c..9b99fcd2 100644 --- a/cookbook/copilot-sdk/go/recipe/multiple-sessions.go +++ b/cookbook/copilot-sdk/go/recipe/multiple-sessions.go @@ -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") diff --git a/cookbook/copilot-sdk/go/recipe/persisting-sessions.go b/cookbook/copilot-sdk/go/recipe/persisting-sessions.go index 11ee7ad0..471e5757 100644 --- a/cookbook/copilot-sdk/go/recipe/persisting-sessions.go +++ b/cookbook/copilot-sdk/go/recipe/persisting-sessions.go @@ -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() } diff --git a/cookbook/copilot-sdk/go/recipe/pr-visualization.go b/cookbook/copilot-sdk/go/recipe/pr-visualization.go index abea027b..54178eec 100644 --- a/cookbook/copilot-sdk/go/recipe/pr-visualization.go +++ b/cookbook/copilot-sdk/go/recipe/pr-visualization.go @@ -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(` 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() } } From ab82accc085952e64d961b2b37b7936d62437a0a Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 06:35:14 -0800 Subject: [PATCH 08/21] Address review feedback: fix event handler leak, error handling, model alignment - Move session.On handler outside loop to prevent handler accumulation (C#) - Use TrySetResult instead of SetResult to avoid duplicate-set exceptions (C#) - Wrap CreateSessionAsync in broader try/finally so client always stops (C#) - Fix PersistentRalphLoop to use maxIterations parameter instead of hardcoded 10 - Align model name to gpt-5.1-codex-mini across all doc snippets - Fix completion promise DONE -> COMPLETE in usage snippet - Replace Claude references with generic model terminology --- cookbook/copilot-sdk/dotnet/ralph-loop.md | 138 ++++++++++-------- .../copilot-sdk/dotnet/recipe/ralph-loop.cs | 65 +++++---- cookbook/copilot-sdk/nodejs/ralph-loop.md | 4 +- 3 files changed, 119 insertions(+), 88 deletions(-) diff --git a/cookbook/copilot-sdk/dotnet/ralph-loop.md b/cookbook/copilot-sdk/dotnet/ralph-loop.md index 5580e0f4..12aa0138 100644 --- a/cookbook/copilot-sdk/dotnet/ralph-loop.md +++ b/cookbook/copilot-sdk/dotnet/ralph-loop.md @@ -20,13 +20,13 @@ RALPH-loop is a development methodology for iterative AI-powered task completion ## Example Scenario -You need to iteratively improve code until all tests pass. Instead of asking Claude to "write perfect code," you use RALPH-loop to: +You need to iteratively improve code until all tests pass. Instead of asking the model to "write perfect code," you use RALPH-loop to: 1. Send the initial prompt with clear success criteria -2. Claude writes code and tests -3. Claude runs tests and sees failures +2. The model writes code and tests +3. The model runs tests and sees failures 4. Loop automatically re-sends the prompt -5. Claude reads test output and previous code, fixes issues +5. The model reads test output and previous code, fixes issues 6. Repeat until all tests pass and completion promise is output ## Basic Implementation @@ -52,56 +52,66 @@ public class RalphLoop public async Task RunAsync(string prompt) { await _client.StartAsync(); - var session = await _client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" }); try { - while (_iteration < _maxIterations) - { - _iteration++; - Console.WriteLine($"\n--- Iteration {_iteration} ---"); + var session = await _client.CreateSessionAsync( + new SessionConfig { Model = "gpt-5.1-codex-mini" }); + try + { var done = new TaskCompletionSource(); session.On(evt => { if (evt is AssistantMessageEvent msg) { _lastResponse = msg.Data.Content; - done.SetResult(msg.Data.Content); + done.TrySetResult(msg.Data.Content); } }); - // Send prompt (on first iteration) or continuation - var messagePrompt = _iteration == 1 - ? prompt - : $"{prompt}\n\nPrevious attempt:\n{_lastResponse}\n\nContinue iterating..."; - - await session.SendAsync(new MessageOptions { Prompt = messagePrompt }); - var response = await done.Task; - - // Check for completion promise - if (response.Contains(_completionPromise)) + while (_iteration < _maxIterations) { - Console.WriteLine($"✓ Completion promise detected: {_completionPromise}"); - return response; + _iteration++; + Console.WriteLine($"\n--- Iteration {_iteration} ---"); + + done = new TaskCompletionSource(); + + // Send prompt (on first iteration) or continuation + var messagePrompt = _iteration == 1 + ? prompt + : $"{prompt}\n\nPrevious attempt:\n{_lastResponse}\n\nContinue iterating..."; + + await session.SendAsync(new MessageOptions { Prompt = messagePrompt }); + var response = await done.Task; + + // Check for completion promise + if (response.Contains(_completionPromise)) + { + Console.WriteLine($"✓ Completion promise detected: {_completionPromise}"); + return response; + } + + Console.WriteLine($"Iteration {_iteration} complete. Continuing..."); } - Console.WriteLine($"Iteration {_iteration} complete. Continuing..."); + throw new InvalidOperationException( + $"Max iterations ({_maxIterations}) reached without completion promise"); + } + finally + { + await session.DisposeAsync(); } - - throw new InvalidOperationException( - $"Max iterations ({_maxIterations}) reached without completion promise"); } finally { - await session.DisposeAsync(); await _client.StopAsync(); } } } // Usage -var loop = new RalphLoop(maxIterations: 5, completionPromise: "DONE"); +var loop = new RalphLoop(maxIterations: 5, completionPromise: "COMPLETE"); var result = await loop.RunAsync("Your task here"); Console.WriteLine(result); ``` @@ -115,11 +125,13 @@ public class PersistentRalphLoop { private readonly string _workDir; private readonly CopilotClient _client; + private readonly int _maxIterations; private int _iteration = 0; public PersistentRalphLoop(string workDir, int maxIterations = 10) { _workDir = workDir; + _maxIterations = maxIterations; Directory.CreateDirectory(_workDir); _client = new CopilotClient(); } @@ -127,26 +139,17 @@ public class PersistentRalphLoop public async Task RunAsync(string prompt) { await _client.StartAsync(); - var session = await _client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" }); try { - // Store initial prompt - var promptFile = Path.Combine(_workDir, "prompt.md"); - await File.WriteAllTextAsync(promptFile, prompt); + var session = await _client.CreateSessionAsync( + new SessionConfig { Model = "gpt-5.1-codex-mini" }); - while (_iteration < 10) + try { - _iteration++; - Console.WriteLine($"\n--- Iteration {_iteration} ---"); - - // Build context including previous work - var contextBuilder = new StringBuilder(prompt); - var previousOutput = Path.Combine(_workDir, $"output-{_iteration - 1}.txt"); - if (File.Exists(previousOutput)) - { - contextBuilder.AppendLine($"\nPrevious iteration output:\n{await File.ReadAllTextAsync(previousOutput)}"); - } + // Store initial prompt + var promptFile = Path.Combine(_workDir, "prompt.md"); + await File.WriteAllTextAsync(promptFile, prompt); var done = new TaskCompletionSource(); string response = ""; @@ -155,29 +158,48 @@ public class PersistentRalphLoop if (evt is AssistantMessageEvent msg) { response = msg.Data.Content; - done.SetResult(msg.Data.Content); + done.TrySetResult(msg.Data.Content); } }); - await session.SendAsync(new MessageOptions { Prompt = contextBuilder.ToString() }); - await done.Task; - - // Persist output - await File.WriteAllTextAsync( - Path.Combine(_workDir, $"output-{_iteration}.txt"), - response); - - if (response.Contains("COMPLETE")) + while (_iteration < _maxIterations) { - return response; - } - } + _iteration++; + Console.WriteLine($"\n--- Iteration {_iteration} ---"); - throw new InvalidOperationException("Max iterations reached"); + done = new TaskCompletionSource(); + + // Build context including previous work + var contextBuilder = new StringBuilder(prompt); + var previousOutput = Path.Combine(_workDir, $"output-{_iteration - 1}.txt"); + if (File.Exists(previousOutput)) + { + contextBuilder.AppendLine($"\nPrevious iteration output:\n{await File.ReadAllTextAsync(previousOutput)}"); + } + + await session.SendAsync(new MessageOptions { Prompt = contextBuilder.ToString() }); + await done.Task; + + // Persist output + await File.WriteAllTextAsync( + Path.Combine(_workDir, $"output-{_iteration}.txt"), + response); + + if (response.Contains("COMPLETE")) + { + return response; + } + } + + throw new InvalidOperationException("Max iterations reached"); + } + finally + { + await session.DisposeAsync(); + } } finally { - await session.DisposeAsync(); await _client.StopAsync(); } } diff --git a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs index a1297656..0e81b5f8 100644 --- a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs +++ b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs @@ -62,54 +62,63 @@ public class RalphLoop public async Task RunAsync(string initialPrompt) { await _client.StartAsync(); - var session = await _client.CreateSessionAsync(new SessionConfig - { - Model = "gpt-5.1-codex-mini" - }); try { - while (_iteration < _maxIterations) - { - _iteration++; - Console.WriteLine($"\n=== Iteration {_iteration}/{_maxIterations} ==="); + var session = await _client.CreateSessionAsync(new SessionConfig + { + Model = "gpt-5.1-codex-mini" + }); + try + { var done = new TaskCompletionSource(); session.On(evt => { if (evt is AssistantMessageEvent msg) { _lastResponse = msg.Data.Content; - done.SetResult(msg.Data.Content); + done.TrySetResult(msg.Data.Content); } }); - var currentPrompt = BuildIterationPrompt(initialPrompt); - Console.WriteLine($"Sending prompt (length: {currentPrompt.Length})..."); - - await session.SendAsync(new MessageOptions { Prompt = currentPrompt }); - var response = await done.Task; - - var summary = response.Length > 200 - ? response.Substring(0, 200) + "..." - : response; - Console.WriteLine($"Response: {summary}"); - - if (response.Contains(_completionPromise)) + while (_iteration < _maxIterations) { - Console.WriteLine($"\n✓ Completion promise detected: '{_completionPromise}'"); - return response; + _iteration++; + Console.WriteLine($"\n=== Iteration {_iteration}/{_maxIterations} ==="); + + done = new TaskCompletionSource(); + + var currentPrompt = BuildIterationPrompt(initialPrompt); + Console.WriteLine($"Sending prompt (length: {currentPrompt.Length})..."); + + await session.SendAsync(new MessageOptions { Prompt = currentPrompt }); + var response = await done.Task; + + var summary = response.Length > 200 + ? response.Substring(0, 200) + "..." + : response; + Console.WriteLine($"Response: {summary}"); + + if (response.Contains(_completionPromise)) + { + Console.WriteLine($"\n✓ Completion promise detected: '{_completionPromise}'"); + return response; + } + + Console.WriteLine($"Iteration {_iteration} complete. Continuing..."); } - Console.WriteLine($"Iteration {_iteration} complete. Continuing..."); + throw new InvalidOperationException( + $"Max iterations ({_maxIterations}) reached without completion promise: '{_completionPromise}'"); + } + finally + { + await session.DisposeAsync(); } - - throw new InvalidOperationException( - $"Max iterations ({_maxIterations}) reached without completion promise: '{_completionPromise}'"); } finally { - await session.DisposeAsync(); await _client.StopAsync(); } } diff --git a/cookbook/copilot-sdk/nodejs/ralph-loop.md b/cookbook/copilot-sdk/nodejs/ralph-loop.md index bc4d1998..deafa628 100644 --- a/cookbook/copilot-sdk/nodejs/ralph-loop.md +++ b/cookbook/copilot-sdk/nodejs/ralph-loop.md @@ -49,7 +49,7 @@ class RalphLoop { async run(initialPrompt: string): Promise { await this.client.start(); - const session = await this.client.createSession({ model: "gpt-5" }); + const session = await this.client.createSession({ model: "gpt-5.1-codex-mini" }); try { while (this.iteration < this.maxIterations) { @@ -115,7 +115,7 @@ class PersistentRalphLoop { async run(initialPrompt: string): Promise { await fs.mkdir(this.workDir, { recursive: true }); await this.client.start(); - const session = await this.client.createSession({ model: "gpt-5" }); + const session = await this.client.createSession({ model: "gpt-5.1-codex-mini" }); try { // Store initial prompt From 836e4fc95d48f5284ca369872914a007ecc5bf36 Mon Sep 17 00:00:00 2001 From: Russ Rimmerman Date: Wed, 11 Feb 2026 12:34:10 -0600 Subject: [PATCH 09/21] Update Power Apps Code Apps instructions: refine project context, improve folder structure, and clarify PowerProvider implementation --- .../power-apps-code-apps.instructions.md | 87 ++++++++----------- 1 file changed, 38 insertions(+), 49 deletions(-) diff --git a/instructions/power-apps-code-apps.instructions.md b/instructions/power-apps-code-apps.instructions.md index 5a2b4e26..0b0e7d9b 100644 --- a/instructions/power-apps-code-apps.instructions.md +++ b/instructions/power-apps-code-apps.instructions.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 ( { return ( { 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 (
@@ -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 = ({ - variant, size, disabled, onClick, children + + export const Button: React.FC = ({ + variant, size, disabled, onClick, children }) => { const classes = `btn btn-${variant} btn-${size} ${disabled ? 'btn-disabled' : ''}`; return ; @@ -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 (
{children}
@@ -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 ( 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 ( -
@@ -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 (

Date: Wed, 11 Feb 2026 11:28:41 -0800 Subject: [PATCH 10/21] Rewrite Ralph loop recipes: split into simple vs ideal versions Align all 4 language recipes (Node.js, Python, .NET, Go) with the Ralph Playbook architecture: - Simple version: minimal outer loop with fresh session per iteration - Ideal version: planning/building modes, backpressure, git integration - Fresh context isolation instead of in-session context accumulation - Disk-based shared state via IMPLEMENTATION_PLAN.md - Example prompt templates (PROMPT_plan.md, PROMPT_build.md, AGENTS.md) - Updated cookbook README descriptions --- cookbook/copilot-sdk/README.md | 8 +- cookbook/copilot-sdk/dotnet/ralph-loop.md | 396 +++++++++--------- .../copilot-sdk/dotnet/recipe/ralph-loop.cs | 179 +++----- cookbook/copilot-sdk/go/ralph-loop.md | 347 ++++++++------- cookbook/copilot-sdk/go/recipe/ralph-loop.go | 160 +++---- cookbook/copilot-sdk/nodejs/ralph-loop.md | 363 ++++++++-------- .../copilot-sdk/nodejs/recipe/ralph-loop.ts | 169 +++----- cookbook/copilot-sdk/python/ralph-loop.md | 371 ++++++++-------- .../copilot-sdk/python/recipe/ralph_loop.py | 181 +++----- 9 files changed, 1052 insertions(+), 1122 deletions(-) diff --git a/cookbook/copilot-sdk/README.md b/cookbook/copilot-sdk/README.md index 6e364457..55981302 100644 --- a/cookbook/copilot-sdk/README.md +++ b/cookbook/copilot-sdk/README.md @@ -6,7 +6,7 @@ This cookbook collects small, focused recipes showing how to accomplish common t ### .NET (C#) -- [RALPH-loop](dotnet/ralph-loop.md): Implement iterative self-referential AI loops for task completion with automatic retries. +- [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. @@ -15,7 +15,7 @@ This cookbook collects small, focused recipes showing how to accomplish common t ### Node.js / TypeScript -- [RALPH-loop](nodejs/ralph-loop.md): Implement iterative self-referential AI loops for task completion with automatic retries. +- [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. @@ -24,7 +24,7 @@ This cookbook collects small, focused recipes showing how to accomplish common t ### Python -- [RALPH-loop](python/ralph-loop.md): Implement iterative self-referential AI loops for task completion with automatic retries. +- [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. @@ -33,7 +33,7 @@ This cookbook collects small, focused recipes showing how to accomplish common t ### Go -- [RALPH-loop](go/ralph-loop.md): Implement iterative self-referential AI loops for task completion with automatic retries. +- [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. diff --git a/cookbook/copilot-sdk/dotnet/ralph-loop.md b/cookbook/copilot-sdk/dotnet/ralph-loop.md index 12aa0138..1cc98762 100644 --- a/cookbook/copilot-sdk/dotnet/ralph-loop.md +++ b/cookbook/copilot-sdk/dotnet/ralph-loop.md @@ -1,6 +1,6 @@ -# RALPH-loop: Iterative Self-Referential AI Loops +# Ralph Loop: Autonomous AI Task Loops -Implement self-referential feedback loops where an AI agent iteratively improves work by reading its own previous output. +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) > @@ -9,252 +9,250 @@ Implement self-referential feedback loops where an AI agent iteratively improves > dotnet run recipe/ralph-loop.cs > ``` -## What is RALPH-loop? +## What is a Ralph Loop? -RALPH-loop is a development methodology for iterative AI-powered task completion. Named after the Ralph Wiggum technique, it embodies the philosophy of persistent iteration: +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. -- **One prompt, multiple iterations**: The same prompt is processed repeatedly -- **Self-referential feedback**: The AI reads its own previous work (file changes, git history) -- **Completion detection**: Loop exits when a completion promise is detected in output -- **Safety limits**: Always include a maximum iteration count to prevent infinite loops +``` +┌─────────────────────────────────────────────────┐ +│ 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) │ +└─────────────────────────────────────────────────┘ +``` -## Example Scenario +**Core principles:** -You need to iteratively improve code until all tests pass. Instead of asking the model to "write perfect code," you use RALPH-loop to: +- **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) -1. Send the initial prompt with clear success criteria -2. The model writes code and tests -3. The model runs tests and sees failures -4. Loop automatically re-sends the prompt -5. The model reads test output and previous code, fixes issues -6. Repeat until all tests pass and completion promise is output +## Simple Version -## Basic Implementation +The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | claude ; done`: ```csharp using GitHub.Copilot.SDK; -public class RalphLoop +var client = new CopilotClient(); +await client.StartAsync(); + +try { - private readonly CopilotClient _client; - private int _iteration = 0; - private readonly int _maxIterations; - private readonly string _completionPromise; - private string? _lastResponse; + var prompt = await File.ReadAllTextAsync("PROMPT.md"); + var maxIterations = 50; - public RalphLoop(int maxIterations = 10, string completionPromise = "COMPLETE") + for (var i = 1; i <= maxIterations; i++) { - _client = new CopilotClient(); - _maxIterations = maxIterations; - _completionPromise = completionPromise; - } - - public async Task RunAsync(string prompt) - { - await _client.StartAsync(); + Console.WriteLine($"\n=== Iteration {i}/{maxIterations} ==="); + // Fresh session each iteration — context isolation is the point + var session = await client.CreateSessionAsync( + new SessionConfig { Model = "claude-sonnet-4.5" }); try { - var session = await _client.CreateSessionAsync( - new SessionConfig { Model = "gpt-5.1-codex-mini" }); - - try + var done = new TaskCompletionSource(); + session.On(evt => { - var done = new TaskCompletionSource(); - session.On(evt => - { - if (evt is AssistantMessageEvent msg) - { - _lastResponse = msg.Data.Content; - done.TrySetResult(msg.Data.Content); - } - }); + if (evt is AssistantMessageEvent msg) + done.TrySetResult(msg.Data.Content); + }); - while (_iteration < _maxIterations) - { - _iteration++; - Console.WriteLine($"\n--- Iteration {_iteration} ---"); - - done = new TaskCompletionSource(); - - // Send prompt (on first iteration) or continuation - var messagePrompt = _iteration == 1 - ? prompt - : $"{prompt}\n\nPrevious attempt:\n{_lastResponse}\n\nContinue iterating..."; - - await session.SendAsync(new MessageOptions { Prompt = messagePrompt }); - var response = await done.Task; - - // Check for completion promise - if (response.Contains(_completionPromise)) - { - Console.WriteLine($"✓ Completion promise detected: {_completionPromise}"); - return response; - } - - Console.WriteLine($"Iteration {_iteration} complete. Continuing..."); - } - - throw new InvalidOperationException( - $"Max iterations ({_maxIterations}) reached without completion promise"); - } - finally - { - await session.DisposeAsync(); - } + await session.SendAsync(new MessageOptions { Prompt = prompt }); + await done.Task; } finally { - await _client.StopAsync(); + await session.DisposeAsync(); } + + Console.WriteLine($"Iteration {i} complete."); } } - -// Usage -var loop = new RalphLoop(maxIterations: 5, completionPromise: "COMPLETE"); -var result = await loop.RunAsync("Your task here"); -Console.WriteLine(result); +finally +{ + await client.StopAsync(); +} ``` -## With File Persistence +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. -For tasks involving code generation, persist state to files so the AI can see changes: +## Ideal Version + +The full Ralph pattern with planning and building modes, matching the [Ralph Playbook](https://github.com/ClaytonFarr/ralph-playbook) architecture: ```csharp -public class PersistentRalphLoop +using System.Diagnostics; +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(); + +var branchInfo = new ProcessStartInfo("git", "branch --show-current") + { RedirectStandardOutput = true }; +var branch = Process.Start(branchInfo)!; +var branchName = (await branch.StandardOutput.ReadToEndAsync()).Trim(); +await branch.WaitForExitAsync(); + +Console.WriteLine(new string('━', 40)); +Console.WriteLine($"Mode: {mode}"); +Console.WriteLine($"Prompt: {promptFile}"); +Console.WriteLine($"Branch: {branchName}"); +Console.WriteLine($"Max: {maxIterations} iterations"); +Console.WriteLine(new string('━', 40)); + +try { - private readonly string _workDir; - private readonly CopilotClient _client; - private readonly int _maxIterations; - private int _iteration = 0; + var prompt = await File.ReadAllTextAsync(promptFile); - public PersistentRalphLoop(string workDir, int maxIterations = 10) + for (var i = 1; i <= maxIterations; i++) { - _workDir = workDir; - _maxIterations = maxIterations; - Directory.CreateDirectory(_workDir); - _client = new CopilotClient(); - } - - public async Task RunAsync(string prompt) - { - await _client.StartAsync(); + Console.WriteLine($"\n=== Iteration {i}/{maxIterations} ==="); + // Fresh session — each task gets full context budget + var session = await client.CreateSessionAsync( + new SessionConfig { Model = "claude-sonnet-4.5" }); try { - var session = await _client.CreateSessionAsync( - new SessionConfig { Model = "gpt-5.1-codex-mini" }); - - try + var done = new TaskCompletionSource(); + session.On(evt => { - // Store initial prompt - var promptFile = Path.Combine(_workDir, "prompt.md"); - await File.WriteAllTextAsync(promptFile, prompt); + if (evt is AssistantMessageEvent msg) + done.TrySetResult(msg.Data.Content); + }); - var done = new TaskCompletionSource(); - string response = ""; - session.On(evt => - { - if (evt is AssistantMessageEvent msg) - { - response = msg.Data.Content; - done.TrySetResult(msg.Data.Content); - } - }); - - while (_iteration < _maxIterations) - { - _iteration++; - Console.WriteLine($"\n--- Iteration {_iteration} ---"); - - done = new TaskCompletionSource(); - - // Build context including previous work - var contextBuilder = new StringBuilder(prompt); - var previousOutput = Path.Combine(_workDir, $"output-{_iteration - 1}.txt"); - if (File.Exists(previousOutput)) - { - contextBuilder.AppendLine($"\nPrevious iteration output:\n{await File.ReadAllTextAsync(previousOutput)}"); - } - - await session.SendAsync(new MessageOptions { Prompt = contextBuilder.ToString() }); - await done.Task; - - // Persist output - await File.WriteAllTextAsync( - Path.Combine(_workDir, $"output-{_iteration}.txt"), - response); - - if (response.Contains("COMPLETE")) - { - return response; - } - } - - throw new InvalidOperationException("Max iterations reached"); - } - finally - { - await session.DisposeAsync(); - } + await session.SendAsync(new MessageOptions { Prompt = prompt }); + await done.Task; } finally { - await _client.StopAsync(); + await session.DisposeAsync(); } + + // Push changes after each iteration + try + { + Process.Start("git", $"push origin {branchName}")!.WaitForExit(); + } + catch + { + Process.Start("git", $"push -u origin {branchName}")!.WaitForExit(); + } + + 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. + +99999. When authoring documentation, capture the why. +999999. Implement completely. No placeholders or stubs. +9999999. 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. **Write clear completion criteria**: Include exactly what "done" looks like -2. **Use output markers**: Include `COMPLETE` or similar in completion condition -3. **Always set max iterations**: Prevents infinite loops on impossible tasks -4. **Persist state**: Save files so AI can see what changed between iterations -5. **Include context**: Feed previous iteration output back as context -6. **Monitor progress**: Log each iteration to track what's happening +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 -## Example: Iterative Code Generation - -```csharp -var prompt = @"Write a function that: -1. Parses CSV data -2. Validates required fields -3. Returns parsed records or error -4. Has unit tests -5. Output COMPLETE when done"; - -var loop = new RalphLoop(maxIterations: 10, completionPromise: "COMPLETE"); -var result = await loop.RunAsync(prompt); -``` - -## Handling Failures - -```csharp -try -{ - var result = await loop.RunAsync(prompt); - Console.WriteLine("Task completed successfully!"); -} -catch (InvalidOperationException ex) when (ex.Message.Contains("Max iterations")) -{ - Console.WriteLine("Task did not complete within iteration limit."); - Console.WriteLine($"Last response: {loop.LastResponse}"); - // Document what was attempted and suggest alternatives -} -``` - -## When to Use RALPH-loop +## When to Use a Ralph Loop **Good for:** -- Code generation with automatic verification (tests, linters) -- Tasks with clear success criteria -- Iterative refinement where each attempt learns from previous failures -- Unattended long-running improvements +- 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 or design input -- One-shot operations -- Tasks with vague success criteria -- Real-time interactive debugging +- 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 diff --git a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs index 0e81b5f8..c198c727 100644 --- a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs +++ b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs @@ -1,141 +1,90 @@ #:package GitHub.Copilot.SDK@* #:property PublishAot=false +using System.Diagnostics; using GitHub.Copilot.SDK; -using System.Text; -// RALPH-loop: Iterative self-referential AI loops. -// The same prompt is sent repeatedly, with AI reading its own previous output. -// Loop continues until completion promise is detected in the response. +// 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 prompt = @"You are iteratively building a small library. Follow these phases IN ORDER. -Do NOT skip ahead — only do the current phase, then stop and wait for the next iteration. +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"; -Phase 1: Design a DataValidator class that validates records against a schema. - - Schema defines field names, types (string, int, float, bool), and whether required. - - Return a list of validation errors per record. - - Show the class code only. Do NOT output COMPLETE. +var client = new CopilotClient(); +await client.StartAsync(); -Phase 2: Write at least 4 unit tests covering: missing required field, wrong type, - valid record, and empty input. Show test code only. Do NOT output COMPLETE. +var branchProc = Process.Start(new ProcessStartInfo("git", "branch --show-current") + { RedirectStandardOutput = true })!; +var branch = (await branchProc.StandardOutput.ReadToEndAsync()).Trim(); +await branchProc.WaitForExitAsync(); -Phase 3: Review the code from phases 1 and 2. Fix any bugs, add docstrings, and add - an extra edge-case test. Show the final consolidated code with all fixes. - When this phase is fully done, output the exact text: COMPLETE"; - -var loop = new RalphLoop(maxIterations: 5, completionPromise: "COMPLETE"); +Console.WriteLine(new string('━', 40)); +Console.WriteLine($"Mode: {mode}"); +Console.WriteLine($"Prompt: {promptFile}"); +Console.WriteLine($"Branch: {branch}"); +Console.WriteLine($"Max: {maxIterations} iterations"); +Console.WriteLine(new string('━', 40)); try { - var result = await loop.RunAsync(prompt); - Console.WriteLine("\n=== FINAL RESULT ==="); - Console.WriteLine(result); -} -catch (InvalidOperationException ex) -{ - Console.WriteLine($"\nTask did not complete: {ex.Message}"); - if (loop.LastResponse != null) + var prompt = await File.ReadAllTextAsync(promptFile); + + for (var i = 1; i <= maxIterations; i++) { - Console.WriteLine($"\nLast attempt:\n{loop.LastResponse}"); - } -} + Console.WriteLine($"\n=== Iteration {i}/{maxIterations} ==="); -// --- RalphLoop class definition --- - -public class RalphLoop -{ - private readonly CopilotClient _client; - private int _iteration = 0; - private readonly int _maxIterations; - private readonly string _completionPromise; - private string? _lastResponse; - - public RalphLoop(int maxIterations = 10, string completionPromise = "COMPLETE") - { - _client = new CopilotClient(); - _maxIterations = maxIterations; - _completionPromise = completionPromise; - } - - public string? LastResponse => _lastResponse; - - public async Task RunAsync(string initialPrompt) - { - await _client.StartAsync(); + // Fresh session — each task gets full context budget + var session = await client.CreateSessionAsync( + new SessionConfig { Model = "claude-sonnet-4.5" }); try { - var session = await _client.CreateSessionAsync(new SessionConfig - { - Model = "gpt-5.1-codex-mini" + var done = new TaskCompletionSource(); + session.On(evt => + { + if (evt is AssistantMessageEvent msg) + done.TrySetResult(msg.Data.Content); }); - try - { - var done = new TaskCompletionSource(); - session.On(evt => - { - if (evt is AssistantMessageEvent msg) - { - _lastResponse = msg.Data.Content; - done.TrySetResult(msg.Data.Content); - } - }); - - while (_iteration < _maxIterations) - { - _iteration++; - Console.WriteLine($"\n=== Iteration {_iteration}/{_maxIterations} ==="); - - done = new TaskCompletionSource(); - - var currentPrompt = BuildIterationPrompt(initialPrompt); - Console.WriteLine($"Sending prompt (length: {currentPrompt.Length})..."); - - await session.SendAsync(new MessageOptions { Prompt = currentPrompt }); - var response = await done.Task; - - var summary = response.Length > 200 - ? response.Substring(0, 200) + "..." - : response; - Console.WriteLine($"Response: {summary}"); - - if (response.Contains(_completionPromise)) - { - Console.WriteLine($"\n✓ Completion promise detected: '{_completionPromise}'"); - return response; - } - - Console.WriteLine($"Iteration {_iteration} complete. Continuing..."); - } - - throw new InvalidOperationException( - $"Max iterations ({_maxIterations}) reached without completion promise: '{_completionPromise}'"); - } - finally - { - await session.DisposeAsync(); - } + await session.SendAsync(new MessageOptions { Prompt = prompt }); + await done.Task; } finally { - await _client.StopAsync(); + await session.DisposeAsync(); } + + // Push changes after each iteration + try + { + Process.Start("git", $"push origin {branch}")!.WaitForExit(); + } + catch + { + Process.Start("git", $"push -u origin {branch}")!.WaitForExit(); + } + + Console.WriteLine($"\nIteration {i} complete."); } - private string BuildIterationPrompt(string initialPrompt) - { - if (_iteration == 1) - return initialPrompt; - - var sb = new StringBuilder(); - sb.AppendLine(initialPrompt); - sb.AppendLine(); - sb.AppendLine("=== CONTEXT FROM PREVIOUS ITERATION ==="); - sb.AppendLine(_lastResponse); - sb.AppendLine("=== END CONTEXT ==="); - sb.AppendLine(); - sb.AppendLine("Continue working on this task. Review the previous attempt and improve upon it."); - return sb.ToString(); - } + Console.WriteLine($"\nReached max iterations: {maxIterations}"); +} +finally +{ + await client.StopAsync(); } diff --git a/cookbook/copilot-sdk/go/ralph-loop.md b/cookbook/copilot-sdk/go/ralph-loop.md index 2469c181..44d0d1ca 100644 --- a/cookbook/copilot-sdk/go/ralph-loop.md +++ b/cookbook/copilot-sdk/go/ralph-loop.md @@ -1,6 +1,6 @@ -# RALPH-loop: Iterative Self-Referential AI Loops +# Ralph Loop: Autonomous AI Task Loops -Implement self-referential feedback loops where an AI agent iteratively improves work by reading its own previous output. +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) > @@ -9,27 +9,37 @@ Implement self-referential feedback loops where an AI agent iteratively improves > go run recipe/ralph-loop.go > ``` -## What is RALPH-loop? +## What is a Ralph Loop? -RALPH-loop is a development methodology for iterative AI-powered task completion. Named after the Ralph Wiggum technique, it embodies the philosophy of persistent iteration: +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. -- **One prompt, multiple iterations**: The same prompt is processed repeatedly -- **Self-referential feedback**: The AI reads its own previous work (file changes, git history) -- **Completion detection**: Loop exits when a completion promise is detected in output -- **Safety limits**: Always include a maximum iteration count to prevent infinite loops +``` +┌─────────────────────────────────────────────────┐ +│ 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) │ +└─────────────────────────────────────────────────┘ +``` -## Example Scenario +**Core principles:** -You need to iteratively improve code until all tests pass. Instead of asking Copilot to "write perfect code," you use RALPH-loop to: +- **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) -1. Send the initial prompt with clear success criteria -2. Copilot writes code and tests -3. Copilot runs tests and sees failures -4. Loop automatically re-sends the prompt -5. Copilot reads test output and previous code, fixes issues -6. Repeat until all tests pass and completion promise is output +## Simple Version -## Basic Implementation +The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | claude ; done`: ```go package main @@ -38,81 +48,59 @@ import ( "context" "fmt" "log" - "strings" + "os" copilot "github.com/github/copilot-sdk/go" ) -type RalphLoop struct { - client *copilot.Client - iteration int - maxIterations int - completionPromise string - LastResponse string -} - -func NewRalphLoop(maxIterations int, completionPromise string) *RalphLoop { - return &RalphLoop{ - client: copilot.NewClient(nil), - maxIterations: maxIterations, - completionPromise: completionPromise, +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() -func (r *RalphLoop) Run(ctx context.Context, initialPrompt string) (string, error) { - if err := r.client.Start(ctx); err != nil { - return "", err - } - defer r.client.Stop() - - session, err := r.client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-5.1-codex-mini", - }) + prompt, err := os.ReadFile(promptFile) if err != nil { - return "", err + return err } - defer session.Destroy() - for r.iteration < r.maxIterations { - r.iteration++ - fmt.Printf("\n--- Iteration %d/%d ---\n", r.iteration, r.maxIterations) + for i := 1; i <= maxIterations; i++ { + fmt.Printf("\n=== Iteration %d/%d ===\n", i, maxIterations) - prompt := r.buildIterationPrompt(initialPrompt) - - result, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt}) + // Fresh session each iteration — context isolation is the point + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "claude-sonnet-4.5", + }) if err != nil { - return "", err + return err } - if result != nil && result.Data.Content != nil { - r.LastResponse = *result.Data.Content + _, err = session.SendAndWait(ctx, copilot.MessageOptions{ + Prompt: string(prompt), + }) + session.Destroy() + if err != nil { + return err } - if strings.Contains(r.LastResponse, r.completionPromise) { - fmt.Printf("✓ Completion promise detected: %s\n", r.completionPromise) - return r.LastResponse, nil - } + fmt.Printf("Iteration %d complete.\n", i) } - - return "", fmt.Errorf("max iterations (%d) reached without completion promise", - r.maxIterations) + return nil } -// Usage func main() { - ctx := context.Background() - loop := NewRalphLoop(5, "COMPLETE") - result, err := loop.Run(ctx, "Your task here") - if err != nil { + if err := ralphLoop(context.Background(), "PROMPT.md", 20); err != nil { log.Fatal(err) } - fmt.Println(result) } ``` -## With File Persistence +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. -For tasks involving code generation, persist state to files so the AI can see changes: +## 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 @@ -120,121 +108,178 @@ package main import ( "context" "fmt" + "log" "os" - "path/filepath" + "os/exec" + "strconv" "strings" copilot "github.com/github/copilot-sdk/go" ) -type PersistentRalphLoop struct { - client *copilot.Client - workDir string - iteration int - maxIterations int -} - -func NewPersistentRalphLoop(workDir string, maxIterations int) *PersistentRalphLoop { - os.MkdirAll(workDir, 0755) - return &PersistentRalphLoop{ - client: copilot.NewClient(nil), - workDir: workDir, - maxIterations: maxIterations, +func ralphLoop(ctx context.Context, mode string, maxIterations int) error { + promptFile := "PROMPT_build.md" + if mode == "plan" { + promptFile = "PROMPT_plan.md" } -} -func (p *PersistentRalphLoop) Run(ctx context.Context, initialPrompt string) (string, error) { - if err := p.client.Start(ctx); err != nil { - return "", err + client := copilot.NewClient(nil) + if err := client.Start(ctx); err != nil { + return err } - defer p.client.Stop() + defer client.Stop() - os.WriteFile(filepath.Join(p.workDir, "prompt.md"), []byte(initialPrompt), 0644) + branchOut, _ := exec.Command("git", "branch", "--show-current").Output() + branch := strings.TrimSpace(string(branchOut)) - session, err := p.client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-5.1-codex-mini", - }) + fmt.Println(strings.Repeat("━", 40)) + fmt.Printf("Mode: %s\n", mode) + fmt.Printf("Prompt: %s\n", promptFile) + fmt.Printf("Branch: %s\n", branch) + fmt.Printf("Max: %d iterations\n", maxIterations) + fmt.Println(strings.Repeat("━", 40)) + + prompt, err := os.ReadFile(promptFile) if err != nil { - return "", err + return err } - defer session.Destroy() - for p.iteration < p.maxIterations { - p.iteration++ + for i := 1; i <= maxIterations; i++ { + fmt.Printf("\n=== Iteration %d/%d ===\n", i, maxIterations) - prompt := initialPrompt - prevFile := filepath.Join(p.workDir, fmt.Sprintf("output-%d.txt", p.iteration-1)) - if data, err := os.ReadFile(prevFile); err == nil { - prompt = fmt.Sprintf("%s\n\nPrevious iteration:\n%s", initialPrompt, string(data)) - } - - result, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt}) + // Fresh session — each task gets full context budget + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "claude-sonnet-4.5", + }) if err != nil { - return "", err + return err } - response := "" - if result != nil && result.Data.Content != nil { - response = *result.Data.Content + _, err = session.SendAndWait(ctx, copilot.MessageOptions{ + Prompt: string(prompt), + }) + session.Destroy() + if err != nil { + return err } - os.WriteFile(filepath.Join(p.workDir, fmt.Sprintf("output-%d.txt", p.iteration)), - []byte(response), 0644) + // Push changes after each iteration + if err := exec.Command("git", "push", "origin", branch).Run(); err != nil { + exec.Command("git", "push", "-u", "origin", branch).Run() + } - if strings.Contains(response, "COMPLETE") { - return response, nil + 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 } } - return "", fmt.Errorf("max iterations reached") + 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. + +99999. When authoring documentation, capture the why. +999999. Implement completely. No placeholders or stubs. +9999999. 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. **Write clear completion criteria**: Include exactly what "done" looks like -2. **Use output markers**: Include `COMPLETE` or similar in completion condition -3. **Always set max iterations**: Prevents infinite loops on impossible tasks -4. **Persist state**: Save files so AI can see what changed between iterations -5. **Include context**: Feed previous iteration output back as context -6. **Monitor progress**: Log each iteration to track what's happening +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 -## Example: Iterative Code Generation - -```go -prompt := `Write a function that: -1. Parses CSV data -2. Validates required fields -3. Returns parsed records or error -4. Has unit tests -5. Output COMPLETE when done` - -loop := NewRalphLoop(10, "COMPLETE") -result, err := loop.Run(context.Background(), prompt) -``` - -## Handling Failures - -```go -ctx := context.Background() -loop := NewRalphLoop(5, "COMPLETE") -result, err := loop.Run(ctx, prompt) -if err != nil { - log.Printf("Task failed: %v", err) - log.Printf("Last attempt: %s", loop.LastResponse) -} -``` - -## When to Use RALPH-loop +## When to Use a Ralph Loop **Good for:** -- Code generation with automatic verification (tests, linters) -- Tasks with clear success criteria -- Iterative refinement where each attempt learns from previous failures -- Unattended long-running improvements +- 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 or design input -- One-shot operations -- Tasks with vague success criteria -- Real-time interactive debugging +- 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 diff --git a/cookbook/copilot-sdk/go/recipe/ralph-loop.go b/cookbook/copilot-sdk/go/recipe/ralph-loop.go index b99fe54d..1d317842 100644 --- a/cookbook/copilot-sdk/go/recipe/ralph-loop.go +++ b/cookbook/copilot-sdk/go/recipe/ralph-loop.go @@ -4,127 +4,101 @@ import ( "context" "fmt" "log" + "os" + "os/exec" + "strconv" "strings" copilot "github.com/github/copilot-sdk/go" ) -// RalphLoop implements iterative self-referential feedback loops. -// The same prompt is sent repeatedly, with AI reading its own previous output. -// Loop continues until completion promise is detected in the response. -type RalphLoop struct { - client *copilot.Client - iteration int - maxIterations int - completionPromise string - LastResponse string -} +// 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 -// NewRalphLoop creates a new RALPH-loop instance. -func NewRalphLoop(maxIterations int, completionPromise string) *RalphLoop { - return &RalphLoop{ - client: copilot.NewClient(nil), - maxIterations: maxIterations, - completionPromise: completionPromise, +func ralphLoop(ctx context.Context, mode string, maxIterations int) error { + promptFile := "PROMPT_build.md" + if mode == "plan" { + promptFile = "PROMPT_plan.md" } -} -// Run executes the RALPH-loop until completion promise is detected or max iterations reached. -func (r *RalphLoop) Run(ctx context.Context, initialPrompt string) (string, error) { - if err := r.client.Start(ctx); err != nil { - return "", fmt.Errorf("failed to start client: %w", err) + client := copilot.NewClient(nil) + if err := client.Start(ctx); err != nil { + return fmt.Errorf("failed to start client: %w", err) } - defer r.client.Stop() + defer client.Stop() - session, err := r.client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-5.1-codex-mini", - }) + branchOut, _ := exec.Command("git", "branch", "--show-current").Output() + branch := strings.TrimSpace(string(branchOut)) + + fmt.Println(strings.Repeat("━", 40)) + fmt.Printf("Mode: %s\n", mode) + fmt.Printf("Prompt: %s\n", promptFile) + fmt.Printf("Branch: %s\n", branch) + 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 create session: %w", err) + return fmt.Errorf("failed to read %s: %w", promptFile, err) } - defer session.Destroy() - for r.iteration < r.maxIterations { - r.iteration++ - fmt.Printf("\n=== Iteration %d/%d ===\n", r.iteration, r.maxIterations) + for i := 1; i <= maxIterations; i++ { + fmt.Printf("\n=== Iteration %d/%d ===\n", i, maxIterations) - currentPrompt := r.buildIterationPrompt(initialPrompt) - fmt.Printf("Sending prompt (length: %d)...\n", len(currentPrompt)) - - result, err := session.SendAndWait(ctx, copilot.MessageOptions{ - Prompt: currentPrompt, + // Fresh session — each task gets full context budget + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "claude-sonnet-4.5", }) if err != nil { - return "", fmt.Errorf("send failed on iteration %d: %w", r.iteration, err) + return fmt.Errorf("failed to create session: %w", err) } - if result != nil && result.Data.Content != nil { - r.LastResponse = *result.Data.Content - } else { - r.LastResponse = "" + _, err = session.SendAndWait(ctx, copilot.MessageOptions{ + Prompt: string(prompt), + }) + session.Destroy() + if err != nil { + return fmt.Errorf("send failed on iteration %d: %w", i, err) } - // Display response summary - summary := r.LastResponse - if len(summary) > 200 { - summary = summary[:200] + "..." - } - fmt.Printf("Response: %s\n", summary) - - // Check for completion promise - if strings.Contains(r.LastResponse, r.completionPromise) { - fmt.Printf("\n✓ Success! Completion promise detected: '%s'\n", r.completionPromise) - return r.LastResponse, nil + // Push changes after each iteration + if err := exec.Command("git", "push", "origin", branch).Run(); err != nil { + exec.Command("git", "push", "-u", "origin", branch).Run() } - fmt.Printf("Iteration %d complete. Continuing...\n", r.iteration) + fmt.Printf("\nIteration %d complete.\n", i) } - return "", fmt.Errorf("maximum iterations (%d) reached without detecting completion promise: '%s'", - r.maxIterations, r.completionPromise) -} - -func (r *RalphLoop) buildIterationPrompt(initialPrompt string) string { - if r.iteration == 1 { - return initialPrompt - } - - return fmt.Sprintf(`%s - -=== CONTEXT FROM PREVIOUS ITERATION === -%s -=== END CONTEXT === - -Continue working on this task. Review the previous attempt and improve upon it.`, - initialPrompt, r.LastResponse) + fmt.Printf("\nReached max iterations: %d\n", maxIterations) + return nil } func main() { - prompt := `You are iteratively building a small library. Follow these phases IN ORDER. -Do NOT skip ahead — only do the current phase, then stop and wait for the next iteration. + mode := "build" + maxIterations := 50 -Phase 1: Design a DataValidator struct that validates records against a schema. - - Schema defines field names, types (string, int, float, bool), and whether required. - - Return a slice of validation errors per record. - - Show the struct and method code only. Do NOT output COMPLETE. - -Phase 2: Write at least 4 unit tests covering: missing required field, wrong type, - valid record, and empty input. Show test code only. Do NOT output COMPLETE. - -Phase 3: Review the code from phases 1 and 2. Fix any bugs, add doc comments, and add - an extra edge-case test. Show the final consolidated code with all fixes. - When this phase is fully done, output the exact text: COMPLETE` - - ctx := context.Background() - loop := NewRalphLoop(5, "COMPLETE") - - result, err := loop.Run(ctx, prompt) - if err != nil { - log.Printf("Task did not complete: %v", err) - log.Printf("Last attempt: %s", loop.LastResponse) - return + for _, arg := range os.Args[1:] { + if arg == "plan" { + mode = "plan" + } else if n, err := strconv.Atoi(arg); err == nil { + maxIterations = n + } } - fmt.Println("\n=== FINAL RESULT ===") - fmt.Println(result) + if err := ralphLoop(context.Background(), mode, maxIterations); err != nil { + log.Fatal(err) + } } diff --git a/cookbook/copilot-sdk/nodejs/ralph-loop.md b/cookbook/copilot-sdk/nodejs/ralph-loop.md index deafa628..41ad3c71 100644 --- a/cookbook/copilot-sdk/nodejs/ralph-loop.md +++ b/cookbook/copilot-sdk/nodejs/ralph-loop.md @@ -1,6 +1,6 @@ -# RALPH-loop: Iterative Self-Referential AI Loops +# Ralph Loop: Autonomous AI Task Loops -Implement self-referential feedback loops where an AI agent iteratively improves work by reading its own previous output. +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) > @@ -9,200 +9,217 @@ Implement self-referential feedback loops where an AI agent iteratively improves > npx tsx ralph-loop.ts > ``` -## What is RALPH-loop? +## What is a Ralph Loop? -RALPH-loop is a development methodology for iterative AI-powered task completion. Named after the Ralph Wiggum technique, it embodies the philosophy of persistent iteration: +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. -- **One prompt, multiple iterations**: The same prompt is processed repeatedly -- **Self-referential feedback**: The AI reads its own previous work (file changes, git history) -- **Completion detection**: Loop exits when a completion promise is detected in output -- **Safety limits**: Always include a maximum iteration count to prevent infinite loops - -## Example Scenario - -You need to iteratively improve code until all tests pass. Instead of asking Copilot to "write perfect code," you use RALPH-loop to: - -1. Send the initial prompt with clear success criteria -2. Copilot writes code and tests -3. Copilot runs tests and sees failures -4. Loop automatically re-sends the prompt -5. Copilot reads test output and previous code, fixes issues -6. Repeat until all tests pass and completion promise is output - -## Basic Implementation - -```typescript -import { CopilotClient } from "@github/copilot-sdk"; - -class RalphLoop { - private client: CopilotClient; - private iteration: number = 0; - private maxIterations: number; - private completionPromise: string; - private lastResponse: string | null = null; - - constructor(maxIterations: number = 10, completionPromise: string = "COMPLETE") { - this.client = new CopilotClient(); - this.maxIterations = maxIterations; - this.completionPromise = completionPromise; - } - - async run(initialPrompt: string): Promise { - await this.client.start(); - const session = await this.client.createSession({ model: "gpt-5.1-codex-mini" }); - - try { - while (this.iteration < this.maxIterations) { - this.iteration++; - console.log(`\n--- Iteration ${this.iteration}/${this.maxIterations} ---`); - - // Build prompt including previous response as context - const prompt = this.iteration === 1 - ? initialPrompt - : `${initialPrompt}\n\nPrevious attempt:\n${this.lastResponse}\n\nContinue improving...`; - - const response = await session.sendAndWait({ prompt }); - this.lastResponse = response?.data.content || ""; - - console.log(`Response (${this.lastResponse.length} chars)`); - - // Check for completion promise - if (this.lastResponse.includes(this.completionPromise)) { - console.log(`✓ Completion promise detected: ${this.completionPromise}`); - return this.lastResponse; - } - - console.log(`Continuing to iteration ${this.iteration + 1}...`); - } - - throw new Error( - `Max iterations (${this.maxIterations}) reached without completion promise` - ); - } finally { - await session.destroy(); - await this.client.stop(); - } - } -} - -// Usage -const loop = new RalphLoop(5, "COMPLETE"); -const result = await loop.run("Your task here"); -console.log(result); +``` +┌─────────────────────────────────────────────────┐ +│ 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) │ +└─────────────────────────────────────────────────┘ ``` -## With File Persistence +**Core principles:** -For tasks involving code generation, persist state to files so the AI can see changes: +- **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 | claude ; done`: ```typescript -import fs from "fs/promises"; -import path from "path"; +import { readFile } from "fs/promises"; import { CopilotClient } from "@github/copilot-sdk"; -class PersistentRalphLoop { - private client: CopilotClient; - private workDir: string; - private iteration: number = 0; - private maxIterations: number; +async function ralphLoop(promptFile: string, maxIterations: number = 50) { + const client = new CopilotClient(); + await client.start(); - constructor(workDir: string, maxIterations: number = 10) { - this.client = new CopilotClient(); - this.workDir = workDir; - this.maxIterations = maxIterations; - } + try { + const prompt = await readFile(promptFile, "utf-8"); - async run(initialPrompt: string): Promise { - await fs.mkdir(this.workDir, { recursive: true }); - await this.client.start(); - const session = await this.client.createSession({ model: "gpt-5.1-codex-mini" }); + for (let i = 1; i <= maxIterations; i++) { + console.log(`\n=== Iteration ${i}/${maxIterations} ===`); - try { - // Store initial prompt - await fs.writeFile(path.join(this.workDir, "prompt.md"), initialPrompt); - - while (this.iteration < this.maxIterations) { - this.iteration++; - console.log(`\n--- Iteration ${this.iteration} ---`); - - // Build context from previous outputs - let context = initialPrompt; - const prevOutputFile = path.join(this.workDir, `output-${this.iteration - 1}.txt`); - try { - const prevOutput = await fs.readFile(prevOutputFile, "utf-8"); - context += `\n\nPrevious iteration:\n${prevOutput}`; - } catch { - // No previous output yet - } - - const response = await session.sendAndWait({ prompt: context }); - const output = response?.data.content || ""; - - // Persist output - await fs.writeFile( - path.join(this.workDir, `output-${this.iteration}.txt`), - output - ); - - if (output.includes("COMPLETE")) { - return output; - } + // Fresh session each iteration — context isolation is the point + const session = await client.createSession({ model: "claude-sonnet-4.5" }); + try { + await session.sendAndWait({ prompt }, 600_000); + } finally { + await session.destroy(); } - throw new Error("Max iterations reached"); - } finally { - await session.destroy(); - await this.client.stop(); + 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 { execSync } from "child_process"; +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(); + + const branch = execSync("git branch --show-current", { encoding: "utf-8" }).trim(); + console.log(`Mode: ${mode} | Prompt: ${promptFile} | Branch: ${branch}`); + + try { + const prompt = await readFile(promptFile, "utf-8"); + + for (let i = 1; i <= maxIterations; i++) { + console.log(`\n=== Iteration ${i}/${maxIterations} ===`); + + // Fresh session — each task gets full context budget + const session = await client.createSession({ model: "claude-sonnet-4.5" }); + try { + await session.sendAndWait({ prompt }, 600_000); + } finally { + await session.destroy(); + } + + // Push changes after each iteration + try { + execSync(`git push origin ${branch}`, { stdio: "inherit" }); + } catch { + execSync(`git push -u origin ${branch}`, { stdio: "inherit" }); + } + + 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. + +99999. When authoring documentation, capture the why. +999999. Implement completely. No placeholders or stubs. +9999999. 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. **Write clear completion criteria**: Include exactly what "done" looks like -2. **Use output markers**: Include `COMPLETE` or similar in completion condition -3. **Always set max iterations**: Prevents infinite loops on impossible tasks -4. **Persist state**: Save files so AI can see what changed between iterations -5. **Include context**: Feed previous iteration output back as context -6. **Monitor progress**: Log each iteration to track what's happening +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 -## Example: Iterative Code Generation - -```typescript -const prompt = `Write a function that: -1. Parses CSV data -2. Validates required fields -3. Returns parsed records or error -4. Has unit tests -5. Output COMPLETE when done`; - -const loop = new RalphLoop(10, "COMPLETE"); -const result = await loop.run(prompt); -``` - -## Handling Failures - -```typescript -try { - const result = await loop.run(prompt); - console.log("Task completed successfully!"); -} catch (error) { - console.error("Task failed:", error.message); - // Analyze what was attempted and suggest alternatives -} -``` - -## When to Use RALPH-loop +## When to Use a Ralph Loop **Good for:** -- Code generation with automatic verification (tests, linters) -- Tasks with clear success criteria -- Iterative refinement where each attempt learns from previous failures -- Unattended long-running improvements +- 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 or design input -- One-shot operations -- Tasks with vague success criteria -- Real-time interactive debugging +- 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 diff --git a/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts index 93a7ebb2..018b8074 100644 --- a/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts +++ b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts @@ -1,128 +1,79 @@ +import { readFile } from "fs/promises"; +import { execSync } from "child_process"; import { CopilotClient } from "@github/copilot-sdk"; /** - * RALPH-loop implementation: Iterative self-referential AI loops. - * The same prompt is sent repeatedly, with AI reading its own previous output. - * Loop continues until completion promise is detected in the response. + * 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 */ -class RalphLoop { - private client: CopilotClient; - private iteration: number = 0; - private readonly maxIterations: number; - private readonly completionPromise: string; - public lastResponse: string | null = null; - constructor(maxIterations: number = 10, completionPromise: string = "COMPLETE") { - this.client = new CopilotClient(); - this.maxIterations = maxIterations; - this.completionPromise = completionPromise; - } +type Mode = "plan" | "build"; - /** - * Run the RALPH-loop until completion promise is detected or max iterations reached. - */ - async run(initialPrompt: string): Promise { - let session: Awaited> | null = null; +async function ralphLoop(mode: Mode, maxIterations: number) { + const promptFile = mode === "plan" ? "PROMPT_plan.md" : "PROMPT_build.md"; - await this.client.start(); - try { - session = await this.client.createSession({ - model: "gpt-5.1-codex-mini" + const client = new CopilotClient(); + await client.start(); + + const branch = execSync("git branch --show-current", { encoding: "utf-8" }).trim(); + + console.log("━".repeat(40)); + console.log(`Mode: ${mode}`); + console.log(`Prompt: ${promptFile}`); + console.log(`Branch: ${branch}`); + 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} ===`); + + // Fresh session — each task gets full context budget + const session = await client.createSession({ + model: "claude-sonnet-4.5", }); try { - while (this.iteration < this.maxIterations) { - this.iteration++; - console.log(`\n=== Iteration ${this.iteration}/${this.maxIterations} ===`); - - // Build the prompt for this iteration - const currentPrompt = this.buildIterationPrompt(initialPrompt); - console.log(`Sending prompt (length: ${currentPrompt.length})...`); - - const response = await session.sendAndWait({ prompt: currentPrompt }, 300_000); - this.lastResponse = response?.data.content || ""; - - // Display response summary - const summary = this.lastResponse.length > 200 - ? this.lastResponse.substring(0, 200) + "..." - : this.lastResponse; - console.log(`Response: ${summary}`); - - // Check for completion promise - if (this.lastResponse.includes(this.completionPromise)) { - console.log(`\n✓ Success! Completion promise detected: '${this.completionPromise}'`); - return this.lastResponse; - } - - console.log(`Iteration ${this.iteration} complete. Checking for next iteration...`); - } - - // Max iterations reached without completion - throw new Error( - `Maximum iterations (${this.maxIterations}) reached without detecting completion promise: '${this.completionPromise}'` - ); - } catch (error) { - console.error(`\nError during RALPH-loop: ${error instanceof Error ? error.message : String(error)}`); - throw error; + await session.sendAndWait({ prompt }, 600_000); } finally { - if (session) { - await session.destroy(); - } + await session.destroy(); } - } finally { - await this.client.stop(); - } - } - /** - * Build the prompt for the current iteration, including previous output as context. - */ - private buildIterationPrompt(initialPrompt: string): string { - if (this.iteration === 1) { - // First iteration: just the initial prompt - return initialPrompt; + // Push changes after each iteration + try { + execSync(`git push origin ${branch}`, { stdio: "inherit" }); + } catch { + execSync(`git push -u origin ${branch}`, { stdio: "inherit" }); + } + + console.log(`\nIteration ${i} complete.`); } - // Subsequent iterations: include previous output as context - return `${initialPrompt} - -=== CONTEXT FROM PREVIOUS ITERATION === -${this.lastResponse} -=== END CONTEXT === - -Continue working on this task. Review the previous attempt and improve upon it.`; + console.log(`\nReached max iterations: ${maxIterations}`); + } finally { + await client.stop(); } } -// Example usage demonstrating RALPH-loop -async function main() { - const prompt = `You are iteratively building a small library. Follow these phases IN ORDER. -Do NOT skip ahead — only do the current phase, then stop and wait for the next iteration. +// 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; -Phase 1: Design a DataValidator class that validates records against a schema. - - Schema defines field names, types (str, int, float, bool), and whether required. - - Return a list of validation errors per record. - - Show the class code only. Do NOT output COMPLETE. - -Phase 2: Write at least 4 unit tests covering: missing required field, wrong type, - valid record, and empty input. Show test code only. Do NOT output COMPLETE. - -Phase 3: Review the code from phases 1 and 2. Fix any bugs, add docstrings, and add - an extra edge-case test. Show the final consolidated code with all fixes. - When this phase is fully done, output the exact text: COMPLETE`; - - const loop = new RalphLoop(5, "COMPLETE"); - - try { - const result = await loop.run(prompt); - console.log("\n=== FINAL RESULT ==="); - console.log(result); - } catch (error) { - console.error(`\nTask did not complete: ${error instanceof Error ? error.message : String(error)}`); - if (loop.lastResponse) { - console.log(`\nLast attempt:\n${loop.lastResponse}`); - } - } -} - -main().catch(console.error); +ralphLoop(mode, maxIterations).catch(console.error); diff --git a/cookbook/copilot-sdk/python/ralph-loop.md b/cookbook/copilot-sdk/python/ralph-loop.md index 9a969ce6..fb8e3ced 100644 --- a/cookbook/copilot-sdk/python/ralph-loop.md +++ b/cookbook/copilot-sdk/python/ralph-loop.md @@ -1,6 +1,6 @@ -# RALPH-loop: Iterative Self-Referential AI Loops +# Ralph Loop: Autonomous AI Task Loops -Implement self-referential feedback loops where an AI agent iteratively improves work by reading its own previous output. +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) > @@ -8,196 +8,235 @@ Implement self-referential feedback loops where an AI agent iteratively improves > cd recipe && pip install -r requirements.txt > python ralph_loop.py > ``` -## What is RALPH-loop? -RALPH-loop is a development methodology for iterative AI-powered task completion. Named after the Ralph Wiggum technique, it embodies the philosophy of persistent iteration: +## What is a Ralph Loop? -- **One prompt, multiple iterations**: The same prompt is processed repeatedly -- **Self-referential feedback**: The AI reads its own previous work (file changes, git history) -- **Completion detection**: Loop exits when a completion promise is detected in output -- **Safety limits**: Always include a maximum iteration count to prevent infinite loops +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. -## Example Scenario - -You need to iteratively improve code until all tests pass. Instead of asking Copilot to "write perfect code," you use RALPH-loop to: - -1. Send the initial prompt with clear success criteria -2. Copilot writes code and tests -3. Copilot runs tests and sees failures -4. Loop automatically re-sends the prompt -5. Copilot reads test output and previous code, fixes issues -6. Repeat until all tests pass and completion promise is output - -## Basic Implementation - -```python -import asyncio -from copilot import CopilotClient, MessageOptions, SessionConfig - -class RalphLoop: - """Iterative self-referential feedback loop using Copilot.""" - - def __init__(self, max_iterations=10, completion_promise="COMPLETE"): - self.client = CopilotClient() - self.iteration = 0 - self.max_iterations = max_iterations - self.completion_promise = completion_promise - self.last_response = None - - async def run(self, initial_prompt): - """Run the RALPH-loop until completion promise detected or max iterations reached.""" - await self.client.start() - session = await self.client.create_session( - SessionConfig(model="gpt-5.1-codex-mini") - ) - - try: - while self.iteration < self.max_iterations: - self.iteration += 1 - print(f"\n--- Iteration {self.iteration}/{self.max_iterations} ---") - - # Build prompt including previous response as context - if self.iteration == 1: - prompt = initial_prompt - else: - prompt = f"{initial_prompt}\n\nPrevious attempt:\n{self.last_response}\n\nContinue improving..." - - result = await session.send_and_wait( - MessageOptions(prompt=prompt), timeout=300 - ) - - self.last_response = result.data.content if result else "" - print(f"Response ({len(self.last_response)} chars)") - - # Check for completion promise - if self.completion_promise in self.last_response: - print(f"✓ Completion promise detected: {self.completion_promise}") - return self.last_response - - print(f"Continuing to iteration {self.iteration + 1}...") - - raise RuntimeError( - f"Max iterations ({self.max_iterations}) reached without completion promise" - ) - finally: - await session.destroy() - await self.client.stop() - -# Usage -async def main(): - loop = RalphLoop(5, "COMPLETE") - result = await loop.run("Your task here") - print(result) - -asyncio.run(main()) +``` +┌─────────────────────────────────────────────────┐ +│ 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) │ +└─────────────────────────────────────────────────┘ ``` -## With File Persistence +**Core principles:** -For tasks involving code generation, persist state to files so the AI can see changes: +- **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 | claude ; done`: ```python import asyncio from pathlib import Path from copilot import CopilotClient, MessageOptions, SessionConfig -class PersistentRalphLoop: - """RALPH-loop with file-based state persistence.""" - - def __init__(self, work_dir, max_iterations=10): - self.client = CopilotClient() - self.work_dir = Path(work_dir) - self.work_dir.mkdir(parents=True, exist_ok=True) - self.iteration = 0 - self.max_iterations = max_iterations - async def run(self, initial_prompt): - """Run the loop with persistent state.""" - await self.client.start() - session = await self.client.create_session( - SessionConfig(model="gpt-5.1-codex-mini") - ) +async def ralph_loop(prompt_file: str, max_iterations: int = 50): + client = CopilotClient() + await client.start() - try: - # Store initial prompt - (self.work_dir / "prompt.md").write_text(initial_prompt) + try: + prompt = Path(prompt_file).read_text() - while self.iteration < self.max_iterations: - self.iteration += 1 - print(f"\n--- Iteration {self.iteration} ---") + for i in range(1, max_iterations + 1): + print(f"\n=== Iteration {i}/{max_iterations} ===") - # Build context from previous outputs - context = initial_prompt - prev_output = self.work_dir / f"output-{self.iteration - 1}.txt" - if prev_output.exists(): - context += f"\n\nPrevious iteration:\n{prev_output.read_text()}" - - result = await session.send_and_wait( - MessageOptions(prompt=context), timeout=300 + # Fresh session each iteration — context isolation is the point + session = await client.create_session( + SessionConfig(model="claude-sonnet-4.5") + ) + try: + await session.send_and_wait( + MessageOptions(prompt=prompt), timeout=600 ) - response = result.data.content if result else "" + finally: + await session.destroy() - # Persist output - output_file = self.work_dir / f"output-{self.iteration}.txt" - output_file.write_text(response) + print(f"Iteration {i} complete.") + finally: + await client.stop() - if "COMPLETE" in response: - return response - raise RuntimeError("Max iterations reached") - finally: - await session.destroy() - await self.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 subprocess +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() + + branch = subprocess.check_output( + ["git", "branch", "--show-current"], text=True + ).strip() + + print("━" * 40) + print(f"Mode: {mode}") + print(f"Prompt: {prompt_file}") + print(f"Branch: {branch}") + 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} ===") + + # Fresh session — each task gets full context budget + session = await client.create_session( + SessionConfig(model="claude-sonnet-4.5") + ) + try: + await session.send_and_wait( + MessageOptions(prompt=prompt), timeout=600 + ) + finally: + await session.destroy() + + # Push changes after each iteration + try: + subprocess.run( + ["git", "push", "origin", branch], check=True + ) + except subprocess.CalledProcessError: + subprocess.run( + ["git", "push", "-u", "origin", branch], check=True + ) + + 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. + +99999. When authoring documentation, capture the why. +999999. Implement completely. No placeholders or stubs. +9999999. 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. **Write clear completion criteria**: Include exactly what "done" looks like -2. **Use output markers**: Include `COMPLETE` or similar in completion condition -3. **Always set max iterations**: Prevents infinite loops on impossible tasks -4. **Persist state**: Save files so AI can see what changed between iterations -5. **Include context**: Feed previous iteration output back as context -6. **Monitor progress**: Log each iteration to track what's happening +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 -## Example: Iterative Code Generation - -```python -prompt = """Write a function that: -1. Parses CSV data -2. Validates required fields -3. Returns parsed records or error -4. Has unit tests -5. Output COMPLETE when done""" - -async def main(): - loop = RalphLoop(10, "COMPLETE") - result = await loop.run(prompt) - -asyncio.run(main()) -``` - -## Handling Failures - -```python -try: - result = await loop.run(prompt) - print("Task completed successfully!") -except RuntimeError as e: - print(f"Task failed: {e}") - if loop.last_response: - print(f"\nLast attempt:\n{loop.last_response}") -``` - -## When to Use RALPH-loop +## When to Use a Ralph Loop **Good for:** -- Code generation with automatic verification (tests, linters) -- Tasks with clear success criteria -- Iterative refinement where each attempt learns from previous failures -- Unattended long-running improvements +- 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 or design input -- One-shot operations -- Tasks with vague success criteria -- Real-time interactive debugging +- 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 diff --git a/cookbook/copilot-sdk/python/recipe/ralph_loop.py b/cookbook/copilot-sdk/python/recipe/ralph_loop.py index 00ecadcc..5e9d9422 100644 --- a/cookbook/copilot-sdk/python/recipe/ralph_loop.py +++ b/cookbook/copilot-sdk/python/recipe/ralph_loop.py @@ -1,127 +1,84 @@ #!/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 subprocess +import sys +from pathlib import Path from copilot import CopilotClient, MessageOptions, SessionConfig -class RalphLoop: - """ - RALPH-loop implementation: Iterative self-referential AI loops. +async def ralph_loop(mode: str = "build", max_iterations: int = 50): + prompt_file = "PROMPT_plan.md" if mode == "plan" else "PROMPT_build.md" - The same prompt is sent repeatedly, with AI reading its own previous output. - Loop continues until completion promise is detected in the response. - """ + client = CopilotClient() + await client.start() - def __init__(self, max_iterations=10, completion_promise="COMPLETE"): - """Initialize RALPH-loop with iteration limits and completion detection.""" - self.client = CopilotClient() - self.iteration = 0 - self.max_iterations = max_iterations - self.completion_promise = completion_promise - self.last_response = None + branch = subprocess.check_output( + ["git", "branch", "--show-current"], text=True + ).strip() - async def run(self, initial_prompt): - """ - Run the RALPH-loop until completion promise is detected or max iterations reached. - """ - session = None - await self.client.start() - try: - session = await self.client.create_session( - SessionConfig(model="gpt-5.1-codex-mini") - ) - - try: - while self.iteration < self.max_iterations: - self.iteration += 1 - print(f"\n=== Iteration {self.iteration}/{self.max_iterations} ===") - - current_prompt = self._build_iteration_prompt(initial_prompt) - print(f"Sending prompt (length: {len(current_prompt)})...") - - result = await session.send_and_wait( - MessageOptions(prompt=current_prompt), - timeout=300, - ) - - self.last_response = result.data.content if result else "" - - # Display response summary - summary = ( - self.last_response[:200] + "..." - if len(self.last_response) > 200 - else self.last_response - ) - print(f"Response: {summary}") - - # Check for completion promise - if self.completion_promise in self.last_response: - print( - f"\n✓ Success! Completion promise detected: '{self.completion_promise}'" - ) - return self.last_response - - print( - f"Iteration {self.iteration} complete. Checking for next iteration..." - ) - - raise RuntimeError( - f"Maximum iterations ({self.max_iterations}) reached without " - f"detecting completion promise: '{self.completion_promise}'" - ) - - except Exception as e: - print(f"\nError during RALPH-loop: {e}") - raise - finally: - if session is not None: - await session.destroy() - finally: - await self.client.stop() - - def _build_iteration_prompt(self, initial_prompt): - """Build the prompt for the current iteration, including previous output as context.""" - if self.iteration == 1: - return initial_prompt - - return f"""{initial_prompt} - -=== CONTEXT FROM PREVIOUS ITERATION === -{self.last_response} -=== END CONTEXT === - -Continue working on this task. Review the previous attempt and improve upon it.""" - - -async def main(): - """Example usage demonstrating RALPH-loop.""" - prompt = """You are iteratively building a small library. Follow these phases IN ORDER. -Do NOT skip ahead — only do the current phase, then stop and wait for the next iteration. - -Phase 1: Design a DataValidator class that validates records against a schema. - - Schema defines field names, types (str, int, float, bool), and whether required. - - Return a list of validation errors per record. - - Show the class code only. Do NOT output COMPLETE. - -Phase 2: Write at least 4 unit tests covering: missing required field, wrong type, - valid record, and empty input. Show test code only. Do NOT output COMPLETE. - -Phase 3: Review the code from phases 1 and 2. Fix any bugs, add docstrings, and add - an extra edge-case test. Show the final consolidated code with all fixes. - When this phase is fully done, output the exact text: COMPLETE""" - - loop = RalphLoop(max_iterations=5, completion_promise="COMPLETE") + print("━" * 40) + print(f"Mode: {mode}") + print(f"Prompt: {prompt_file}") + print(f"Branch: {branch}") + print(f"Max: {max_iterations} iterations") + print("━" * 40) try: - result = await loop.run(prompt) - print("\n=== FINAL RESULT ===") - print(result) - except RuntimeError as e: - print(f"\nTask did not complete: {e}") - if loop.last_response: - print(f"\nLast attempt:\n{loop.last_response}") + prompt = Path(prompt_file).read_text() + + for i in range(1, max_iterations + 1): + print(f"\n=== Iteration {i}/{max_iterations} ===") + + # Fresh session — each task gets full context budget + session = await client.create_session( + SessionConfig(model="claude-sonnet-4.5") + ) + try: + await session.send_and_wait( + MessageOptions(prompt=prompt), timeout=600 + ) + finally: + await session.destroy() + + # Push changes after each iteration + try: + subprocess.run( + ["git", "push", "origin", branch], check=True + ) + except subprocess.CalledProcessError: + subprocess.run( + ["git", "push", "-u", "origin", branch], check=True + ) + + print(f"\nIteration {i} complete.") + + print(f"\nReached max iterations: {max_iterations}") + finally: + await client.stop() if __name__ == "__main__": - asyncio.run(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)) From 92df16da5af80cffab9a8a3240dd77ee941e5b5d Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 11:32:42 -0800 Subject: [PATCH 11/21] Remove git commands from Ralph loop recipes Git operations (commit, push) belong in the prompt instructions, not hardcoded in the loop orchestrator. The SDK recipes should focus purely on the SDK API: create client, create session, send prompt, destroy. --- cookbook/copilot-sdk/dotnet/ralph-loop.md | 18 ------------------ .../copilot-sdk/dotnet/recipe/ralph-loop.cs | 17 ----------------- cookbook/copilot-sdk/go/ralph-loop.md | 11 ----------- cookbook/copilot-sdk/go/recipe/ralph-loop.go | 10 ---------- cookbook/copilot-sdk/nodejs/ralph-loop.md | 11 +---------- .../copilot-sdk/nodejs/recipe/ralph-loop.ts | 11 ----------- cookbook/copilot-sdk/python/ralph-loop.md | 16 ---------------- .../copilot-sdk/python/recipe/ralph_loop.py | 16 ---------------- 8 files changed, 1 insertion(+), 109 deletions(-) diff --git a/cookbook/copilot-sdk/dotnet/ralph-loop.md b/cookbook/copilot-sdk/dotnet/ralph-loop.md index 1cc98762..0b2bb570 100644 --- a/cookbook/copilot-sdk/dotnet/ralph-loop.md +++ b/cookbook/copilot-sdk/dotnet/ralph-loop.md @@ -92,7 +92,6 @@ This is all you need to get started. The prompt file tells the agent what to do; The full Ralph pattern with planning and building modes, matching the [Ralph Playbook](https://github.com/ClaytonFarr/ralph-playbook) architecture: ```csharp -using System.Diagnostics; using GitHub.Copilot.SDK; // Parse args: dotnet run [plan] [max_iterations] @@ -104,16 +103,9 @@ var promptFile = mode == "plan" ? "PROMPT_plan.md" : "PROMPT_build.md"; var client = new CopilotClient(); await client.StartAsync(); -var branchInfo = new ProcessStartInfo("git", "branch --show-current") - { RedirectStandardOutput = true }; -var branch = Process.Start(branchInfo)!; -var branchName = (await branch.StandardOutput.ReadToEndAsync()).Trim(); -await branch.WaitForExitAsync(); - Console.WriteLine(new string('━', 40)); Console.WriteLine($"Mode: {mode}"); Console.WriteLine($"Prompt: {promptFile}"); -Console.WriteLine($"Branch: {branchName}"); Console.WriteLine($"Max: {maxIterations} iterations"); Console.WriteLine(new string('━', 40)); @@ -145,16 +137,6 @@ try await session.DisposeAsync(); } - // Push changes after each iteration - try - { - Process.Start("git", $"push origin {branchName}")!.WaitForExit(); - } - catch - { - Process.Start("git", $"push -u origin {branchName}")!.WaitForExit(); - } - Console.WriteLine($"\nIteration {i} complete."); } diff --git a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs index c198c727..920a78f2 100644 --- a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs +++ b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs @@ -1,7 +1,6 @@ #:package GitHub.Copilot.SDK@* #:property PublishAot=false -using System.Diagnostics; using GitHub.Copilot.SDK; // Ralph loop: autonomous AI task loop with fresh context per iteration. @@ -28,15 +27,9 @@ var promptFile = mode == "plan" ? "PROMPT_plan.md" : "PROMPT_build.md"; var client = new CopilotClient(); await client.StartAsync(); -var branchProc = Process.Start(new ProcessStartInfo("git", "branch --show-current") - { RedirectStandardOutput = true })!; -var branch = (await branchProc.StandardOutput.ReadToEndAsync()).Trim(); -await branchProc.WaitForExitAsync(); - Console.WriteLine(new string('━', 40)); Console.WriteLine($"Mode: {mode}"); Console.WriteLine($"Prompt: {promptFile}"); -Console.WriteLine($"Branch: {branch}"); Console.WriteLine($"Max: {maxIterations} iterations"); Console.WriteLine(new string('━', 40)); @@ -69,16 +62,6 @@ try await session.DisposeAsync(); } - // Push changes after each iteration - try - { - Process.Start("git", $"push origin {branch}")!.WaitForExit(); - } - catch - { - Process.Start("git", $"push -u origin {branch}")!.WaitForExit(); - } - Console.WriteLine($"\nIteration {i} complete."); } diff --git a/cookbook/copilot-sdk/go/ralph-loop.md b/cookbook/copilot-sdk/go/ralph-loop.md index 44d0d1ca..cc7807c4 100644 --- a/cookbook/copilot-sdk/go/ralph-loop.md +++ b/cookbook/copilot-sdk/go/ralph-loop.md @@ -110,9 +110,7 @@ import ( "fmt" "log" "os" - "os/exec" "strconv" - "strings" copilot "github.com/github/copilot-sdk/go" ) @@ -129,13 +127,9 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { } defer client.Stop() - branchOut, _ := exec.Command("git", "branch", "--show-current").Output() - branch := strings.TrimSpace(string(branchOut)) - fmt.Println(strings.Repeat("━", 40)) fmt.Printf("Mode: %s\n", mode) fmt.Printf("Prompt: %s\n", promptFile) - fmt.Printf("Branch: %s\n", branch) fmt.Printf("Max: %d iterations\n", maxIterations) fmt.Println(strings.Repeat("━", 40)) @@ -163,11 +157,6 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { return err } - // Push changes after each iteration - if err := exec.Command("git", "push", "origin", branch).Run(); err != nil { - exec.Command("git", "push", "-u", "origin", branch).Run() - } - fmt.Printf("\nIteration %d complete.\n", i) } diff --git a/cookbook/copilot-sdk/go/recipe/ralph-loop.go b/cookbook/copilot-sdk/go/recipe/ralph-loop.go index 1d317842..18de5976 100644 --- a/cookbook/copilot-sdk/go/recipe/ralph-loop.go +++ b/cookbook/copilot-sdk/go/recipe/ralph-loop.go @@ -5,7 +5,6 @@ import ( "fmt" "log" "os" - "os/exec" "strconv" "strings" @@ -40,13 +39,9 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { } defer client.Stop() - branchOut, _ := exec.Command("git", "branch", "--show-current").Output() - branch := strings.TrimSpace(string(branchOut)) - fmt.Println(strings.Repeat("━", 40)) fmt.Printf("Mode: %s\n", mode) fmt.Printf("Prompt: %s\n", promptFile) - fmt.Printf("Branch: %s\n", branch) fmt.Printf("Max: %d iterations\n", maxIterations) fmt.Println(strings.Repeat("━", 40)) @@ -74,11 +69,6 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { return fmt.Errorf("send failed on iteration %d: %w", i, err) } - // Push changes after each iteration - if err := exec.Command("git", "push", "origin", branch).Run(); err != nil { - exec.Command("git", "push", "-u", "origin", branch).Run() - } - fmt.Printf("\nIteration %d complete.\n", i) } diff --git a/cookbook/copilot-sdk/nodejs/ralph-loop.md b/cookbook/copilot-sdk/nodejs/ralph-loop.md index 41ad3c71..13e74fc8 100644 --- a/cookbook/copilot-sdk/nodejs/ralph-loop.md +++ b/cookbook/copilot-sdk/nodejs/ralph-loop.md @@ -82,7 +82,6 @@ The full Ralph pattern with planning and building modes, matching the [Ralph Pla ```typescript import { readFile } from "fs/promises"; -import { execSync } from "child_process"; import { CopilotClient } from "@github/copilot-sdk"; type Mode = "plan" | "build"; @@ -92,8 +91,7 @@ async function ralphLoop(mode: Mode, maxIterations: number = 50) { const client = new CopilotClient(); await client.start(); - const branch = execSync("git branch --show-current", { encoding: "utf-8" }).trim(); - console.log(`Mode: ${mode} | Prompt: ${promptFile} | Branch: ${branch}`); + console.log(`Mode: ${mode} | Prompt: ${promptFile}`); try { const prompt = await readFile(promptFile, "utf-8"); @@ -109,13 +107,6 @@ async function ralphLoop(mode: Mode, maxIterations: number = 50) { await session.destroy(); } - // Push changes after each iteration - try { - execSync(`git push origin ${branch}`, { stdio: "inherit" }); - } catch { - execSync(`git push -u origin ${branch}`, { stdio: "inherit" }); - } - console.log(`Iteration ${i} complete.`); } } finally { diff --git a/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts index 018b8074..3bd5e4cf 100644 --- a/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts +++ b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts @@ -1,5 +1,4 @@ import { readFile } from "fs/promises"; -import { execSync } from "child_process"; import { CopilotClient } from "@github/copilot-sdk"; /** @@ -28,12 +27,9 @@ async function ralphLoop(mode: Mode, maxIterations: number) { const client = new CopilotClient(); await client.start(); - const branch = execSync("git branch --show-current", { encoding: "utf-8" }).trim(); - console.log("━".repeat(40)); console.log(`Mode: ${mode}`); console.log(`Prompt: ${promptFile}`); - console.log(`Branch: ${branch}`); console.log(`Max: ${maxIterations} iterations`); console.log("━".repeat(40)); @@ -54,13 +50,6 @@ async function ralphLoop(mode: Mode, maxIterations: number) { await session.destroy(); } - // Push changes after each iteration - try { - execSync(`git push origin ${branch}`, { stdio: "inherit" }); - } catch { - execSync(`git push -u origin ${branch}`, { stdio: "inherit" }); - } - console.log(`\nIteration ${i} complete.`); } diff --git a/cookbook/copilot-sdk/python/ralph-loop.md b/cookbook/copilot-sdk/python/ralph-loop.md index fb8e3ced..ae666eb2 100644 --- a/cookbook/copilot-sdk/python/ralph-loop.md +++ b/cookbook/copilot-sdk/python/ralph-loop.md @@ -85,7 +85,6 @@ The full Ralph pattern with planning and building modes, matching the [Ralph Pla ```python import asyncio -import subprocess import sys from pathlib import Path @@ -97,14 +96,9 @@ async def ralph_loop(mode: str = "build", max_iterations: int = 50): client = CopilotClient() await client.start() - branch = subprocess.check_output( - ["git", "branch", "--show-current"], text=True - ).strip() - print("━" * 40) print(f"Mode: {mode}") print(f"Prompt: {prompt_file}") - print(f"Branch: {branch}") print(f"Max: {max_iterations} iterations") print("━" * 40) @@ -125,16 +119,6 @@ async def ralph_loop(mode: str = "build", max_iterations: int = 50): finally: await session.destroy() - # Push changes after each iteration - try: - subprocess.run( - ["git", "push", "origin", branch], check=True - ) - except subprocess.CalledProcessError: - subprocess.run( - ["git", "push", "-u", "origin", branch], check=True - ) - print(f"\nIteration {i} complete.") print(f"\nReached max iterations: {max_iterations}") diff --git a/cookbook/copilot-sdk/python/recipe/ralph_loop.py b/cookbook/copilot-sdk/python/recipe/ralph_loop.py index 5e9d9422..f6ef1f62 100644 --- a/cookbook/copilot-sdk/python/recipe/ralph_loop.py +++ b/cookbook/copilot-sdk/python/recipe/ralph_loop.py @@ -19,7 +19,6 @@ Usage: """ import asyncio -import subprocess import sys from pathlib import Path @@ -32,14 +31,9 @@ async def ralph_loop(mode: str = "build", max_iterations: int = 50): client = CopilotClient() await client.start() - branch = subprocess.check_output( - ["git", "branch", "--show-current"], text=True - ).strip() - print("━" * 40) print(f"Mode: {mode}") print(f"Prompt: {prompt_file}") - print(f"Branch: {branch}") print(f"Max: {max_iterations} iterations") print("━" * 40) @@ -60,16 +54,6 @@ async def ralph_loop(mode: str = "build", max_iterations: int = 50): finally: await session.destroy() - # Push changes after each iteration - try: - subprocess.run( - ["git", "push", "origin", branch], check=True - ) - except subprocess.CalledProcessError: - subprocess.run( - ["git", "push", "-u", "origin", branch], check=True - ) - print(f"\nIteration {i} complete.") print(f"\nReached max iterations: {max_iterations}") From 1074e34682942ed5e6f46304ee248c9af3e1dd33 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 11:56:11 -0800 Subject: [PATCH 12/21] Add SDK features to all Ralph loop recipes - Add WorkingDirectory/working_directory to pin sessions to project root - Add OnPermissionRequest/on_permission_request for unattended operation - Add tool execution event logging for visibility - Add See Also cross-links to error-handling and persisting-sessions - Add best practices for WorkingDirectory and permission auto-approval - Consistent across all 4 languages (Node.js, Python, .NET, Go) --- cookbook/copilot-sdk/dotnet/ralph-loop.md | 22 +++++++++++++++-- .../copilot-sdk/dotnet/recipe/ralph-loop.cs | 15 ++++++++++-- cookbook/copilot-sdk/go/ralph-loop.md | 24 +++++++++++++++++-- cookbook/copilot-sdk/go/recipe/ralph-loop.go | 16 +++++++++++-- cookbook/copilot-sdk/nodejs/ralph-loop.md | 24 +++++++++++++++++-- .../nodejs/recipe/package-lock.json | 2 +- .../copilot-sdk/nodejs/recipe/ralph-loop.ts | 12 +++++++++- cookbook/copilot-sdk/python/ralph-loop.md | 23 +++++++++++++++--- .../copilot-sdk/python/recipe/ralph_loop.py | 19 ++++++++++++--- 9 files changed, 139 insertions(+), 18 deletions(-) diff --git a/cookbook/copilot-sdk/dotnet/ralph-loop.md b/cookbook/copilot-sdk/dotnet/ralph-loop.md index 0b2bb570..e8807c6d 100644 --- a/cookbook/copilot-sdk/dotnet/ralph-loop.md +++ b/cookbook/copilot-sdk/dotnet/ralph-loop.md @@ -119,13 +119,24 @@ try // Fresh session — each task gets full context budget var session = await client.CreateSessionAsync( - new SessionConfig { Model = "claude-sonnet-4.5" }); + new SessionConfig + { + Model = "claude-sonnet-4.5", + // 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(); session.On(evt => { - if (evt is AssistantMessageEvent msg) + // 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); }); @@ -224,6 +235,8 @@ dotnet build 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 @@ -238,3 +251,8 @@ dotnet build - 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 diff --git a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs index 920a78f2..ba6b2d51 100644 --- a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs +++ b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs @@ -43,14 +43,25 @@ try // Fresh session — each task gets full context budget var session = await client.CreateSessionAsync( - new SessionConfig { Model = "claude-sonnet-4.5" }); + new SessionConfig + { + Model = "claude-sonnet-4.5", + // 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(); session.On(evt => { - if (evt is AssistantMessageEvent msg) + // 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); }); diff --git a/cookbook/copilot-sdk/go/ralph-loop.md b/cookbook/copilot-sdk/go/ralph-loop.md index cc7807c4..f7a8c27c 100644 --- a/cookbook/copilot-sdk/go/ralph-loop.md +++ b/cookbook/copilot-sdk/go/ralph-loop.md @@ -111,6 +111,7 @@ import ( "log" "os" "strconv" + "strings" copilot "github.com/github/copilot-sdk/go" ) @@ -127,6 +128,8 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { } defer client.Stop() + cwd, _ := os.Getwd() + fmt.Println(strings.Repeat("━", 40)) fmt.Printf("Mode: %s\n", mode) fmt.Printf("Prompt: %s\n", promptFile) @@ -141,14 +144,24 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { for i := 1; i <= maxIterations; i++ { fmt.Printf("\n=== Iteration %d/%d ===\n", i, maxIterations) - // Fresh session — each task gets full context budget session, err := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-4.5", + 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 te, ok := event.(copilot.ToolExecutionStartEvent); ok { + fmt.Printf(" ⚙ %s\n", te.Data.ToolName) + } + }) + _, err = session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: string(prompt), }) @@ -258,6 +271,8 @@ go build ./... 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 @@ -272,3 +287,8 @@ go build ./... - 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 diff --git a/cookbook/copilot-sdk/go/recipe/ralph-loop.go b/cookbook/copilot-sdk/go/recipe/ralph-loop.go index 18de5976..30513927 100644 --- a/cookbook/copilot-sdk/go/recipe/ralph-loop.go +++ b/cookbook/copilot-sdk/go/recipe/ralph-loop.go @@ -39,6 +39,8 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { } defer client.Stop() + cwd, _ := os.Getwd() + fmt.Println(strings.Repeat("━", 40)) fmt.Printf("Mode: %s\n", mode) fmt.Printf("Prompt: %s\n", promptFile) @@ -53,14 +55,24 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { for i := 1; i <= maxIterations; i++ { fmt.Printf("\n=== Iteration %d/%d ===\n", i, maxIterations) - // Fresh session — each task gets full context budget session, err := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-4.5", + 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 te, ok := event.(copilot.ToolExecutionStartEvent); ok { + fmt.Printf(" ⚙ %s\n", te.Data.ToolName) + } + }) + _, err = session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: string(prompt), }) diff --git a/cookbook/copilot-sdk/nodejs/ralph-loop.md b/cookbook/copilot-sdk/nodejs/ralph-loop.md index 13e74fc8..5153790a 100644 --- a/cookbook/copilot-sdk/nodejs/ralph-loop.md +++ b/cookbook/copilot-sdk/nodejs/ralph-loop.md @@ -99,8 +99,21 @@ async function ralphLoop(mode: Mode, maxIterations: number = 50) { for (let i = 1; i <= maxIterations; i++) { console.log(`\n=== Iteration ${i}/${maxIterations} ===`); - // Fresh session — each task gets full context budget - const session = await client.createSession({ model: "claude-sonnet-4.5" }); + const session = await client.createSession({ + model: "claude-sonnet-4.5", + // 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 { @@ -200,6 +213,8 @@ npm run build 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 @@ -214,3 +229,8 @@ npm run build - 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 diff --git a/cookbook/copilot-sdk/nodejs/recipe/package-lock.json b/cookbook/copilot-sdk/nodejs/recipe/package-lock.json index a5a8fea5..47e85e5a 100644 --- a/cookbook/copilot-sdk/nodejs/recipe/package-lock.json +++ b/cookbook/copilot-sdk/nodejs/recipe/package-lock.json @@ -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", diff --git a/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts index 3bd5e4cf..f22e2bee 100644 --- a/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts +++ b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts @@ -39,9 +39,19 @@ async function ralphLoop(mode: Mode, maxIterations: number) { for (let i = 1; i <= maxIterations; i++) { console.log(`\n=== Iteration ${i}/${maxIterations} ===`); - // Fresh session — each task gets full context budget const session = await client.createSession({ model: "claude-sonnet-4.5", + // 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 { diff --git a/cookbook/copilot-sdk/python/ralph-loop.md b/cookbook/copilot-sdk/python/ralph-loop.md index ae666eb2..55790c27 100644 --- a/cookbook/copilot-sdk/python/ralph-loop.md +++ b/cookbook/copilot-sdk/python/ralph-loop.md @@ -108,10 +108,20 @@ async def ralph_loop(mode: str = "build", max_iterations: int = 50): for i in range(1, max_iterations + 1): print(f"\n=== Iteration {i}/{max_iterations} ===") - # Fresh session — each task gets full context budget - session = await client.create_session( - SessionConfig(model="claude-sonnet-4.5") + session = await client.create_session(SessionConfig( + model="claude-sonnet-4.5", + # 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 + session.on(lambda event: + print(f" ⚙ {event.data.tool_name}") + if event.type.value == "tool.execution_start" else None ) + try: await session.send_and_wait( MessageOptions(prompt=prompt), timeout=600 @@ -210,6 +220,8 @@ python -m pytest 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 @@ -224,3 +236,8 @@ python -m pytest - 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 diff --git a/cookbook/copilot-sdk/python/recipe/ralph_loop.py b/cookbook/copilot-sdk/python/recipe/ralph_loop.py index f6ef1f62..823981bf 100644 --- a/cookbook/copilot-sdk/python/recipe/ralph_loop.py +++ b/cookbook/copilot-sdk/python/recipe/ralph_loop.py @@ -43,10 +43,23 @@ async def ralph_loop(mode: str = "build", max_iterations: int = 50): for i in range(1, max_iterations + 1): print(f"\n=== Iteration {i}/{max_iterations} ===") - # Fresh session — each task gets full context budget - session = await client.create_session( - SessionConfig(model="claude-sonnet-4.5") + session = await client.create_session(SessionConfig( + model="claude-sonnet-4.5", + # 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 + session.on(lambda event: + print(f" ⚙ {event.data.tool_name}") + if event.type.value == "tool.execution_start" else None ) + try: await session.send_and_wait( MessageOptions(prompt=prompt), timeout=600 From 0e616701a5f8050c3ed9abca2aefaebfad84a998 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 12:50:30 -0800 Subject: [PATCH 13/21] Remove package-lock.json from tracking --- .../nodejs/recipe/package-lock.json | 629 ------------------ 1 file changed, 629 deletions(-) delete mode 100644 cookbook/copilot-sdk/nodejs/recipe/package-lock.json diff --git a/cookbook/copilot-sdk/nodejs/recipe/package-lock.json b/cookbook/copilot-sdk/nodejs/recipe/package-lock.json deleted file mode 100644 index 47e85e5a..00000000 --- a/cookbook/copilot-sdk/nodejs/recipe/package-lock.json +++ /dev/null @@ -1,629 +0,0 @@ -{ - "name": "copilot-sdk-cookbook-recipes", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "copilot-sdk-cookbook-recipes", - "version": "1.0.0", - "dependencies": { - "@github/copilot-sdk": "*" - }, - "devDependencies": { - "@types/node": "^22.19.7", - "tsx": "^4.19.2", - "typescript": "^5.7.2" - } - }, - "../..": { - "name": "@github/copilot-sdk", - "version": "0.1.8", - "license": "MIT", - "dependencies": { - "@github/copilot": "^0.0.388-1", - "vscode-jsonrpc": "^8.2.1", - "zod": "^4.3.5" - }, - "devDependencies": { - "@types/node": "^22.19.6", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", - "esbuild": "^0.27.0", - "eslint": "^9.0.0", - "glob": "^11.0.0", - "json-schema": "^0.4.0", - "json-schema-to-typescript": "^15.0.4", - "prettier": "^3.4.0", - "quicktype-core": "^23.2.6", - "rimraf": "^6.1.2", - "semver": "^7.7.3", - "tsx": "^4.20.6", - "typescript": "^5.0.0", - "vitest": "^4.0.16" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "../../..": {}, - "../../src": {}, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@github/copilot-sdk": { - "resolved": "../../src", - "link": true - }, - "node_modules/@types/node": { - "version": "22.19.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", - "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - } - } -} From 3eb7efc9907a4a66b35536ed32c26d65bc8ffbd6 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 13:31:05 -0800 Subject: [PATCH 14/21] Use gpt-5.1-codex-mini as default model in Ralph loop recipes --- cookbook/copilot-sdk/dotnet/ralph-loop.md | 4 +- .../copilot-sdk/dotnet/recipe/ralph-loop.cs | 2 +- cookbook/copilot-sdk/go/ralph-loop.md | 4 +- cookbook/copilot-sdk/go/recipe/ralph-loop.go | 2 +- cookbook/copilot-sdk/nodejs/ralph-loop.md | 4 +- .../nodejs/recipe/package-lock.json | 629 ++++++++++++++++++ .../copilot-sdk/nodejs/recipe/ralph-loop.ts | 2 +- cookbook/copilot-sdk/python/ralph-loop.md | 4 +- .../copilot-sdk/python/recipe/ralph_loop.py | 2 +- 9 files changed, 641 insertions(+), 12 deletions(-) create mode 100644 cookbook/copilot-sdk/nodejs/recipe/package-lock.json diff --git a/cookbook/copilot-sdk/dotnet/ralph-loop.md b/cookbook/copilot-sdk/dotnet/ralph-loop.md index e8807c6d..eccd2c29 100644 --- a/cookbook/copilot-sdk/dotnet/ralph-loop.md +++ b/cookbook/copilot-sdk/dotnet/ralph-loop.md @@ -58,7 +58,7 @@ try // Fresh session each iteration — context isolation is the point var session = await client.CreateSessionAsync( - new SessionConfig { Model = "claude-sonnet-4.5" }); + new SessionConfig { Model = "gpt-5.1-codex-mini" }); try { var done = new TaskCompletionSource(); @@ -121,7 +121,7 @@ try var session = await client.CreateSessionAsync( new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "gpt-5.1-codex-mini", // Pin the agent to the project directory WorkingDirectory = Environment.CurrentDirectory, // Auto-approve tool calls for unattended operation diff --git a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs index ba6b2d51..d0baa2e7 100644 --- a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs +++ b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs @@ -45,7 +45,7 @@ try var session = await client.CreateSessionAsync( new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "gpt-5.1-codex-mini", // Pin the agent to the project directory WorkingDirectory = Environment.CurrentDirectory, // Auto-approve tool calls for unattended operation diff --git a/cookbook/copilot-sdk/go/ralph-loop.md b/cookbook/copilot-sdk/go/ralph-loop.md index f7a8c27c..4d3bf373 100644 --- a/cookbook/copilot-sdk/go/ralph-loop.md +++ b/cookbook/copilot-sdk/go/ralph-loop.md @@ -70,7 +70,7 @@ func ralphLoop(ctx context.Context, promptFile string, maxIterations int) error // Fresh session each iteration — context isolation is the point session, err := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "gpt-5.1-codex-mini", }) if err != nil { return err @@ -145,7 +145,7 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { fmt.Printf("\n=== Iteration %d/%d ===\n", i, maxIterations) session, err := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "gpt-5.1-codex-mini", WorkingDirectory: cwd, OnPermissionRequest: func(_ copilot.PermissionRequest, _ map[string]string) copilot.PermissionRequestResult { return copilot.PermissionRequestResult{Kind: "approved"} diff --git a/cookbook/copilot-sdk/go/recipe/ralph-loop.go b/cookbook/copilot-sdk/go/recipe/ralph-loop.go index 30513927..72685927 100644 --- a/cookbook/copilot-sdk/go/recipe/ralph-loop.go +++ b/cookbook/copilot-sdk/go/recipe/ralph-loop.go @@ -56,7 +56,7 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { fmt.Printf("\n=== Iteration %d/%d ===\n", i, maxIterations) session, err := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "gpt-5.1-codex-mini", WorkingDirectory: cwd, OnPermissionRequest: func(_ copilot.PermissionRequest, _ map[string]string) copilot.PermissionRequestResult { return copilot.PermissionRequestResult{Kind: "approved"} diff --git a/cookbook/copilot-sdk/nodejs/ralph-loop.md b/cookbook/copilot-sdk/nodejs/ralph-loop.md index 5153790a..c4074a1b 100644 --- a/cookbook/copilot-sdk/nodejs/ralph-loop.md +++ b/cookbook/copilot-sdk/nodejs/ralph-loop.md @@ -56,7 +56,7 @@ async function ralphLoop(promptFile: string, maxIterations: number = 50) { console.log(`\n=== Iteration ${i}/${maxIterations} ===`); // Fresh session each iteration — context isolation is the point - const session = await client.createSession({ model: "claude-sonnet-4.5" }); + const session = await client.createSession({ model: "gpt-5.1-codex-mini" }); try { await session.sendAndWait({ prompt }, 600_000); } finally { @@ -100,7 +100,7 @@ async function ralphLoop(mode: Mode, maxIterations: number = 50) { console.log(`\n=== Iteration ${i}/${maxIterations} ===`); const session = await client.createSession({ - model: "claude-sonnet-4.5", + model: "gpt-5.1-codex-mini", // Pin the agent to the project directory workingDirectory: process.cwd(), // Auto-approve tool calls for unattended operation diff --git a/cookbook/copilot-sdk/nodejs/recipe/package-lock.json b/cookbook/copilot-sdk/nodejs/recipe/package-lock.json new file mode 100644 index 00000000..47e85e5a --- /dev/null +++ b/cookbook/copilot-sdk/nodejs/recipe/package-lock.json @@ -0,0 +1,629 @@ +{ + "name": "copilot-sdk-cookbook-recipes", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "copilot-sdk-cookbook-recipes", + "version": "1.0.0", + "dependencies": { + "@github/copilot-sdk": "*" + }, + "devDependencies": { + "@types/node": "^22.19.7", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } + }, + "../..": { + "name": "@github/copilot-sdk", + "version": "0.1.8", + "license": "MIT", + "dependencies": { + "@github/copilot": "^0.0.388-1", + "vscode-jsonrpc": "^8.2.1", + "zod": "^4.3.5" + }, + "devDependencies": { + "@types/node": "^22.19.6", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "esbuild": "^0.27.0", + "eslint": "^9.0.0", + "glob": "^11.0.0", + "json-schema": "^0.4.0", + "json-schema-to-typescript": "^15.0.4", + "prettier": "^3.4.0", + "quicktype-core": "^23.2.6", + "rimraf": "^6.1.2", + "semver": "^7.7.3", + "tsx": "^4.20.6", + "typescript": "^5.0.0", + "vitest": "^4.0.16" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "../../..": {}, + "../../src": {}, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@github/copilot-sdk": { + "resolved": "../../src", + "link": true + }, + "node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts index f22e2bee..fb0fbe45 100644 --- a/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts +++ b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts @@ -40,7 +40,7 @@ async function ralphLoop(mode: Mode, maxIterations: number) { console.log(`\n=== Iteration ${i}/${maxIterations} ===`); const session = await client.createSession({ - model: "claude-sonnet-4.5", + model: "gpt-5.1-codex-mini", // Pin the agent to the project directory workingDirectory: process.cwd(), // Auto-approve tool calls for unattended operation diff --git a/cookbook/copilot-sdk/python/ralph-loop.md b/cookbook/copilot-sdk/python/ralph-loop.md index 55790c27..845354bd 100644 --- a/cookbook/copilot-sdk/python/ralph-loop.md +++ b/cookbook/copilot-sdk/python/ralph-loop.md @@ -59,7 +59,7 @@ async def ralph_loop(prompt_file: str, max_iterations: int = 50): # Fresh session each iteration — context isolation is the point session = await client.create_session( - SessionConfig(model="claude-sonnet-4.5") + SessionConfig(model="gpt-5.1-codex-mini") ) try: await session.send_and_wait( @@ -109,7 +109,7 @@ async def ralph_loop(mode: str = "build", max_iterations: int = 50): print(f"\n=== Iteration {i}/{max_iterations} ===") session = await client.create_session(SessionConfig( - model="claude-sonnet-4.5", + 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 diff --git a/cookbook/copilot-sdk/python/recipe/ralph_loop.py b/cookbook/copilot-sdk/python/recipe/ralph_loop.py index 823981bf..c182e3c7 100644 --- a/cookbook/copilot-sdk/python/recipe/ralph_loop.py +++ b/cookbook/copilot-sdk/python/recipe/ralph_loop.py @@ -44,7 +44,7 @@ async def ralph_loop(mode: str = "build", max_iterations: int = 50): print(f"\n=== Iteration {i}/{max_iterations} ===") session = await client.create_session(SessionConfig( - model="claude-sonnet-4.5", + 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 From 3b4d601ba7437eb1f5510fd2fc8556454c753e26 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 13:31:15 -0800 Subject: [PATCH 15/21] Remove package-lock.json from tracking --- .../nodejs/recipe/package-lock.json | 629 ------------------ 1 file changed, 629 deletions(-) delete mode 100644 cookbook/copilot-sdk/nodejs/recipe/package-lock.json diff --git a/cookbook/copilot-sdk/nodejs/recipe/package-lock.json b/cookbook/copilot-sdk/nodejs/recipe/package-lock.json deleted file mode 100644 index 47e85e5a..00000000 --- a/cookbook/copilot-sdk/nodejs/recipe/package-lock.json +++ /dev/null @@ -1,629 +0,0 @@ -{ - "name": "copilot-sdk-cookbook-recipes", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "copilot-sdk-cookbook-recipes", - "version": "1.0.0", - "dependencies": { - "@github/copilot-sdk": "*" - }, - "devDependencies": { - "@types/node": "^22.19.7", - "tsx": "^4.19.2", - "typescript": "^5.7.2" - } - }, - "../..": { - "name": "@github/copilot-sdk", - "version": "0.1.8", - "license": "MIT", - "dependencies": { - "@github/copilot": "^0.0.388-1", - "vscode-jsonrpc": "^8.2.1", - "zod": "^4.3.5" - }, - "devDependencies": { - "@types/node": "^22.19.6", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", - "esbuild": "^0.27.0", - "eslint": "^9.0.0", - "glob": "^11.0.0", - "json-schema": "^0.4.0", - "json-schema-to-typescript": "^15.0.4", - "prettier": "^3.4.0", - "quicktype-core": "^23.2.6", - "rimraf": "^6.1.2", - "semver": "^7.7.3", - "tsx": "^4.20.6", - "typescript": "^5.0.0", - "vitest": "^4.0.16" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "../../..": {}, - "../../src": {}, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@github/copilot-sdk": { - "resolved": "../../src", - "link": true - }, - "node_modules/@types/node": { - "version": "22.19.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", - "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - } - } -} From 84486c2e46912b86c304b5c45d01df9d29120320 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 13:33:29 -0800 Subject: [PATCH 16/21] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cookbook/copilot-sdk/go/recipe/ralph-loop.go | 9 +++++++-- cookbook/copilot-sdk/nodejs/ralph-loop.md | 4 ++-- cookbook/copilot-sdk/python/ralph-loop.md | 8 ++++++-- cookbook/copilot-sdk/python/recipe/ralph_loop.py | 8 ++++---- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/cookbook/copilot-sdk/go/recipe/ralph-loop.go b/cookbook/copilot-sdk/go/recipe/ralph-loop.go index 72685927..35d862b7 100644 --- a/cookbook/copilot-sdk/go/recipe/ralph-loop.go +++ b/cookbook/copilot-sdk/go/recipe/ralph-loop.go @@ -39,7 +39,10 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { } defer client.Stop() - cwd, _ := os.Getwd() + 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) @@ -76,7 +79,9 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { _, err = session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: string(prompt), }) - session.Destroy() + 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) } diff --git a/cookbook/copilot-sdk/nodejs/ralph-loop.md b/cookbook/copilot-sdk/nodejs/ralph-loop.md index c4074a1b..1abb692c 100644 --- a/cookbook/copilot-sdk/nodejs/ralph-loop.md +++ b/cookbook/copilot-sdk/nodejs/ralph-loop.md @@ -5,8 +5,8 @@ Build autonomous coding loops where an AI agent picks tasks, implements them, va > **Runnable example:** [recipe/ralph-loop.ts](recipe/ralph-loop.ts) > > ```bash -> cd recipe && npm install -> npx tsx ralph-loop.ts +> npm install +> npx tsx recipe/ralph-loop.ts > ``` ## What is a Ralph Loop? diff --git a/cookbook/copilot-sdk/python/ralph-loop.md b/cookbook/copilot-sdk/python/ralph-loop.md index 845354bd..82bfc7f5 100644 --- a/cookbook/copilot-sdk/python/ralph-loop.md +++ b/cookbook/copilot-sdk/python/ralph-loop.md @@ -4,10 +4,14 @@ Build autonomous coding loops where an AI agent picks tasks, implements them, va > **Runnable example:** [recipe/ralph_loop.py](recipe/ralph_loop.py) > +> From the repository root, install dependencies and run: +> > ```bash -> cd recipe && pip install -r requirements.txt -> python ralph_loop.py +> 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? diff --git a/cookbook/copilot-sdk/python/recipe/ralph_loop.py b/cookbook/copilot-sdk/python/recipe/ralph_loop.py index c182e3c7..918e8c66 100644 --- a/cookbook/copilot-sdk/python/recipe/ralph_loop.py +++ b/cookbook/copilot-sdk/python/recipe/ralph_loop.py @@ -55,11 +55,11 @@ async def ralph_loop(mode: str = "build", max_iterations: int = 50): )) # Log tool usage for visibility - session.on(lambda event: - print(f" ⚙ {event.data.tool_name}") - if event.type.value == "tool.execution_start" else None - ) + 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 From 147b4f8a8408c7de184e9483f64473d81fddcdf9 Mon Sep 17 00:00:00 2001 From: Julian Date: Wed, 11 Feb 2026 16:39:43 -0500 Subject: [PATCH 17/21] Remove note section from commit prompt template Removed a note section from the conventional commit prompt template. --- prompts/conventional-commit.prompt.md | 1 - 1 file changed, 1 deletion(-) diff --git a/prompts/conventional-commit.prompt.md b/prompts/conventional-commit.prompt.md index 01cf2b5a..1bec5f69 100644 --- a/prompts/conventional-commit.prompt.md +++ b/prompts/conventional-commit.prompt.md @@ -7,7 +7,6 @@ tools: ['execute/runInTerminal', 'execute/getTerminalOutput'] ```xml 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. - ``` ### Workflow From ff69b804ac4d8a2ceb11b28eb28e82562a2d0e4d Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 14:23:25 -0800 Subject: [PATCH 18/21] renaming --- cookbook/copilot-sdk/dotnet/ralph-loop.md | 10 +- cookbook/copilot-sdk/go/ralph-loop.md | 10 +- cookbook/copilot-sdk/nodejs/ralph-loop.md | 108 +-- .../nodejs/recipe/package-lock.json | 629 ++++++++++++++++++ cookbook/copilot-sdk/python/ralph-loop.md | 23 +- 5 files changed, 710 insertions(+), 70 deletions(-) create mode 100644 cookbook/copilot-sdk/nodejs/recipe/package-lock.json diff --git a/cookbook/copilot-sdk/dotnet/ralph-loop.md b/cookbook/copilot-sdk/dotnet/ralph-loop.md index eccd2c29..8ff85246 100644 --- a/cookbook/copilot-sdk/dotnet/ralph-loop.md +++ b/cookbook/copilot-sdk/dotnet/ralph-loop.md @@ -39,7 +39,7 @@ A [Ralph loop](https://ghuntley.com/ralph/) is an autonomous development workflo ## Simple Version -The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | claude ; done`: +The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | copilot ; done`: ```csharp using GitHub.Copilot.SDK; @@ -205,9 +205,9 @@ creating ad-hoc copies. 4. When tests pass, update IMPLEMENTATION_PLAN.md, then `git add -A` then `git commit` with a descriptive message. -99999. When authoring documentation, capture the why. -999999. Implement completely. No placeholders or stubs. -9999999. Keep IMPLEMENTATION_PLAN.md current — future iterations depend on it. +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` @@ -241,12 +241,14 @@ dotnet build ## 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 diff --git a/cookbook/copilot-sdk/go/ralph-loop.md b/cookbook/copilot-sdk/go/ralph-loop.md index 4d3bf373..626ed5ea 100644 --- a/cookbook/copilot-sdk/go/ralph-loop.md +++ b/cookbook/copilot-sdk/go/ralph-loop.md @@ -39,7 +39,7 @@ A [Ralph loop](https://ghuntley.com/ralph/) is an autonomous development workflo ## Simple Version -The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | claude ; done`: +The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | copilot ; done`: ```go package main @@ -241,9 +241,9 @@ creating ad-hoc copies. 4. When tests pass, update IMPLEMENTATION_PLAN.md, then `git add -A` then `git commit` with a descriptive message. -99999. When authoring documentation, capture the why. -999999. Implement completely. No placeholders or stubs. -9999999. Keep IMPLEMENTATION_PLAN.md current — future iterations depend on it. +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` @@ -277,12 +277,14 @@ go build ./... ## 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 diff --git a/cookbook/copilot-sdk/nodejs/ralph-loop.md b/cookbook/copilot-sdk/nodejs/ralph-loop.md index 1abb692c..87c5225f 100644 --- a/cookbook/copilot-sdk/nodejs/ralph-loop.md +++ b/cookbook/copilot-sdk/nodejs/ralph-loop.md @@ -39,35 +39,35 @@ A [Ralph loop](https://ghuntley.com/ralph/) is an autonomous development workflo ## Simple Version -The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | claude ; done`: +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(); + const client = new CopilotClient(); + await client.start(); - try { - const prompt = await readFile(promptFile, "utf-8"); + try { + const prompt = await readFile(promptFile, "utf-8"); - for (let i = 1; i <= maxIterations; i++) { - console.log(`\n=== Iteration ${i}/${maxIterations} ===`); + 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(); - } + // 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(); + console.log(`Iteration ${i} complete.`); } + } finally { + await client.stop(); + } } // Usage: point at your PROMPT.md @@ -87,50 +87,50 @@ 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(); + const promptFile = mode === "plan" ? "PROMPT_plan.md" : "PROMPT_build.md"; + const client = new CopilotClient(); + await client.start(); - console.log(`Mode: ${mode} | Prompt: ${promptFile}`); + console.log(`Mode: ${mode} | Prompt: ${promptFile}`); - try { - const prompt = await readFile(promptFile, "utf-8"); + try { + const prompt = await readFile(promptFile, "utf-8"); - for (let i = 1; i <= maxIterations; i++) { - console.log(`\n=== Iteration ${i}/${maxIterations} ===`); + 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 }), - }); + 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.`); + // Log tool usage for visibility + session.on((event) => { + if (event.type === "tool.execution_start") { + console.log(` ⚙ ${event.data.toolName}`); } - } finally { - await client.stop(); + }); + + 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 maxArg = args.find((a) => /^\d+$/.test(a)); const maxIterations = maxArg ? parseInt(maxArg) : 50; ralphLoop(mode, maxIterations); @@ -182,9 +182,9 @@ creating ad-hoc copies. 4. When tests pass, update IMPLEMENTATION_PLAN.md, then `git add -A` then `git commit` with a descriptive message. -99999. When authoring documentation, capture the why. -999999. Implement completely. No placeholders or stubs. -9999999. Keep IMPLEMENTATION_PLAN.md current — future iterations depend on it. +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` @@ -219,12 +219,14 @@ npm run build ## 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 diff --git a/cookbook/copilot-sdk/nodejs/recipe/package-lock.json b/cookbook/copilot-sdk/nodejs/recipe/package-lock.json new file mode 100644 index 00000000..47e85e5a --- /dev/null +++ b/cookbook/copilot-sdk/nodejs/recipe/package-lock.json @@ -0,0 +1,629 @@ +{ + "name": "copilot-sdk-cookbook-recipes", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "copilot-sdk-cookbook-recipes", + "version": "1.0.0", + "dependencies": { + "@github/copilot-sdk": "*" + }, + "devDependencies": { + "@types/node": "^22.19.7", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } + }, + "../..": { + "name": "@github/copilot-sdk", + "version": "0.1.8", + "license": "MIT", + "dependencies": { + "@github/copilot": "^0.0.388-1", + "vscode-jsonrpc": "^8.2.1", + "zod": "^4.3.5" + }, + "devDependencies": { + "@types/node": "^22.19.6", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "esbuild": "^0.27.0", + "eslint": "^9.0.0", + "glob": "^11.0.0", + "json-schema": "^0.4.0", + "json-schema-to-typescript": "^15.0.4", + "prettier": "^3.4.0", + "quicktype-core": "^23.2.6", + "rimraf": "^6.1.2", + "semver": "^7.7.3", + "tsx": "^4.20.6", + "typescript": "^5.0.0", + "vitest": "^4.0.16" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "../../..": {}, + "../../src": {}, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@github/copilot-sdk": { + "resolved": "../../src", + "link": true + }, + "node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/cookbook/copilot-sdk/python/ralph-loop.md b/cookbook/copilot-sdk/python/ralph-loop.md index 82bfc7f5..b0d1c4b6 100644 --- a/cookbook/copilot-sdk/python/ralph-loop.md +++ b/cookbook/copilot-sdk/python/ralph-loop.md @@ -43,7 +43,7 @@ A [Ralph loop](https://ghuntley.com/ralph/) is an autonomous development workflo ## Simple Version -The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | claude ; done`: +The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | copilot ; done`: ```python import asyncio @@ -117,14 +117,17 @@ async def ralph_loop(mode: str = "build", max_iterations: int = 50): # 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": []}, + on_permission_request=lambda _req, _ctx: { + "kind": "approved", "rules": [] + }, )) # Log tool usage for visibility - session.on(lambda event: - print(f" ⚙ {event.data.tool_name}") - if event.type.value == "tool.execution_start" else None - ) + 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( @@ -193,9 +196,9 @@ creating ad-hoc copies. 4. When tests pass, update IMPLEMENTATION_PLAN.md, then `git add -A` then `git commit` with a descriptive message. -99999. When authoring documentation, capture the why. -999999. Implement completely. No placeholders or stubs. -9999999. Keep IMPLEMENTATION_PLAN.md current — future iterations depend on it. +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` @@ -230,12 +233,14 @@ python -m pytest ## 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 From 717c0121bc0e6e9c8328338a41479d9125e20d85 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 15:16:44 -0800 Subject: [PATCH 19/21] PR feedback --- cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs | 1 - cookbook/copilot-sdk/go/ralph-loop.md | 4 ++-- cookbook/copilot-sdk/go/recipe/ralph-loop.go | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs index d0baa2e7..9f153324 100644 --- a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs +++ b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs @@ -1,5 +1,4 @@ #:package GitHub.Copilot.SDK@* -#:property PublishAot=false using GitHub.Copilot.SDK; diff --git a/cookbook/copilot-sdk/go/ralph-loop.md b/cookbook/copilot-sdk/go/ralph-loop.md index 626ed5ea..f8462c3d 100644 --- a/cookbook/copilot-sdk/go/ralph-loop.md +++ b/cookbook/copilot-sdk/go/ralph-loop.md @@ -157,8 +157,8 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { // Log tool usage for visibility session.On(func(event copilot.Event) { - if te, ok := event.(copilot.ToolExecutionStartEvent); ok { - fmt.Printf(" ⚙ %s\n", te.Data.ToolName) + if toolExecution, ok := event.(copilot.ToolExecutionStartEvent); ok { + fmt.Printf(" ⚙ %s\n", toolExecution.Data.ToolName) } }) diff --git a/cookbook/copilot-sdk/go/recipe/ralph-loop.go b/cookbook/copilot-sdk/go/recipe/ralph-loop.go index 35d862b7..03d99987 100644 --- a/cookbook/copilot-sdk/go/recipe/ralph-loop.go +++ b/cookbook/copilot-sdk/go/recipe/ralph-loop.go @@ -71,8 +71,8 @@ func ralphLoop(ctx context.Context, mode string, maxIterations int) error { // Log tool usage for visibility session.On(func(event copilot.Event) { - if te, ok := event.(copilot.ToolExecutionStartEvent); ok { - fmt.Printf(" ⚙ %s\n", te.Data.ToolName) + if toolExecution, ok := event.(copilot.ToolExecutionStartEvent); ok { + fmt.Printf(" ⚙ %s\n", toolExecution.Data.ToolName) } }) From 2a06b99b9b93963b49eb2ca71b9bd9e1758aed7c Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 12 Feb 2026 11:25:49 +1100 Subject: [PATCH 20/21] Add accessibility report cookbook recipe Add a new cookbook recipe that generates WCAG accessibility reports using the Playwright MCP server. Includes streaming output, structured report formatting, and optional Playwright test generation. - C#, TypeScript, Python, and Go implementations - Markdown documentation for each language - Updated cookbook.yml, copilot-sdk README, and package.json --- cookbook/cookbook.yml | 8 + cookbook/copilot-sdk/README.md | 6 +- .../dotnet/accessibility-report.md | 287 +++++++++++++++++ .../dotnet/recipe/accessibility-report.cs | 184 +++++++++++ .../copilot-sdk/go/accessibility-report.md | 291 ++++++++++++++++++ .../go/recipe/accessibility-report.go | 213 +++++++++++++ .../nodejs/accessibility-report.md | 265 ++++++++++++++++ .../nodejs/recipe/accessibility-report.ts | 187 +++++++++++ .../copilot-sdk/nodejs/recipe/package.json | 3 +- .../python/accessibility-report.md | 253 +++++++++++++++ .../python/recipe/accessibility_report.py | 171 ++++++++++ 11 files changed, 1866 insertions(+), 2 deletions(-) create mode 100644 cookbook/copilot-sdk/dotnet/accessibility-report.md create mode 100644 cookbook/copilot-sdk/dotnet/recipe/accessibility-report.cs create mode 100644 cookbook/copilot-sdk/go/accessibility-report.md create mode 100644 cookbook/copilot-sdk/go/recipe/accessibility-report.go create mode 100644 cookbook/copilot-sdk/nodejs/accessibility-report.md create mode 100644 cookbook/copilot-sdk/nodejs/recipe/accessibility-report.ts create mode 100644 cookbook/copilot-sdk/python/accessibility-report.md create mode 100644 cookbook/copilot-sdk/python/recipe/accessibility_report.py diff --git a/cookbook/cookbook.yml b/cookbook/cookbook.yml index d80454b5..43f66f3f 100644 --- a/cookbook/cookbook.yml +++ b/cookbook/cookbook.yml @@ -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 diff --git a/cookbook/copilot-sdk/README.md b/cookbook/copilot-sdk/README.md index 55981302..3e2738d1 100644 --- a/cookbook/copilot-sdk/README.md +++ b/cookbook/copilot-sdk/README.md @@ -12,6 +12,7 @@ This cookbook collects small, focused recipes showing how to accomplish common t - [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 @@ -21,6 +22,7 @@ This cookbook collects small, focused recipes showing how to accomplish common t - [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 @@ -30,6 +32,7 @@ This cookbook collects small, focused recipes showing how to accomplish common t - [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 @@ -39,6 +42,7 @@ This cookbook collects small, focused recipes showing how to accomplish common t - [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 @@ -87,4 +91,4 @@ go run .go ## Status -Cookbook structure is complete with 6 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. diff --git a/cookbook/copilot-sdk/dotnet/accessibility-report.md b/cookbook/copilot-sdk/dotnet/accessibility-report.md new file mode 100644 index 00000000..6c2f1d8d --- /dev/null +++ b/cookbook/copilot-sdk/dotnet/accessibility-report.md @@ -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() + { + ["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
landmark | 1.3.1, 2.4.1 | Wrap main content in
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() +{ + ["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 === +``` diff --git a/cookbook/copilot-sdk/dotnet/recipe/accessibility-report.cs b/cookbook/copilot-sdk/dotnet/recipe/accessibility-report.cs new file mode 100644 index 00000000..3fe4e387 --- /dev/null +++ b/cookbook/copilot-sdk/dotnet/recipe/accessibility-report.cs @@ -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() + { + ["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
landmark | 1.3.1, 2.4.1 | Wrap main content in
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
landmark - Wrap page content in
for screen reader navigation + - Add skip link - Hidden link at start: + - 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 ==="); +} diff --git a/cookbook/copilot-sdk/go/accessibility-report.md b/cookbook/copilot-sdk/go/accessibility-report.md new file mode 100644 index 00000000..afe7ea27 --- /dev/null +++ b/cookbook/copilot-sdk/go/accessibility-report.md @@ -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 === +``` diff --git a/cookbook/copilot-sdk/go/recipe/accessibility-report.go b/cookbook/copilot-sdk/go/recipe/accessibility-report.go new file mode 100644 index 00000000..e1ae2a49 --- /dev/null +++ b/cookbook/copilot-sdk/go/recipe/accessibility-report.go @@ -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
landmark | 1.3.1, 2.4.1 | Wrap main content in
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
landmark - Wrap page content in
for screen reader navigation + - Add skip link - Hidden link at start: + - 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 ===") + } +} diff --git a/cookbook/copilot-sdk/nodejs/accessibility-report.md b/cookbook/copilot-sdk/nodejs/accessibility-report.md new file mode 100644 index 00000000..74cb7747 --- /dev/null +++ b/cookbook/copilot-sdk/nodejs/accessibility-report.md @@ -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 => + 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 => + 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 === +``` diff --git a/cookbook/copilot-sdk/nodejs/recipe/accessibility-report.ts b/cookbook/copilot-sdk/nodejs/recipe/accessibility-report.ts new file mode 100644 index 00000000..a096726e --- /dev/null +++ b/cookbook/copilot-sdk/nodejs/recipe/accessibility-report.ts @@ -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 => + 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 => + 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
landmark | 1.3.1, 2.4.1 | Wrap main content in
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
landmark - Wrap page content in
for screen reader navigation + - Add skip link - Hidden link at start: + - 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); diff --git a/cookbook/copilot-sdk/nodejs/recipe/package.json b/cookbook/copilot-sdk/nodejs/recipe/package.json index 53584216..c8ee65a2 100644 --- a/cookbook/copilot-sdk/nodejs/recipe/package.json +++ b/cookbook/copilot-sdk/nodejs/recipe/package.json @@ -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": "*" diff --git a/cookbook/copilot-sdk/python/accessibility-report.md b/cookbook/copilot-sdk/python/accessibility-report.md new file mode 100644 index 00000000..3d67a5fa --- /dev/null +++ b/cookbook/copilot-sdk/python/accessibility-report.md @@ -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 === +``` diff --git a/cookbook/copilot-sdk/python/recipe/accessibility_report.py b/cookbook/copilot-sdk/python/recipe/accessibility_report.py new file mode 100644 index 00000000..c5e0b6c9 --- /dev/null +++ b/cookbook/copilot-sdk/python/recipe/accessibility_report.py @@ -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
landmark | 1.3.1, 2.4.1 | Wrap main content in
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
landmark - Wrap page content in
for screen reader navigation + - Add skip link - Hidden link at start: + - 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()) From 2c5d25179235c9120a355128cadf568a50b62334 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 12 Feb 2026 14:23:47 +1100 Subject: [PATCH 21/21] Fix modal close button on samples page The samples page never called setupModal(), so the close button, Escape key, and backdrop click handlers were never registered. Add the missing setupModal() call matching all other pages. --- website/src/scripts/pages/samples.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/src/scripts/pages/samples.ts b/website/src/scripts/pages/samples.ts index 8a896284..cdc2e7d3 100644 --- a/website/src/scripts/pages/samples.ts +++ b/website/src/scripts/pages/samples.ts @@ -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 { search = new FuzzySearch(allRecipes); // Setup UI + setupModal(); setupFilters(); setupSearch(); renderCookbooks();