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
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.
@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.
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
@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
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');
});
});
});
I run the tests:
npm test -- --grep "Task due dates"
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
...
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
model Task {
id String @id @default(cuid())
title String
dueAt DateTime? // ← added
createdAt DateTime @default(now())
completedAt DateTime?
}
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' }
),
});
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 })),
];
}
Run the tests:
npm test -- --grep "Task due dates"
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)
Full suite:
npm test
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.
The agent flags one thing:
src/services/task.ts:34— ThePromise.allcall with two separate queries is correct but the variable namesoverdueandupcomingcould be more precise. SuggestoverduetasksandnonOverdueTasksto 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.
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
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
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 |
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)