diff --git a/docs/SYMPHONY_ISSUES.md b/docs/SYMPHONY_ISSUES.md new file mode 100644 index 00000000..01660cd7 --- /dev/null +++ b/docs/SYMPHONY_ISSUES.md @@ -0,0 +1,176 @@ +# Creating Symphony Issues + +Maintainers create GitHub Issues to define contribution opportunities for the Symphony community. + +## Overview + +Symphony issues are standard GitHub issues with the `runmaestro.ai` label. The issue body contains paths to Auto Run documents that define the work to be done. When a contributor starts working on an issue, a draft PR is automatically created to claim it. + +## Issue Requirements + +1. **Label**: Add the `runmaestro.ai` label to the issue +2. **Title**: Clear description of the contribution (e.g., "Add unit tests for user module") +3. **Body**: List the Auto Run document paths (one per line) + +## Issue Body Format + +Simply list the paths to your Auto Run documents: + +``` +.maestro/autorun/add-user-tests.md +.maestro/autorun/add-user-tests-2.md +``` + +That's it! No special formatting required. The system will: +- Parse the `.md` file paths from the issue body +- Clone your repository when a contributor starts +- Run each document in sequence via Auto Run +- Create a PR with all changes + +### Supported Path Formats + +The following formats are recognized: + +```markdown +# Bare paths (recommended) +.maestro/autorun/task-1.md +.maestro/autorun/task-2.md + +# Markdown list items +- .maestro/autorun/task-1.md +- `.maestro/autorun/task-2.md` + +# Numbered lists +1. .maestro/autorun/task-1.md +2. .maestro/autorun/task-2.md +``` + +## Example Issue + +**Title**: Add comprehensive tests for the authentication module + +**Labels**: `runmaestro.ai` + +**Body**: +```markdown +Add test coverage for the authentication module. + +Documents to process: +.maestro/autorun/auth-unit-tests.md +.maestro/autorun/auth-integration-tests.md +.maestro/autorun/auth-e2e-tests.md + +## Context + +The `src/auth/` module currently has low test coverage. These documents will guide the AI to add comprehensive tests following our existing patterns. + +## Expected Outcome + +- Unit tests for all public functions +- Integration tests for auth flow +- E2E tests for login/logout + +Estimated time: ~45 minutes of AI agent time. +``` + +## Auto Run Document Format + +Each `.md` file should be a complete Auto Run document: + +```markdown +# Task: Add Unit Tests for Auth Module + +## Context +The authentication module at `src/auth/` needs test coverage. + +## Objectives +- [ ] Create `src/__tests__/auth.test.ts` +- [ ] Add tests for `login()` function +- [ ] Add tests for `logout()` function +- [ ] Add tests for `refreshToken()` function +- [ ] Ensure `npm test` passes +- [ ] Verify coverage > 80% + +## Constraints +- Use Jest testing framework +- Follow existing test patterns in the codebase +- Do not modify production code +``` + +### Document Best Practices + +1. **Small, focused tasks**: Each document should be ~30-60 minutes of AI time +2. **Clear objectives**: Use checkboxes (`- [ ]`) for verification steps +3. **Provide context**: Include file paths, existing patterns, constraints +4. **Verification steps**: Include test commands, linting checks +5. **Independence**: Each document should be self-contained + +## Issue Availability + +An issue is **available** for contribution when: +- It has the `runmaestro.ai` label +- It is **open** (not closed) +- There is **no open PR** with "Closes #N" in the body + +When a contributor starts working on an issue, a draft PR is immediately created with "Closes #N" in the body. This claims the issue and prevents duplicate work. + +### Claim Flow + +``` +1. Contributor clicks "Start Symphony" on an issue +2. Repository is cloned locally +3. A new branch is created (symphony/issue-{number}-{timestamp}) +4. An empty commit is made +5. The branch is pushed to origin +6. A draft PR is created with "Closes #{issue}" in the body +7. Auto Run begins processing documents +8. When complete, contributor clicks "Finalize PR" +9. Draft PR is converted to "Ready for Review" +``` + +## Creating Good Issues + +### Do + +- ✅ Break large tasks into multiple smaller issues +- ✅ Include all necessary context in the documents +- ✅ Provide clear acceptance criteria +- ✅ Estimate the expected time/complexity +- ✅ Link to relevant documentation or examples + +### Don't + +- ❌ Create issues that require human judgment calls +- ❌ Include tasks that need external credentials/access +- ❌ Bundle unrelated tasks in a single issue +- ❌ Assume contributors know your codebase intimately +- ❌ Create documents with ambiguous requirements + +## Example Document Structure + +For complex tasks, organize your documents like this: + +``` +.maestro/autorun/ +├── feature-1-setup.md # First: Set up files/structure +├── feature-1-implement.md # Second: Implement the feature +├── feature-1-tests.md # Third: Add tests +└── feature-1-docs.md # Fourth: Update documentation +``` + +Each document builds on the previous one, and contributors can see the full scope in the issue body. + +## Monitoring Contributions + +As a maintainer: + +1. You'll receive a GitHub notification when a draft PR is created +2. Watch the PR for progress as the contributor works +3. Review and provide feedback once the PR is ready +4. Merge when satisfied + +## Questions? + +- See [SYMPHONY_REGISTRY.md](SYMPHONY_REGISTRY.md) for registry information +- Check the [Maestro documentation](https://docs.runmaestro.ai) for Auto Run guides +- Open an issue on the Maestro repository for support diff --git a/docs/SYMPHONY_REGISTRY.md b/docs/SYMPHONY_REGISTRY.md new file mode 100644 index 00000000..d78aec07 --- /dev/null +++ b/docs/SYMPHONY_REGISTRY.md @@ -0,0 +1,158 @@ +# Maestro Symphony Registry + +The central registry for open source projects participating in Symphony. + +## Overview + +Symphony connects open source maintainers with AI-powered contributors. Maintainers register their repositories, create Auto Run documents, and open GitHub Issues with the `runmaestro.ai` label. Contributors browse available tasks and complete them via Maestro's Auto Run feature. + +## Repository Structure + +The registry lives in the main Maestro repository: + +``` +pedramamini/Maestro/ +├── symphony-registry.json # Central list of all projects +└── docs/ + └── SYMPHONY_REGISTRY.md # This documentation +``` + +## symphony-registry.json Schema + +```json +{ + "schemaVersion": "1.0", + "lastUpdated": "2025-01-01T00:00:00Z", + "repositories": [ + { + "slug": "owner/repo-name", + "name": "Human Readable Name", + "description": "Short description of the project", + "url": "https://github.com/owner/repo-name", + "category": "developer-tools", + "tags": ["cli", "productivity"], + "maintainer": { + "name": "Name", + "url": "https://..." + }, + "isActive": true, + "featured": false, + "addedAt": "2025-01-01" + } + ] +} +``` + +### Field Reference + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `slug` | string | Yes | Repository identifier in `owner/repo` format | +| `name` | string | Yes | Human-readable project name | +| `description` | string | Yes | Short description (max 200 chars) | +| `url` | string | Yes | Full GitHub repository URL | +| `category` | string | Yes | Primary category (see Categories below) | +| `tags` | string[] | No | Optional tags for search/filtering | +| `maintainer.name` | string | Yes | Maintainer or organization name | +| `maintainer.url` | string | No | Optional link to maintainer profile | +| `isActive` | boolean | Yes | Whether repo is accepting contributions | +| `featured` | boolean | No | Show in featured section (default: false) | +| `addedAt` | string | Yes | ISO 8601 date when registered | + +## How It Works + +1. **Maintainers register once** by submitting a PR to add their repo to `symphony-registry.json` +2. **Maintainers create Auto Run documents** in their repository (e.g., `.maestro/autorun/`) +3. **Maintainers open GitHub Issues** with the `runmaestro.ai` label, listing document paths +4. **Contributors browse** available issues in Maestro Symphony +5. **One-click contribution** clones the repo, creates a draft PR (claiming the issue), and runs Auto Run +6. **Finalize PR** when all documents are processed +7. **Maintainer reviews** and merges the contribution + +## Categories + +| ID | Label | Use Case | +|----|-------|----------| +| `ai-ml` | AI & ML | AI/ML tools and libraries | +| `developer-tools` | Developer Tools | Developer productivity tools | +| `infrastructure` | Infrastructure | DevOps, cloud, infrastructure | +| `documentation` | Documentation | Documentation projects | +| `web` | Web | Web frameworks and libraries | +| `mobile` | Mobile | Mobile development | +| `data` | Data | Data processing, databases | +| `security` | Security | Security tools | +| `other` | Other | Miscellaneous projects | + +## Registering a Repository + +### Prerequisites + +Before registering, ensure your repository: + +- Has a clear README explaining the project +- Has contribution guidelines (CONTRIBUTING.md) +- Uses a license compatible with open source (MIT, Apache 2.0, etc.) +- Has at least one Auto Run document ready + +### Registration Steps + +1. **Fork** the `pedramamini/Maestro` repository +2. **Add your entry** to `symphony-registry.json`: + +```json +{ + "slug": "your-org/your-repo", + "name": "Your Project Name", + "description": "Brief description of your project", + "url": "https://github.com/your-org/your-repo", + "category": "developer-tools", + "tags": ["typescript", "cli"], + "maintainer": { + "name": "Your Name", + "url": "https://github.com/your-username" + }, + "isActive": true, + "featured": false, + "addedAt": "2025-01-15" +} +``` + +3. **Submit a PR** with your repository details +4. Once merged, **create issues** with the `runmaestro.ai` label to enable contributions + +### After Registration + +Once your repository is in the registry: + +1. Create a `.maestro/autorun/` directory in your repo (optional, but recommended) +2. Write Auto Run documents for contribution tasks +3. Open GitHub Issues with the `runmaestro.ai` label +4. List the document paths in the issue body + +See [SYMPHONY_ISSUES.md](SYMPHONY_ISSUES.md) for detailed issue formatting guidelines. + +## Updating Your Entry + +To update your registry entry (e.g., change category, update description): + +1. Submit a PR modifying your entry in `symphony-registry.json` +2. Keep your `slug` unchanged to maintain history + +## Removing Your Repository + +To remove your repository from Symphony: + +1. Set `isActive: false` in your registry entry, OR +2. Submit a PR removing your entry entirely + +Note: Setting `isActive: false` hides your repo from the contributor UI but preserves contribution history. + +## Registry Caching + +The Symphony client caches the registry for 2 hours to reduce API calls. Changes to the registry may take up to 2 hours to propagate to all users. + +## Questions? + +- See [SYMPHONY_ISSUES.md](SYMPHONY_ISSUES.md) for issue formatting +- Check the [Maestro documentation](https://docs.runmaestro.ai) for Auto Run guides +- Open an issue on the Maestro repository for support diff --git a/src/main/services/symphony-runner.ts b/src/main/services/symphony-runner.ts new file mode 100644 index 00000000..1ebaa39c --- /dev/null +++ b/src/main/services/symphony-runner.ts @@ -0,0 +1,290 @@ +/** + * Symphony Runner Service + * + * Orchestrates contributions using Auto Run with draft PR claiming. + */ + +import path from 'path'; +import { logger } from '../utils/logger'; +import { execFileNoThrow } from '../utils/execFile'; +// Types imported for documentation and future use +// import type { ActiveContribution, SymphonyIssue } from '../../shared/symphony-types'; + +const LOG_CONTEXT = '[SymphonyRunner]'; + +export interface SymphonyRunnerOptions { + contributionId: string; + repoSlug: string; + repoUrl: string; + issueNumber: number; + issueTitle: string; + documentPaths: string[]; + localPath: string; + branchName: string; + onProgress?: (progress: { completedDocuments: number; totalDocuments: number }) => void; + onStatusChange?: (status: string) => void; +} + +/** + * Clone repository to local path (shallow clone for speed). + */ +async function cloneRepo(repoUrl: string, localPath: string): Promise { + logger.info('Cloning repository', LOG_CONTEXT, { repoUrl, localPath }); + const result = await execFileNoThrow('git', ['clone', '--depth=1', repoUrl, localPath]); + return result.exitCode === 0; +} + +/** + * Create and checkout a new branch. + */ +async function createBranch(localPath: string, branchName: string): Promise { + const result = await execFileNoThrow('git', ['checkout', '-b', branchName], localPath); + return result.exitCode === 0; +} + +/** + * Create an empty commit to enable pushing without changes. + */ +async function createEmptyCommit(localPath: string, message: string): Promise { + const result = await execFileNoThrow('git', ['commit', '--allow-empty', '-m', message], localPath); + return result.exitCode === 0; +} + +/** + * Push branch to origin. + */ +async function pushBranch(localPath: string, branchName: string): Promise { + const result = await execFileNoThrow('git', ['push', '-u', 'origin', branchName], localPath); + return result.exitCode === 0; +} + +/** + * Create a draft PR using GitHub CLI. + */ +async function createDraftPR( + localPath: string, + issueNumber: number, + issueTitle: string +): Promise<{ success: boolean; prUrl?: string; prNumber?: number; error?: string }> { + const title = `[WIP] Symphony: ${issueTitle}`; + const body = `## Symphony Contribution + +This draft PR was created via Maestro Symphony. + +Closes #${issueNumber} + +--- + +*Work in progress - will be updated when Auto Run completes*`; + + const result = await execFileNoThrow( + 'gh', + ['pr', 'create', '--draft', '--title', title, '--body', body], + localPath + ); + + if (result.exitCode !== 0) { + return { success: false, error: `PR creation failed: ${result.stderr}` }; + } + + const prUrl = result.stdout.trim(); + const prNumberMatch = prUrl.match(/\/pull\/(\d+)/); + + return { + success: true, + prUrl, + prNumber: prNumberMatch ? parseInt(prNumberMatch[1], 10) : undefined, + }; +} + +/** + * Copy Auto Run documents from repo to local Auto Run Docs folder. + */ +async function setupAutoRunDocs( + localPath: string, + documentPaths: string[] +): Promise { + const autoRunPath = path.join(localPath, 'Auto Run Docs'); + await execFileNoThrow('mkdir', ['-p', autoRunPath]); + + for (const docPath of documentPaths) { + const sourcePath = path.join(localPath, docPath); + const destPath = path.join(autoRunPath, path.basename(docPath)); + await execFileNoThrow('cp', [sourcePath, destPath]); + } + + return autoRunPath; +} + +/** + * Start a Symphony contribution. + * + * Flow: + * 1. Clone the repository (shallow) + * 2. Create a new branch + * 3. Create an empty commit + * 4. Push the branch + * 5. Create a draft PR (claims the issue via "Closes #N") + * 6. Set up Auto Run documents + */ +export async function startContribution(options: SymphonyRunnerOptions): Promise<{ + success: boolean; + draftPrUrl?: string; + draftPrNumber?: number; + autoRunPath?: string; + error?: string; +}> { + const { + repoUrl, + localPath, + branchName, + issueNumber, + issueTitle, + documentPaths, + onStatusChange, + } = options; + + try { + // 1. Clone + onStatusChange?.('cloning'); + if (!await cloneRepo(repoUrl, localPath)) { + return { success: false, error: 'Clone failed' }; + } + + // 2. Create branch + onStatusChange?.('setting_up'); + if (!await createBranch(localPath, branchName)) { + return { success: false, error: 'Branch creation failed' }; + } + + // 3. Empty commit + const commitMessage = `[Symphony] Start contribution for #${issueNumber}`; + if (!await createEmptyCommit(localPath, commitMessage)) { + return { success: false, error: 'Empty commit failed' }; + } + + // 4. Push branch + if (!await pushBranch(localPath, branchName)) { + return { success: false, error: 'Push failed' }; + } + + // 5. Create draft PR + const prResult = await createDraftPR(localPath, issueNumber, issueTitle); + if (!prResult.success) { + return { success: false, error: prResult.error }; + } + + // 6. Setup Auto Run docs + const autoRunPath = await setupAutoRunDocs(localPath, documentPaths); + + // Ready - actual Auto Run processing happens via session + onStatusChange?.('running'); + + return { + success: true, + draftPrUrl: prResult.prUrl, + draftPrNumber: prResult.prNumber, + autoRunPath, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } +} + +/** + * Finalize a contribution by converting draft PR to ready for review. + */ +export async function finalizeContribution( + localPath: string, + prNumber: number, + issueNumber: number, + issueTitle: string +): Promise<{ success: boolean; prUrl?: string; error?: string }> { + // Commit all changes + await execFileNoThrow('git', ['add', '-A'], localPath); + + const commitMessage = `[Symphony] Complete contribution for #${issueNumber} + +Processed all Auto Run documents for: ${issueTitle}`; + + const commitResult = await execFileNoThrow('git', ['commit', '-m', commitMessage], localPath); + if (commitResult.exitCode !== 0 && !commitResult.stderr.includes('nothing to commit')) { + return { success: false, error: `Commit failed: ${commitResult.stderr}` }; + } + + // Push changes + const pushResult = await execFileNoThrow('git', ['push'], localPath); + if (pushResult.exitCode !== 0) { + return { success: false, error: `Push failed: ${pushResult.stderr}` }; + } + + // Convert draft to ready for review + const readyResult = await execFileNoThrow( + 'gh', + ['pr', 'ready', prNumber.toString()], + localPath + ); + if (readyResult.exitCode !== 0) { + return { success: false, error: `Failed to mark PR ready: ${readyResult.stderr}` }; + } + + // Update PR body with completion summary + const body = `## Symphony Contribution + +This PR was created via Maestro Symphony. + +Closes #${issueNumber} + +--- + +**Task:** ${issueTitle} + +*Contributed by the Maestro Symphony community* 🎵`; + + await execFileNoThrow( + 'gh', + ['pr', 'edit', prNumber.toString(), '--body', body], + localPath + ); + + // Get final PR URL + const prInfoResult = await execFileNoThrow( + 'gh', + ['pr', 'view', prNumber.toString(), '--json', 'url', '-q', '.url'], + localPath + ); + + return { + success: true, + prUrl: prInfoResult.stdout.trim(), + }; +} + +/** + * Cancel a contribution by closing the draft PR and cleaning up. + */ +export async function cancelContribution( + localPath: string, + prNumber: number, + cleanup: boolean = true +): Promise<{ success: boolean; error?: string }> { + // Close the draft PR + const closeResult = await execFileNoThrow( + 'gh', + ['pr', 'close', prNumber.toString(), '--delete-branch'], + localPath + ); + if (closeResult.exitCode !== 0) { + logger.warn('Failed to close PR', LOG_CONTEXT, { prNumber, error: closeResult.stderr }); + } + + // Clean up local directory + if (cleanup) { + await execFileNoThrow('rm', ['-rf', localPath]); + } + + return { success: true }; +}