Anthropic Code Sandbox for GitLab SMB Issue Auto-Resolution
An AI coding agent that automatically suggests fixes for reported GitLab issues, runs code in a sandbox, verifies the solution, and opens a merge request.
SMB development teams drown in routine bug reports and feature requests. Manually assigning, coding, and testing small fixes eats hours that should go to feature work, while issues sit unresolved and customers wait.
A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
This tutorial walks you through building an autonomous AI coding agent that suggests fixes for reported GitLab issues. When a new issue is filed, the system routes it through specialist agents: one fetches repository context, one uses Anthropic Claude to generate a code fix, another executes that fix in an E2B sandbox to verify it works, and a confidence router decides if the fix is reliable enough to create a GitLab merge request. Circuit-breaker agents prevent runaway loops, and structured repair ensures LLM output becomes valid patch files.
You’ll build an Express webhook server to receive GitLab issue events, a Next.js admin dashboard to review proposals, and all the agent wiring in between.
Familiarity with TypeScript, Next.js App Router, and Express routing
Step 1: Scaffold the project and install dependencies
Create a Next.js App Router project, then add the dependencies this recipe needs. The project uses @reaatech/* packages for agent orchestration, confidence scoring, circuit breaking, and structured LLM output repair, plus @anthropic-ai/sdk for Claude and @e2b/code-interpreter for sandboxed code execution.
Create src/types/index.ts to re-export everything:
ts
export type { GitLabIssue, GitLabMergeRequest, GitLabUser, WebhookPayload,} from "./gitlab.js";export type { FixProposal, VerificationResult, ProjectConfig,} from "./fix.js";
Expected output:pnpm typecheck passes with no errors.
Step 3: Build the GitLab API client
The GitLabClient wraps the GitLab REST API. It handles authentication, issue fetching, file retrieval, branch creation, MR creation, and file commits. All fetch calls throw typed GitLabApiError on non-2xx responses, with network errors caught and rethrown as 0-status errors.
Create src/services/gitlab-client.ts:
ts
import type { GitLabIssue, GitLabMergeRequest, GitLabUser } from "../types/index.js";export class GitLabApiError extends Error { constructor( public status: number, public body: string, ) { super(`GitLab API error: ${String(status)} ${body}`); this.name = "GitLabApiError"; }}export class GitLabClient { private baseUrl: string; private accessToken: string;
Expected output:pnpm typecheck reports no errors.
Step 4: Set up circuit breakers
Circuit breakers protect each agent behind a fail-fast wrapper. When an agent fails failureThreshold consecutive times, the circuit opens and subsequent calls throw CircuitOpenError instead of hitting the failing agent. After recoveryTimeoutMs elapses, the circuit transitions to half-open, allowing one probe call through.
Create src/lib/circuit-breaker-config.ts:
ts
import { CircuitBreaker, CircuitOpenError, InMemoryAdapter } from "@reaatech/circuit-breaker-agents";export { CircuitOpenError };export function createCircuitBreaker(name: string): CircuitBreaker { return new CircuitBreaker({ name, failureThreshold: Number(process.env.CIRCUIT_BREAKER_FAILURE_THRESHOLD) || 5, recoveryTimeoutMs: Number(process.env.CIRCUIT_BREAKER_RECOVERY_MS) || 30000, persistence: new InMemoryAdapter(), });}export const circuitBreakers: Map<string, CircuitBreaker> = new Map([ ["code-generator", createCircuitBreaker("code-generator")], ["context-fetcher", createCircuitBreaker("context-fetcher")], ["sandbox-verifier", createCircuitBreaker("sandbox-verifier")], ["gitlab-api", createCircuitBreaker("gitlab-api")],]);
Expected output:pnpm typecheck passes. Each agent call site will later wrap its invocation with await breaker.execute(() => agent.call()).
Step 5: Build the context fetcher
The context fetcher retrieves the issue from GitLab and parses the description to identify relevant source files. It then fetches each file’s content, returning a ContextBundle that the code generator can use.
Create src/agent/context-fetcher.ts:
ts
import type { GitLabIssue } from "../types/gitlab.js";import type { GitLabClient } from "../services/gitlab-client.js";export interface ContextBundle { issue: GitLabIssue; relevantFiles: Array<{ path: string; content: string }>;}export class ContextFetcher { constructor(private gitlabClient: GitLabClient) {} async fetchContext(issueIid: number): Promise<ContextBundle> { const issue = await this.gitlabClient.getIssue(issueIid); const paths = this.guessRelevantFiles(issue); const relevantFiles: Array<{ path: string; content: string }> = []; for (const filePath of paths) { try { const content = await this.gitlabClient.getRepositoryFile(filePath); relevantFiles.push({ path: filePath, content }); } catch { // file may not exist; skip } } return { issue, relevantFiles }; } private guessRelevantFiles(issue: GitLabIssue): string[] { const paths: string[] = []; const filePattern = /(?:src\/[\w./-]+\.[a-z]+|[\w./-]+\.(ts|tsx|js|jsx|py|go|rs|rb|java))/g; const textToSearch = `${issue.description} ${issue.labels.join(" ")}`; const matches = textToSearch.match(filePattern); if (matches) { for (const match of matches) { if (!paths.includes(match)) { paths.push(match); } } } return paths; }}
Expected output:pnpm typecheck passes. The guessRelevantFiles method extracts file paths from issue descriptions and labels using a regex that matches common source file extensions.
Step 6: Build the Claude code generator
The code generator sends a formatted prompt to Anthropic Claude with the issue context and asks it to produce a minimal code fix. The system prompt instructs Claude to return structured JSON with a files array and a summary string.
Create src/agent/code-generator.ts:
ts
import Anthropic from "@anthropic-ai/sdk";import type { ContextBundle } from "./context-fetcher.js";export class CodeGenerationError extends Error { constructor(message: string, cause?: unknown) { super(message); this.name = "CodeGenerationError"; this.cause = cause; }}export class ClaudeCodeGenerator { private client: Anthropic; constructor() { this.client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); } async generateFix(context: ContextBundle): Promise<string> { const prompt = this.buildPrompt(context); try { const message = await this.client.messages.create({ model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-6", max_tokens: 4096, system: "You are a code-fixing AI. Analyse the given issue and produce a minimal code fix. Return output as a JSON object with shape { files: Array<{ path: string, content: string }>, summary: string }.", messages: [{ role: "user", content: prompt }], }); console.log(message.usage); if (message.stop_reason === "tool_use") { const textBlock = message.content[0]; return textBlock.type === "text" ? textBlock.text : ""; } const textBlock = message.content[0]; return textBlock.type === "text" ? textBlock.text : ""; } catch (err) { throw new CodeGenerationError("Failed to generate fix", err); } } private buildPrompt(context: ContextBundle): string { let prompt = `Issue #${String(context.issue.iid)}: ${context.issue.title}\n\n${context.issue.description}\n\n`; if (context.relevantFiles.length > 0) { prompt += "Relevant files:\n\n"; for (const file of context.relevantFiles) { prompt += `--- ${file.path} ---\n${file.content}\n\n`; } } prompt += "\nProduce a JSON fix with files array and summary."; return prompt; }}
Expected output:pnpm typecheck passes. The generator logs token usage via message.usage for cost tracking.
Step 7: Build the E2B sandbox verifier
The sandbox verifier executes the proposed fix files inside an E2B cloud sandbox. It writes each file to the sandbox filesystem, runs a validation command (or exit 0 if none provided), and returns pass/fail with execution output.
Expected output:pnpm typecheck passes. The verifier handles timeouts via Promise.race with your configured SANDBOX_TIMEOUT_MS.
Step 8: Integrate the confidence router
The confidence router scores fix quality. If the E2B sandbox passes the fix and produces meaningful output, the confidence score rises above the route threshold (0.8), and the system proceeds to MR creation. If the sandbox fails, confidence drops to 0.1 and the fix is rejected.
Expected output:pnpm typecheck passes. A sandbox-verified fix with output over 1KB gets ~0.95 confidence.
Step 9: Integrate structured repair
Claude sometimes returns JSON wrapped in markdown fences or with trailing commas. The @reaatech/structured-repair-core package handles these edge cases — it strips fences, removes extra fields, and repairs malformed JSON before returning typed data.
Create src/lib/structured-repair.ts:
ts
import { z } from "zod";import { repair, repairOutput, isValid, analyzeInput, UnrepairableError } from "@reaatech/structured-repair-core";import type { RepairResult } from "@reaatech/structured-repair-core";export const FixOutputSchema = z.object({ files: z.array(z.object({ path: z.string(), content: z.string() })), summary: z.string(),});export async function repairLlmOutput(raw: string): Promise<z.infer<typeof FixOutputSchema>> { return repair(FixOutputSchema, raw);}export async function repairLlmOutputWithDiagnostics(raw: string): Promise<RepairResult<z.infer<typeof FixOutputSchema>>> { return await Promise.resolve(repairOutput({ schema: FixOutputSchema, input: raw, debug: false }));}export function validateFixOutput(obj: unknown): boolean { return isValid(FixOutputSchema, typeof obj === "string" ? obj : JSON.stringify(obj));}export { UnrepairableError };export function analyzeFixOutput(input: string): ReturnType<typeof analyzeInput> { return analyzeInput(input);}
Expected output:pnpm typecheck passes. The Zod schema FixOutputSchema is shared by both the repair functions and the patch service for type-safe MR creation.
Step 10: Build the patch service
The patch service creates a branch on the GitLab repo, commits the fix files, and opens a merge request. The branch name is derived from the issue IID and a timestamp for uniqueness.
Create src/services/patch-service.ts:
ts
import type { GitLabClient } from "./gitlab-client.js";import type { GitLabMergeRequest } from "../types/gitlab.js";import type { FixOutputSchema } from "../lib/structured-repair.js";import type { z } from "zod";export class PatchService { constructor(private gitlabClient: GitLabClient) {} async createBranchAndMR( fix: z.infer<typeof FixOutputSchema>, issueIid: number, ): Promise<GitLabMergeRequest | null> { if (fix.files.length === 0) { return null; } const branchName = `fix/issue-${String(issueIid)}-${String(Date.now())}`; await this.gitlabClient.createBranch(branchName, "main"); await this.gitlabClient.commitFiles(branchName, fix.files, `Fix: ${fix.summary}`); return this.gitlabClient.createMergeRequest({ title: fix.summary, description: `Auto-fix for issue #${String(issueIid)}\n\n${fix.summary}`, sourceBranch: branchName, targetBranch: "main", }); }}
Expected output:pnpm typecheck passes.
Step 11: Wire the issue router
The issue router is the orchestrator. It validates the incoming webhook payload, builds a ContextPacket from the issue data, then runs the pipeline through each circuit-broken step: fetch context → generate fix → parse/repair → verify in sandbox → score confidence → repair output → create MR. If any circuit is open, processing stops gracefully.
Create src/agent/issue-router.ts:
ts
import { ContextPacketSchema, type ContextPacket } from "@reaatech/agent-mesh";import type { WebhookPayload } from "../types/gitlab.js";import type { ContextBundle } from "./context-fetcher.js";import type { ClaudeCodeGenerator } from "./code-generator.js";import type { ContextFetcher } from "./context-fetcher.js";import type { SandboxVerifier } from "./verifier.js";import type { PatchService } from "../services/patch-service.js";import type { RoutingDecision } from "../lib/confidence-router.js";import type { VerificationResult } from "../types/fix.js";
Expected output:pnpm typecheck passes. The createDefaultIssueRouter factory wires all dependencies from environment-configured instances, using dynamic imports so that route modules can call it without circular imports.
Step 12: Build the Express webhook server
The Express server exposes the POST /gitlab_webhook endpoint that GitLab calls when issues are created. It validates the x-gitlab-token header, handles GitLab ping events, and fires the issue router asynchronously.
import { app } from "./app.js";const port = Number(process.env.PORT) || 3001;app.listen(port, () => { console.log(`Server running on port ${String(port)}`); });
Expected output: Run npx tsx src/server/index.ts and you see Server running on port 3001. A curl to POST /gitlab_webhook with a valid token returns 202 {"status": "processing"}.
Step 13: Build the Next.js admin dashboard and API
The admin dashboard shows health status, configured projects, circuit breaker states, and recent fix proposals. Two API routes support the dashboard: one returns project config, and one handles MR listing and approve/reject actions.
Expected output: The dashboard renders at http://localhost:3000/dashboard showing a health indicator, project list, and a circuit breaker state table with green CLOSED / red OPEN / yellow HALF_OPEN state colors.
Step 14: Write and run the tests
The test suite uses vitest with supertest for the Express routes and direct function invocation for Next.js API handlers. Here’s a representative test for structured repair and one for the GitLab webhook route.
Expected output: All tests pass, and coverage metrics on runtime code (src/**/*.ts and app/**/route.ts) meet the 90% threshold for lines, branches, functions, and statements.
Next steps
Add a persistent circuit breaker store — Swap InMemoryAdapter for Redis or SQLite so breaker state survives server restarts across a fleet of instances.
Multi-turn Claude tool use — Extend the code generator to use Claude’s tool-calling mode for iterative fix refinement, instead of the single-shot JSON prompt used here.
Slack or Discord notifications — Add a notification agent that posts a summary to team chat when an MR is created, rejected due to low confidence, or when a circuit breaker opens.
Human-in-the-loop approval — Instead of auto-merging fixes that score above threshold, send the proposed MR to a dashboard for human review before the merge is executed.
Scheduled issue reconciliation — Run the router on a schedule (e.g., daily cron) against all open issues, not just newly filed ones, so existing bugs also get triaged.