Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions kits/agentic/github-auto-fix-agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
GITHUB_AUTO_FIX="YOUR_FLOW_ID"
LAMATIC_API_URL="YOUR_API_ENDPOINT"
LAMATIC_API_KEY="YOUR_API_KEY"
LAMATIC_PROJECT_ID="YOUR_PROJECT_ID"
GITHUB_TOKEN="YOUR_GITHUB_TOKEN"
41 changes: 41 additions & 0 deletions kits/agentic/github-auto-fix-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env

Comment thread
RitoG09 marked this conversation as resolved.
# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
55 changes: 55 additions & 0 deletions kits/agentic/github-auto-fix-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# GitHub Auto Fix Agent

An AI-powered agent that analyzes GitHub issues and generates code fixes with ready-to-create pull requests.

---

## Features

- Understands GitHub issues automatically
- Identifies root cause of bugs
- Generates minimal code fixes (diff-based)
- Prepares PR metadata (title, body, branch)
- One-click PR creation

---

## How It Works

1. User provides a GitHub issue URL
2. Lamatic flow analyzes the issue
3. AI generates:
- Issue summary
- Root cause
- Code fix (diff)
- PR metadata
4. User reviews and creates PR

---

## Flow Overview

```mermaid
flowchart TD
A[User Input] --> B[Frontend]
B --> C[Backend API]
C --> D[Lamatic Flow]
D --> E[Analysis + Fix + PR Data]
E --> F[Frontend Preview]
F --> G[User Creates PR]
G --> H[GitHub PR Created]
```

## Lamatic Workflow

<p align="center">
<img src="./public/flows.png" alt="Lamatic Workflow" width="700"/>
</p>

## Setup Locally

```bash
cd kits/agentic/github-auto-fix-agent
npm install
cp .env.example .env
npm run dev
Comment thread
RitoG09 marked this conversation as resolved.
178 changes: 178 additions & 0 deletions kits/agentic/github-auto-fix-agent/actions/orchestrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"use server";

import { lamaticClient } from "@/lib/lamatic-client";

/**
* Step 1: Analyze the issue and generate a fix (NO PR creation).
* Returns analysis, fix data, and PR metadata from the Lamatic flow.
*/
export async function handleFixIssue(input: {
issue_url: string;
file_path?: string;
file_content?: string;
}) {
try {
console.log("[agent] Input received", {
hasIssueUrl: Boolean(input.issue_url),
hasFilePath: Boolean(input.file_path),
hasFileContent: Boolean(input.file_content),
});
const flowId = process.env.GITHUB_AUTO_FIX;
if (!flowId) throw new Error("Missing flow ID");

Comment thread
RitoG09 marked this conversation as resolved.
// Run Lamatic Flow
const resData = await lamaticClient.executeFlow(flowId, input);

if (resData.status !== "success" || !resData.result) {
throw new Error(resData.message || "Lamatic flow failed");
}

const result = resData.result;
const { analysis, fix, pr } = result;

console.log("[agent] Flow output received", {
hasAnalysis: Boolean(result?.analysis),
hasFix: Boolean(result?.fix),
hasPr: Boolean(result?.pr),
});

return {
success: true,
analysis,
fix,
pr, // branch_name, commit_message, pr_title, pr_body
};
} catch (error) {
console.error("[agent] Error:", error);

return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
};
}
}

/**
* Step 2: Create a GitHub PR using the fix data from Step 1.
* This is called separately only when the user clicks "Create PR".
*/
export async function handleCreatePR(input: {
issue_url: string;
file_path: string;
fix: { updated_code: string };
pr: {
branch_name: string;
commit_message: string;
pr_title: string;
pr_body: string;
};
}) {
try {
const { issue_url, file_path, fix, pr } = input;
Comment thread
RitoG09 marked this conversation as resolved.

// Extract repo info
const match = issue_url.match(/github.com\/(.*?)\/(.*?)\/issues\/(\d+)/);
if (!match) throw new Error("Invalid GitHub issue URL");

const [, owner, repo] = match;

// Get repo default branch
const repoData = await fetch(
`https://api.github.com/repos/${owner}/${repo}`,
{
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
},
},
).then((res) => res.json());
Comment thread
RitoG09 marked this conversation as resolved.

const baseBranch = repoData.default_branch;

// Get latest commit SHA
const refData = await fetch(
`https://api.github.com/repos/${owner}/${repo}/git/ref/heads/${baseBranch}`,
{
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
},
},
).then((res) => res.json());

const baseSha = refData.object.sha;

// Create branch
await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
ref: `refs/heads/${pr.branch_name}`,
sha: baseSha,
}),
});
Comment thread
RitoG09 marked this conversation as resolved.

// Get file SHA
const fileData = await fetch(
`https://api.github.com/repos/${owner}/${repo}/contents/${file_path}`,
{
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
},
},
).then((res) => res.json());

const fileSha = fileData.sha;

// Update file on branch
await fetch(
`https://api.github.com/repos/${owner}/${repo}/contents/${file_path}`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
message: pr.commit_message,
content: Buffer.from(fix.updated_code).toString("base64"),
branch: pr.branch_name,
sha: fileSha,
}),
},
);

// Create PR
const prRes = await fetch(
`https://api.github.com/repos/${owner}/${repo}/pulls`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: pr.pr_title,
head: pr.branch_name,
base: baseBranch,
body: pr.pr_body,
}),
},
);

const prData = await prRes.json();

return {
success: true,
pr_url: prData.html_url,
};
Comment on lines +164 to +169
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

