DEV Community

Abhishek Pandit
Abhishek Pandit

Posted on

A Day in the Life: Complete Copilot Workflow Session From Idea to Merged PR

Everything in this series has been theory. Good theory — I hope — but theory.

This article is different. We're going to walk through a complete, real session from start to merge. Real prompts, real outputs, real code. Every step of the 8-step workflow, nothing skipped.

The feature: add a due date to tasks, surface overdue tasks in the active list.

The starting point: an Express + TypeScript + Prisma backend with an existing task system. The feature doesn't exist yet.

Let's go.


The 8-Step Workflow

1. /spec chatmode → idea → spec → tasks
2. @test-engineer → write failing tests first
3. Implement → make tests pass
4. @simplifier → readability pass
5. /ship chatmode → SHIP verdict
6. CI green
7. Merge
8. Update CONTEXT.md
Enter fullscreen mode Exit fullscreen mode

One session. One feature. One merge.


Step 1: /spec Chatmode — Before a Single Line of Code

I open the spec chatmode in Copilot Chat and describe the feature:

I want to add due dates to tasks. Tasks past their due date should
be flagged as overdue. The active task list should sort overdue
tasks to the top.
Enter fullscreen mode Exit fullscreen mode

@idea-refiner asks five questions:

  • Should tasks without a due date appear in the overdue list? (Yes/No)
  • Is "overdue" calculated server-side or client-side?
  • Should overdue status persist (stored in DB) or be computed on-the-fly?
  • Do you need due date filtering (e.g., "due this week")?
  • Is this sprint backend-only or does the UI need updating too?

