Blog
Blog

From GitHub Backlog to Merged PR with AI: A Walkthrough

A practical walkthrough of how Codowave takes a real GitHub issue from backlog to merged PR — what happens at each step, what you review, and what you skip.

8 min read

From GitHub Backlog to Merged PR with AI: A Walkthrough

The pitch for autonomous AI coding agents sounds clean: connect your repo, watch PRs appear. The reality is a bit more detailed — and understanding the mechanics matters for setting up filters correctly, knowing what to review, and calibrating expectations.

This post walks through a real issue run: from a GitHub issue sitting in your backlog to a merged PR. What happens at each step, what you see, and what you're skipping vs. what you're responsible for.


The Starting State

Let's say your repo is a Node.js/TypeScript API. You have the following open issue:

Issue #247 — Bug: GET /api/orders returns 500 when order has no line items
Labels: bug, backend, p2
Assignee: (unassigned)
Story points: 2
Body:
When an order has zero line items (it was cancelled before any items were added), 
the GET /api/orders endpoint returns a 500 error instead of returning the order 
with an empty items array.

Steps to reproduce:
1. Create an order (POST /api/orders) → note the order ID
2. Cancel the order immediately (DELETE /api/orders/:id) — don't add items
3. GET /api/orders — 500 error

Expected: order returned with items: []
Current: 500 Internal Server Error

Relevant file: src/services/order.service.ts (suspecting mapOrderToResponse function)

This is a well-defined bug. Reproducible, scoped, with a suspected location. Good Codowave input.


Step 1: Issue Selection

Codowave's issue poller runs on your configured schedule (default: every 30 minutes, more frequently on the Team plan). It reads your open issues and scores them against your configured filters:

Your filters:

  • Labels must include bug OR backend
  • Story points ≤ 5
  • Risk level: exclude issues tagged auth, payments, security
  • Assignee: unassigned only

Issue #247 matches. Codowave marks it as "picked" (posts a comment on the issue: "Codowave is working on this"), creates a new branch (codowave/247-fix-orders-500-empty-line-items), and starts the pipeline.

What you see at this point: A comment on issue #247 and a new branch in your repo.


Step 2: Planner Agent

The Planner agent receives the issue body and runs a codebase search to understand the scope.

It reads:

  • src/services/order.service.ts (mentioned in the issue)
  • src/controllers/orders.controller.ts (the endpoint handler)
  • src/types/order.types.ts (type definitions)
  • src/tests/orders.test.ts (existing test file, to understand test patterns)

Planner output (logged and visible in the run timeline):

Decomposition:
1. Find mapOrderToResponse function in order.service.ts
2. Identify the line causing 500 on empty items array (likely a .map() or .reduce() on undefined)
3. Add null/empty array guard
4. Verify existing tests still pass
5. Add regression test: GET /api/orders with order that has no line items

Affected files:
- src/services/order.service.ts (primary change)
- src/tests/orders.test.ts (regression test)

Risk: Low — isolated to order mapping function, no auth or payments involvement

The Planner takes about 2-3 minutes.


Step 3: Coder Agent

The Coder receives the Planner's decomposition and reads only the identified files. It checks Codowave's pattern memory for this repo:

Pattern memory notes:

  • TypeScript interfaces, not type aliases, for response types
  • Error handling uses AppError class, not raw Error
  • Tests use describe/it with factory functions (createOrder(), createLineItem())
  • expect().resolves for async tests

The Coder finds the bug in order.service.ts:

// Before (line 34)
const mapOrderToResponse = (order: Order): OrderResponse => ({
  id: order.id,
  status: order.status,
  items: order.lineItems.map(item => ({  // Throws if lineItems is undefined
    id: item.id,
    quantity: item.quantity,
    price: item.price
  }))
});

The fix:

// After
const mapOrderToResponse = (order: Order): OrderResponse => ({
  id: order.id,
  status: order.status,
  items: (order.lineItems ?? []).map(item => ({
    id: item.id,
    quantity: item.quantity,
    price: item.price
  }))
});

The Coder writes the change and commits to the branch. It takes about 4-6 minutes.


Step 4: Reviewer Agent

The Reviewer receives only the git diff — not the Planner's reasoning or the Coder's implementation process. Fresh context, focused on the diff.