PR creation response not validated before returning URL.

prData.html_url is returned without verifying the PR was actually created. If PR creation fails (e.g., duplicate PR, permission issues), html_url will be undefined and the success response will be misleading.

🐛 Proposed fix to validate PR creation
     const prData = await prRes.json();

+    if (!prRes.ok || !prData.html_url) {
+      throw new Error(`Failed to create PR: ${prData.message || "Unknown error"}`);
+    }
+
     return {
       success: true,
       pr_url: prData.html_url,
       analysis,
       fix,
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const prData = await prRes.json();
return {
success: true,
pr_url: prData.html_url,
analysis,
fix,
};
const prData = await prRes.json();
if (!prRes.ok || !prData.html_url) {
throw new Error(`Failed to create PR: ${prData.message || "Unknown error"}`);
}
return {
success: true,
pr_url: prData.html_url,
analysis,
fix,
};

} catch (error) {
console.error("[agent] PR creation error:", error);

return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
32 changes: 32 additions & 0 deletions kits/agentic/github-auto-fix-agent/app/api/create-pr/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { handleCreatePR } from "@/actions/orchestrate";

export async function POST(req: Request) {
try {
const body = await req.json();

if (!body.issue_url || !body.file_path || !body.fix || !body.pr) {
return Response.json(
{
success: false,
error:
"issue_url, file_path, fix, and pr are all required",
},
{ status: 400 },
);
}
Comment thread
RitoG09 marked this conversation as resolved.

const result = await handleCreatePR(body);

return Response.json(result);
} catch (error) {
console.error("[API ERROR]", error);

return Response.json(
{
success: false,
error: "Internal server error",
},
{ status: 500 },
);
}
}
28 changes: 28 additions & 0 deletions kits/agentic/github-auto-fix-agent/app/api/fix/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { handleFixIssue } from "@/actions/orchestrate";

export async function POST(req: Request) {
try {
const body = await req.json();

if (!body.issue_url) {
return Response.json(
{ success: false, error: "issue_url is required" },
{ status: 400 },
);
}

const result = await handleFixIssue(body);

return Response.json(result);
Comment on lines +3 to +16
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Protect this endpoint with authentication and repository scope checks.

POST /api/fix is currently unauthenticated, but it can trigger privileged actions (issue analysis + PR creation). This allows arbitrary external callers to consume your token-backed automation.

🔐 Proposed hardening
 export async function POST(req: Request) {
   try {
+    const internalApiToken = process.env.INTERNAL_API_TOKEN;
+    const authHeader = req.headers.get("authorization");
+    if (!internalApiToken || authHeader !== `Bearer ${internalApiToken}`) {
+      return Response.json(
+        { success: false, error: "Unauthorized" },
+        { status: 401 },
+      );
+    }
+
     const body = await req.json();
 
-    if (!body.issue_url) {
+    if (typeof body.issue_url !== "string" || body.issue_url.trim() === "") {
       return Response.json(
         { success: false, error: "issue_url is required" },
         { status: 400 },
       );
     }
+
+    let parsedIssueUrl: URL;
+    try {
+      parsedIssueUrl = new URL(body.issue_url);
+    } catch {
+      return Response.json(
+        { success: false, error: "issue_url must be a valid URL" },
+        { status: 400 },
+      );
+    }
+    if (parsedIssueUrl.hostname !== "github.com") {
+      return Response.json(
+        { success: false, error: "Only github.com issue URLs are supported" },
+        { status: 400 },
+      );
+    }
 
     const result = await handleFixIssue(body);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function POST(req: Request) {
try {
const body = await req.json();
if (!body.issue_url) {
return Response.json(
{ success: false, error: "issue_url is required" },
{ status: 400 },
);
}
const result = await handleFixIssue(body);
return Response.json(result);
export async function POST(req: Request) {
try {
const internalApiToken = process.env.INTERNAL_API_TOKEN;
const authHeader = req.headers.get("authorization");
if (!internalApiToken || authHeader !== `Bearer ${internalApiToken}`) {
return Response.json(
{ success: false, error: "Unauthorized" },
{ status: 401 },
);
}
const body = await req.json();
if (typeof body.issue_url !== "string" || body.issue_url.trim() === "") {
return Response.json(
{ success: false, error: "issue_url is required" },
{ status: 400 },
);
}
let parsedIssueUrl: URL;
try {
parsedIssueUrl = new URL(body.issue_url);
} catch {
return Response.json(
{ success: false, error: "issue_url must be a valid URL" },
{ status: 400 },
);
}
if (parsedIssueUrl.hostname !== "github.com") {
return Response.json(
{ success: false, error: "Only github.com issue URLs are supported" },
{ status: 400 },
);
}
const result = await handleFixIssue(body);
return Response.json(result);

} catch (error) {
console.error("[API ERROR]", error);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid logging raw error objects from privileged flows.

Raw exceptions may include sensitive context from upstream systems. Log a request ID + sanitized metadata instead.

🛡️ Proposed safe logging pattern
   } catch (error) {
-    console.error("[API ERROR]", error);
+    const requestId = crypto.randomUUID();
+    console.error("[API ERROR]", {
+      requestId,
+      name: error instanceof Error ? error.name : "UnknownError",
+    });
 
     return Response.json(
       {
         success: false,
-        error: "Internal server error",
+        error: `Internal server error (${requestId})`,
       },
       { status: 500 },
     );
   }


return Response.json(
{
success: false,
error: "Internal server error",
},
{ status: 500 },
);
}
}
Binary file not shown.
Loading
Loading