My answers: No (tasks without due dates aren't overdue), server-side, computed on-the-fly (no extra column), no filtering this sprint, backend only.

@spec-writer surfaces assumptions then writes the spec:

ASSUMPTIONS:
1. Due date is optional — tasks without one are never overdue
2. Overdue = dueDate < current timestamp (UTC), checked at query time
3. No new database column needed — status is computed in the query
4. Sorting: overdue tasks first, then by creation date within each group
5. No UI changes this sprint — API only
→ Confirm before I proceed.
Enter fullscreen mode Exit fullscreen mode

I confirm. The spec lands:

# Spec: Task Due Dates and Overdue Sorting

## Objective
Allow tasks to have optional due dates. Active task list returns overdue tasks
first (dueAt < NOW()), then remaining tasks by creation date.

## Success Criteria
- [ ] POST /api/tasks accepts optional dueAt (ISO 8601 datetime)
- [ ] GET /api/tasks returns tasks sorted: overdue first, then by createdAt desc
- [ ] PATCH /api/tasks/:id accepts dueAt updates
- [ ] A task with no dueAt is never flagged as overdue
- [ ] All existing task tests continue to pass

## Boundaries
Always: validate dueAt is a valid future date on creation
Ask first: any change to the Task DB schema beyond adding dueAt column
Never: store computed "isOverdue" flag in database — compute at query time
Enter fullscreen mode Exit fullscreen mode

@planner produces 3 tasks. I approve the plan.

Time spent: 12 minutes. Nothing built yet. Everything clear.


Step 2: @test-engineer — Failing Tests First

Task 1 is the schema + service layer. Before touching any implementation:

@test-engineer Write failing tests for the due date feature.
Spec is approved. Tests must fail with current code.

Requirements:
- POST /api/tasks accepts optional dueAt
- GET /api/tasks returns overdue tasks first, then by createdAt desc
- Tasks without dueAt are never overdue
Enter fullscreen mode Exit fullscreen mode

The agent produces:

describe('Task due dates', () => {
  describe('POST /api/tasks', () => {
    it('accepts a valid ISO 8601 due date', async () => {
      const res = await request(app)
        .post('/api/tasks')
        .send({ title: 'File taxes', dueAt: '2026-12-31T23:59:00Z' })
        .expect(201);

      expect(res.body.dueAt).toBe('2026-12-31T23:59:00.000Z');
    });

    it('rejects a past due date on creation', async () => {
      await request(app)
        .post('/api/tasks')
        .send({ title: 'Old task', dueAt: '2020-01-01T00:00:00Z' })
        .expect(422);
    });

    it('creates task successfully without a due date', async () => {
      const res = await request(app)
        .post('/api/tasks')
        .send({ title: 'No deadline' })
        .expect(201);

      expect(res.body.dueAt).toBeNull();
    });
  });

  describe('GET /api/tasks — overdue sorting', () => {
    it('returns overdue tasks before non-overdue tasks', async () => {
      const overdueTask = await createTask({
        title: 'Overdue',
        dueAt: new Date(Date.now() - 86400000) // yesterday
      });
      const futureTask = await createTask({
        title: 'Future',
        dueAt: new Date(Date.now() + 86400000) // tomorrow
      });
      const noDeadline = await createTask({ title: 'No deadline' });

      const res = await request(app).get('/api/tasks').expect(200);
      const titles = res.body.data.map((t: Task) => t.title);

      expect(titles[0]).toBe('Overdue');
      // Future and No deadline can be in either order after overdue
      expect(titles).toContain('Future');
      expect(titles).toContain('No deadline');
    });

    it('never marks a task without dueAt as overdue', async () => {
      await createTask({ title: 'No deadline' });

      const res = await request(app).get('/api/tasks').expect(200);
      const task = res.body.data.find((t: Task) => t.title === 'No deadline');

      expect(task.isOverdue).toBe(false);
    });

    it('sorts multiple overdue tasks by creation date descending', async () => {
      const older = await createTask({ title: 'Older overdue', dueAt: new Date(Date.now() - 172800000) });
      const newer = await createTask({ title: 'Newer overdue', dueAt: new Date(Date.now() - 86400000) });

      const res = await request(app).get('/api/tasks').expect(200);
      const overdueTitles = res.body.data
        .filter((t: Task) => t.isOverdue)
        .map((t: Task) => t.title);

      expect(overdueTitles[0]).toBe('Newer overdue');
      expect(overdueTitles[1]).toBe('Older overdue');
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

I run the tests:

npm test -- --grep "Task due dates"
Enter fullscreen mode Exit fullscreen mode
FAIL src/routes/tasks.test.ts
  ✕ accepts a valid ISO 8601 due date — 400 response (unexpected field)
  ✕ rejects a past due date on creation — 201 (no validation)
  ✕ returns overdue tasks before non-overdue tasks — column does not exist
  ...
Enter fullscreen mode Exit fullscreen mode

All failing. The spec is confirmed in test form. Now I implement.


Step 3: Implement — Make the Tests Pass

Migration first:

npx prisma migrate dev --name add-due-at-to-tasks
Enter fullscreen mode Exit fullscreen mode
model Task {
  id        String   @id @default(cuid())
  title     String
  dueAt     DateTime?     // ← added
  createdAt DateTime @default(now())
  completedAt DateTime?
}
Enter fullscreen mode Exit fullscreen mode

Update the Zod schema:

const CreateTaskSchema = z.object({
  title: z.string().min(1).max(200),
  dueAt: z.string().datetime().optional().refine(
    (val) => !val || new Date(val) > new Date(),
    { message: 'Due date must be in the future' }
  ),
});
Enter fullscreen mode Exit fullscreen mode

Update the list query for overdue sorting:

async function listActiveTasks(): Promise<Task[]> {
  const now = new Date();

  const [overdue, upcoming] = await Promise.all([
    // Overdue: completedAt is null AND dueAt is past
    db.task.findMany({
      where: { completedAt: null, dueAt: { lt: now, not: null } },
      orderBy: { createdAt: 'desc' },
    }),
    // Non-overdue: either no due date or future due date
    db.task.findMany({
      where: {
        completedAt: null,
        OR: [{ dueAt: null }, { dueAt: { gte: now } }],
      },
      orderBy: { createdAt: 'desc' },
    }),
  ]);

  return [
    ...overdue.map(t => ({ ...t, isOverdue: true })),
    ...upcoming.map(t => ({ ...t, isOverdue: false })),
  ];
}
Enter fullscreen mode Exit fullscreen mode

Run the tests:

npm test -- --grep "Task due dates"
Enter fullscreen mode Exit fullscreen mode
PASS src/routes/tasks.test.ts
  ✓ accepts a valid ISO 8601 due date (45ms)
  ✓ rejects a past due date on creation (12ms)
  ✓ creates task successfully without a due date (11ms)
  ✓ returns overdue tasks before non-overdue tasks (67ms)
  ✓ never marks a task without dueAt as overdue (43ms)
  ✓ sorts multiple overdue tasks by creation date descending (58ms)
Enter fullscreen mode Exit fullscreen mode

Full suite:

npm test
Enter fullscreen mode Exit fullscreen mode

All green. No regressions.


Step 4: @simplifier — Readability Pass

@simplifier Do a readability pass on src/services/task.ts
and src/routes/tasks.ts. Tests all pass. Focus on the new
due date and overdue sorting code.
Enter fullscreen mode Exit fullscreen mode

The agent flags one thing:

src/services/task.ts:34 — The Promise.all call with two separate queries is correct but the variable names overdue and upcoming could be more precise. Suggest overduetasks and nonOverdueTasks to match the domain language in the spec.

One rename. I apply it. Tests still pass.


Step 5: /ship Chatmode — The Verdict

I switch to the ship chatmode and describe the changes:

[In /ship chatmode]
Review the due date changes. New Prisma field, updated Zod
validation, new list query with overdue sorting.
Key concern: the parallel query approach — want to confirm
it's correct and won't have race conditions.
Enter fullscreen mode Exit fullscreen mode

The verdict:

## Pre-Merge Review

### Verdict: SHIP ✅

**Summary:** Due date implementation is clean, well-tested, and
correctly computes overdue status at query time as the spec requires.
No critical issues.

### Must Fix Before Merge
— None —

### Should Fix Before Merge
- [IMPORTANT] src/routes/tasks.ts:67 — The 422 validation error for
  past due dates returns the Zod error object directly. Wrap it in the
  project's standard error shape: `{ error: { code: 'VALIDATION_ERROR',
  message: '...', details: result.error.flatten() } }`

### Optional Improvements
- [SUGGESTION] Consider adding an index on (dueAt, completedAt) if
  this table grows large — the overdue query will do a full scan otherwise.

### Clean ✅
- Correct use of Promise.all — no race condition, both queries read
  independently from the same committed state
- Zod validation correctly rejects past dates at the boundary
- isOverdue computed at query time — no stale data risk
- All edge cases covered by tests
Enter fullscreen mode Exit fullscreen mode

One Important issue. I fix the error shape in 2 minutes. Re-run /ship. Clean verdict. SHIP.


Step 6: CI Green

I push the branch. GitHub Actions runs:

✓ Lint & Type Check
✓ Unit & Integration Tests
✓ Build
✓ Security Audit
Enter fullscreen mode Exit fullscreen mode

All green in 3 minutes.


Step 7: Merge

PR opened. CI badge green. /ship verdict documented in the PR description. Merge.


Step 8: Update CONTEXT.md

The last step most developers skip — and the one that pays compound interest.

## Recent Decisions

| Date | Decision | Reason |
|------|----------|--------|
| 2026-06-12 | Compute isOverdue at query time, not stored in DB | Avoids stale data, no background job needed |
| 2026-06-12 | Parallel queries for overdue/non-overdue | Single query with CASE sorting was complex; two clean queries are clearer |
Enter fullscreen mode Exit fullscreen mode

Next session, when I (or a teammate, or an AI agent) opens this project, the context is there. No archaeology required.


The Session at a Glance

Step Time What happened
/spec chatmode 12 min Idea → assumptions surfaced → spec → 3 tasks
@test-engineer 8 min 6 failing tests written
Implement 25 min Schema, validation, query
@simplifier 3 min One rename
/ship chatmode 5 min One Important issue found and fixed
CI 3 min All green
Merge + CONTEXT 2 min
Total 58 min Feature shipped with spec, tests, review

A working, tested, reviewed feature — in under an hour. With a spec that proves we built the right thing, tests that prove it works, a review that confirmed it's safe, and CI that will catch any regression.


Get the Template

Everything in this walkthrough — all 17 agents, 3 chatmodes, CI pipeline, MCP config, CONTEXT.md template — is in one place.

👉 github.com/panditAbhis/copilot-workflow

Click Use this template. Five minutes to set up. The discipline is yours to keep.


Series navigation

Top comments (0)