Reviewer checks:

  • Does the fix address the reported issue? ✓
  • Does order.lineItems ?? [] handle both undefined and null? ✓ (?? catches both)
  • Is there an edge case where lineItems is not undefined but contains null items? Flagged.
  • Does the fix follow the repo's TypeScript patterns? ✓
  • Does the PR title/branch match commit message format? ✓

Reviewer output:

1 issue flagged (low severity):
- Line 37: order.lineItems could theoretically contain null elements within the array 
  (e.g., [null, {...}]). The current fix handles undefined/null array but not null 
  elements. Recommend: filter nulls before mapping.
  Suggested fix: (order.lineItems ?? []).filter(Boolean).map(...)

The Coder gets one more pass to apply the Reviewer's suggestion:

items: (order.lineItems ?? []).filter(Boolean).map(item => ({

Step 5: Tester Agent

The Tester runs the existing test suite:

npm test -- src/tests/orders.test.ts

✓ GET /api/orders returns list of orders (45ms)
✓ GET /api/orders/:id returns single order (23ms)  
✓ POST /api/orders creates new order (67ms)
✓ GET /api/orders filters by status (31ms)

4 tests passed, 0 failed

The Tester checks test coverage on the modified code — the mapOrderToResponse function wasn't covered by the existing tests. It writes a regression test:

it('returns order with empty items array when order has no line items', async () => {
  const order = await createOrder({ lineItems: undefined });
  const response = await request(app).get('/api/orders');
  
  expect(response.status).toBe(200);
  const found = response.body.find((o: OrderResponse) => o.id === order.id);
  expect(found).toBeDefined();
  expect(found.items).toEqual([]);
});

Runs the full suite again:

✓ GET /api/orders returns list of orders (45ms)
✓ GET /api/orders/:id returns single order (23ms)
✓ POST /api/orders creates new order (67ms)
✓ GET /api/orders filters by status (31ms)
✓ GET /api/orders returns order with empty items array when order has no line items (38ms)

5 tests passed, 0 failed

The Tester step takes about 5-8 minutes including test writing.


Step 6: PR Opens

Total time from issue selection to PR: approximately 22 minutes.

The PR Codowave opens:

Title: Fix: GET /api/orders 500 error when order has no line items (#247)

Body:
## Summary
Fixes #247. The `mapOrderToResponse` function in `order.service.ts` called 
`.map()` on `order.lineItems` without guarding against undefined/null values. 
Orders created and immediately cancelled have no line items, causing the 500.

## Changes
- `src/services/order.service.ts`: Added null guard `(order.lineItems ?? []).filter(Boolean)` 
  before `.map()` call in `mapOrderToResponse` (line 34)
- `src/tests/orders.test.ts`: Added regression test for GET /api/orders with 
  order that has no line items

## Reviewer notes addressed
- Added `.filter(Boolean)` to handle potential null elements within the array 
  (flagged by Reviewer agent, low severity)

## Test results
5 tests passing, 0 failing
New test added: "returns order with empty items array when order has no line items"

## Run details

<CtaButton href="https://app.codowave.com/runs/r_abc123">View full run replay</CtaButton>

Cost: $1.40 / $5.00 ceiling
Agents: Planner (0.40s), Coder (0.45s), Reviewer (0.30s), Tester (0.25s)

Step 7: What You Do Now

Since you're in watch-only mode:

  1. You get a notification (GitHub, Slack if on Team plan, email) — "Codowave opened PR #248"
  2. You open the PR — reads like a real PR from a careful developer
  3. You review the diff — 2 files, ~15 lines changed, looks correct
  4. You read the test output — 5 passing, regression test added
  5. You merge — or click approve if branch protection requires review

Total time on your end: 5-10 minutes reviewing, not 2 hours writing.

Once you've seen 5-10 PRs like this and they're consistently correct, you configure auto-merge for issues labeled bug + p2 + story points ≤ 2. Codowave handles those end-to-end. You wake up to already-merged fixes.


What Makes a Good Issue for This Workflow

Based on this example, the patterns that make issues Codowave-ready:

  • Specific reproduction steps — the more concrete, the better the Planner's decomposition
  • Suspected location — "Relevant file: X" dramatically improves accuracy
  • Clear expected behavior — "expected: empty array, current: 500" gives the Tester a clear pass/fail criterion
  • Bounded scope — changes to 1-3 files, not sweeping architectural changes

You don't need perfect issues — Codowave posts clarifying questions when issues are ambiguous. But well-written issues produce better PRs faster.


Frequently asked questions