Monorepo for Aesthetic.Computer
aesthetic.computer
1// patch.mjs - Admin-only endpoint for spawning GitHub Copilot coding agents
2// 2026.02.01
3//
4// POST /api/patch - Spawn a GitHub Copilot coding agent with the given prompt
5// Requires authentication and admin privileges (@jeffrey only)
6//
7// Request body:
8// {
9// prompt: string, // The task/instruction for the PR agent
10// branch?: string, // Optional: target branch (defaults to main)
11// title?: string // Optional: PR title prefix
12// }
13//
14// Response:
15// {
16// success: boolean,
17// message: string,
18// data?: {
19// jobId: string, // ID for tracking the agent job
20// status: string, // "queued" | "running" | "completed" | "failed"
21// }
22// }
23
24import { authorize, handleFor, hasAdmin } from "../../backend/authorization.mjs";
25import { respond } from "../../backend/http.mjs";
26import { shell } from "../../backend/shell.mjs";
27
28const dev = process.env.CONTEXT === "dev";
29
30// GitHub Copilot Coding Agent configuration
31const GITHUB_TOKEN = process.env.GITHUB_COPILOT_TOKEN;
32const REPO_OWNER = "whistlegraph";
33const REPO_NAME = "aesthetic-computer";
34
35export async function handler(event, context) {
36 // Handle CORS preflight
37 if (event.httpMethod === "OPTIONS") {
38 return respond(200, {}, {
39 "Access-Control-Allow-Origin": "*",
40 "Access-Control-Allow-Methods": "POST, OPTIONS",
41 "Access-Control-Allow-Headers": "Content-Type, Authorization",
42 });
43 }
44
45 if (event.httpMethod !== "POST") {
46 return respond(405, { success: false, message: "Method not allowed" });
47 }
48
49 try {
50 // 1. Authenticate the user
51 const user = await authorize(event.headers);
52
53 if (!user) {
54 shell.log("🔐 Patch: No user found");
55 return respond(401, { success: false, message: "Authentication required" });
56 }
57
58 if (!user.email_verified) {
59 shell.log("🔐 Patch: Email not verified");
60 return respond(401, { success: false, message: "Email verification required" });
61 }
62
63 // 2. Check admin privileges (only @jeffrey can use this)
64 const isAdmin = await hasAdmin(user);
65 const handle = await handleFor(user.sub);
66
67 shell.log(`🔐 Patch: User @${handle}, admin: ${isAdmin}`);
68
69 if (!isAdmin) {
70 shell.log(`🚫 Patch: Unauthorized access attempt by @${handle}`);
71 return respond(403, {
72 success: false,
73 message: "Admin privileges required. Only @jeffrey can use this command."
74 });
75 }
76
77 // 3. Parse the request body
78 let body;
79 try {
80 body = JSON.parse(event.body);
81 } catch (e) {
82 return respond(400, { success: false, message: "Invalid JSON body" });
83 }
84
85 const { prompt, branch = "main", title } = body;
86
87 if (!prompt || typeof prompt !== "string" || prompt.trim().length === 0) {
88 return respond(400, {
89 success: false,
90 message: "A prompt/instruction is required for the PR agent"
91 });
92 }
93
94 // 4. Check for GitHub token
95 if (!GITHUB_TOKEN) {
96 shell.error("🔴 Patch: GITHUB_COPILOT_TOKEN not configured");
97 return respond(503, {
98 success: false,
99 message: "GitHub Copilot integration not configured. Please set GITHUB_COPILOT_TOKEN."
100 });
101 }
102
103 // 5. Spawn the GitHub Copilot PR agent
104 shell.log(`🤖 Patch: Spawning PR agent for @${handle}`);
105 shell.log(`📝 Prompt: ${prompt.substring(0, 100)}...`);
106
107 const agentResult = await spawnCopilotAgent({
108 prompt: prompt.trim(),
109 branch,
110 title: title || `[patch] ${prompt.substring(0, 50)}...`,
111 handle,
112 });
113
114 if (agentResult.success) {
115 shell.log(`✅ Patch: Agent spawned successfully, job: ${agentResult.jobId}`);
116 return respond(200, {
117 success: true,
118 message: "PR agent spawned successfully",
119 data: {
120 jobId: agentResult.jobId,
121 status: agentResult.status,
122 url: agentResult.url,
123 },
124 });
125 } else {
126 shell.error(`🔴 Patch: Agent spawn failed: ${agentResult.error}`);
127 return respond(500, {
128 success: false,
129 message: agentResult.error || "Failed to spawn PR agent",
130 });
131 }
132
133 } catch (error) {
134 shell.error(`🔴 Patch error: ${error.message}`);
135 return respond(500, {
136 success: false,
137 message: dev ? error.message : "Internal server error"
138 });
139 }
140}
141
142/**
143 * Spawn a GitHub Copilot coding agent to create a PR
144 * Uses the GitHub API to trigger Copilot workspace/coding agent
145 */
146async function spawnCopilotAgent({ prompt, branch, title, handle }) {
147 try {
148 const { Octokit } = await import("@octokit/rest");
149
150 const octokit = new Octokit({
151 auth: GITHUB_TOKEN,
152 });
153
154 // Create an issue that Copilot can work on
155 // GitHub Copilot Coding Agent can be triggered by creating issues
156 // with specific labels or via the GitHub API
157
158 const issueBody = `## Task Description
159
160${prompt}
161
162---
163*Triggered by @${handle} via aesthetic.computer/patch command*
164*Target branch: ${branch}*
165
166## Instructions for Copilot Agent
167
168Please implement this change and create a pull request. Follow existing code patterns and conventions in the repository.
169
170---
171🤖 **This issue was auto-generated by the AC patch system**
172`;
173
174 // First, try to create the issue
175 const issueResponse = await octokit.issues.create({
176 owner: REPO_OWNER,
177 repo: REPO_NAME,
178 title: title,
179 body: issueBody,
180 labels: ["copilot", "auto-patch"],
181 });
182
183 const issueNumber = issueResponse.data.number;
184 const issueUrl = issueResponse.data.html_url;
185
186 shell.log(`📋 Created issue #${issueNumber}: ${issueUrl}`);
187
188 // Trigger Copilot coding agent on this issue
189 // This uses the repository dispatch event which Copilot can respond to
190 try {
191 await octokit.repos.createDispatchEvent({
192 owner: REPO_OWNER,
193 repo: REPO_NAME,
194 event_type: "copilot-patch",
195 client_payload: {
196 issue_number: issueNumber,
197 prompt: prompt,
198 branch: branch,
199 triggered_by: handle,
200 },
201 });
202 shell.log(`🚀 Dispatched copilot-patch event for issue #${issueNumber}`);
203 } catch (dispatchError) {
204 // Dispatch might fail if not set up, but issue creation is still useful
205 shell.log(`⚠️ Could not dispatch event (may need workflow setup): ${dispatchError.message}`);
206 }
207
208 return {
209 success: true,
210 jobId: `issue-${issueNumber}`,
211 status: "queued",
212 url: issueUrl,
213 };
214
215 } catch (error) {
216 shell.error(`🔴 spawnCopilotAgent error: ${error.message}`);
217 return {
218 success: false,
219 error: error.message,
220 };
221 }
222}