⌂ Home Gemini CLI Mastery — AI SDLC Track
Track 3 — AI SDLC
📚 Module 8 of 18 ⏰ 55 min
M07 — Implementation

From Spec to Working Code

You have a JSON spec from M05 and design artifacts from M06. Now use Gemini CLI to scaffold the RecurringTask model, generate the router, wire it into TaskFlow, and fix the first test failure — all guided by the actual codebase patterns Gemini reads in real-time.

  • Scaffold the RecurringTask model directly from spec/recurring-tasks.json
  • Generate routers/recurrence.py by loading the entire routers/ folder for style matching
  • Understand why Gemini matches db.query(...).filter(...).first() and model_config patterns
  • Generate conventional commit messages from staged diffs
  • Run an iterative refinement cycle: paste error output, get a fix

Scaffold From a Spec

The JSON spec from M05 is not just documentation — it's executable input. When you feed @spec/recurring-tasks.json to Gemini alongside the existing source files, you get code that implements exactly what was specced, using the patterns already in the codebase.

Analogy — The Blueprint Translator

BEFORE: A building architect hands blueprints to a contractor. The contractor reads the structural drawings, notices the building uses steel frame everywhere, and builds the extension in the same steel frame style — not wood, not concrete.
PAIN: If the contractor ignores the blueprint and builds in whatever material is easiest that day, the extension doesn't match the rest of the building. It's technically standing, but it creates maintenance headaches for decades.
MAPPING: Gemini CLI is the contractor who reads both the blueprint (spec JSON) and the existing structure (models.py, schemas.py). It generates code that fits — same import structure, same naming conventions, same SQLAlchemy patterns — not code that merely compiles.

Scaffold the RecurringTask Model

PowerShell / Terminal — from sample-project/taskflow/
gemini "@spec/recurring-tasks.json @models.py @database.py
Implement the RecurringTask SQLAlchemy model following the spec.
The spec is in recurring-tasks.json under 'new_models'.

Requirements:
- Match the import style at the top of models.py exactly
- Use Column(DateTime(timezone=True), server_default=func.now()) for timestamps
- Add relationship() to the existing Task class using back_populates
- Add cascade='all, delete-orphan' to mirror the User.tasks pattern

Output: the complete RecurringTask class definition + the 2 lines
to add to the Task class. Python only, no markdown." >> models.py
WHAT: The >> models.py appends Gemini's output directly to the existing file. This is an efficient workflow when you trust the output — but always review before committing.
WHY THE SPEC: Without @spec/recurring-tasks.json, you'd describe the model fields in natural language and risk mismatches. The JSON spec has exact field names, types, and constraints — Gemini reads it directly.
GOTCHA: Review the appended output before running the server. If Gemini added a duplicate import, remove it. The append pattern is fast but not foolproof.

Scaffold the Pydantic Schemas

PowerShell / Terminal
gemini "@schemas.py @spec/recurring-tasks.json
Add Pydantic v2 schemas for the RecurringTask model.
The spec defines the fields under 'new_models[0].fields'.

Requirements:
- Create RecurringTaskCreate (schedule_type, interval_value)
- Create RecurringTask response schema (all fields + model_config)
- Follow the exact pattern of TagBase / TagCreate / Tag (lines 7-16 of schemas.py)
  - Use model_config = {'from_attributes': True} on the response schema
  - Keep the three-schema pattern: Base → Create → Response

Output: the three schema classes. Python only." >> schemas.py
PATTERN MATCHING: Gemini reads lines 7-16 of schemas.py — the TagBase/TagCreate/Tag trio — and replicates that exact structure for RecurringTask. It uses model_config = {"from_attributes": True} because that's how TaskFlow's schemas work. Without this context, it might use deprecated Pydantic v1 class Config: orm_mode = True.

Multi-File Context Loading

The most powerful feature of Gemini CLI's @ syntax is loading an entire directory. When you write @routers/, Gemini reads every file in that directory — giving it complete knowledge of every existing router pattern before generating a new one.

What @routers/ Loads — Files Read Before Generating recurrence.py
📄
routers/tasks.py
CRUD patterns, auth usage, query style
📄
routers/users.py
Registration, login, OAuth2 form pattern
📄
routers/tags.py
Simple CRUD, uniqueness check pattern
models.py
SQLAlchemy model definitions
📋
schemas.py
Pydantic request/response shapes
routers/recurrence.py
Generated — matches all loaded patterns

Generate the Recurrence Router

PowerShell / Terminal
gemini "@routers/ @models.py @schemas.py @spec/recurring-tasks.json
Create a new file routers/recurrence.py with three endpoints from the spec:
  POST   /tasks/{task_id}/recurrence
  GET    /tasks/{task_id}/recurrence
  DELETE /tasks/{task_id}/recurrence

Follow ALL patterns from the existing routers/:
- Router prefix and tags declaration style (see routers/tasks.py line 9)
- Auth: use Depends(get_current_user) on every endpoint
- Query pattern: db.query(Model).filter(...).first() — not ORM session style
- 404 handling: raise HTTPException(status_code=404, detail='...')
- Owner validation: only the task owner can manage its recurrence
- Response models: use schemas.RecurringTask and schemas.RecurringTaskCreate

Output the complete routers/recurrence.py file. Python only." \
  > routers/recurrence.py
WHAT: @routers/ loads tasks.py, users.py, and tags.py simultaneously. Gemini infers the team's code style from three different files before generating a fourth.
WHY THIS MATTERS: The instruction "db.query(Model).filter(...).first() — not ORM session style" is explicit — but Gemini would likely get it right anyway because it reads the existing routers. Explicit style instructions are belt-and-suspenders when generating files that must match a specific team's patterns.
GOTCHA: The > redirect creates the file from scratch. Don't use >> here — you want a clean new file, not an append.

Generated Router — Key Patterns

routers/recurrence.py — Gemini output
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from datetime import datetime, timedelta, timezone

from database import get_db
from auth import get_current_user
import models, schemas

router = APIRouter(prefix="/tasks", tags=["recurrence"])


@router.post("/{task_id}/recurrence",
             response_model=schemas.RecurringTask,
             status_code=201)
def create_recurrence(
    task_id: int,
    recurrence: schemas.RecurringTaskCreate,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(get_current_user),
):
    # Verify task exists and belongs to the current user
    task = db.query(models.Task).filter(
        models.Task.id == task_id,
        models.Task.owner_id == current_user.id,
    ).first()
    if not task:
        raise HTTPException(status_code=404, detail="Task not found")

    # Prevent duplicate recurrences
    if db.query(models.RecurringTask).filter(
        models.RecurringTask.task_id == task_id
    ).first():
        raise HTTPException(status_code=400, detail="Recurrence already exists")

    # Calculate first next_run_at based on schedule_type
    now = datetime.now(tz=timezone.utc)
    if recurrence.schedule_type == "daily":
        next_run = now + timedelta(days=recurrence.interval_value or 1)
    elif recurrence.schedule_type == "weekly":
        next_run = now + timedelta(weeks=recurrence.interval_value or 1)
    else:  # monthly
        next_run = now + timedelta(days=30 * (recurrence.interval_value or 1))

    db_rec = models.RecurringTask(
        task_id=task_id,
        schedule_type=recurrence.schedule_type,
        interval_value=recurrence.interval_value,
        next_run_at=next_run,
    )
    db.add(db_rec)
    db.commit()
    db.refresh(db_rec)
    return db_rec


@router.get("/{task_id}/recurrence", response_model=schemas.RecurringTask)
def get_recurrence(
    task_id: int,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(get_current_user),
):
    task = db.query(models.Task).filter(
        models.Task.id == task_id,
        models.Task.owner_id == current_user.id,
    ).first()
    if not task:
        raise HTTPException(status_code=404, detail="Task not found")

    rec = db.query(models.RecurringTask).filter(
        models.RecurringTask.task_id == task_id
    ).first()
    if not rec:
        raise HTTPException(status_code=404, detail="No recurrence set for this task")
    return rec


@router.delete("/{task_id}/recurrence", status_code=204)
def delete_recurrence(
    task_id: int,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(get_current_user),
):
    task = db.query(models.Task).filter(
        models.Task.id == task_id,
        models.Task.owner_id == current_user.id,
    ).first()
    if not task:
        raise HTTPException(status_code=404, detail="Task not found")

    rec = db.query(models.RecurringTask).filter(
        models.RecurringTask.task_id == task_id
    ).first()
    if not rec:
        raise HTTPException(status_code=404, detail="No recurrence to delete")
    db.delete(rec)
    db.commit()
PATTERNS MATCHED: Every pattern from the existing routers is present. Line 1-7 imports match tasks.py exactly. The auth pattern (Depends(get_current_user)) matches all three existing routers. The owner validation (models.Task.owner_id == current_user.id) mirrors tasks.py line 58-59. The 404 raise matches tags.py exactly. This is codebase-aware generation, not generic code.

Wire the Router Into main.py

PowerShell / Terminal
gemini "@main.py @routers/recurrence.py
Show me the minimal diff to wire the new recurrence router into main.py.
Follow the exact pattern used to add tasks and tags routers.
Output only the 2 lines to add — the import and the app.include_router() call."
Gemini Output — 2 lines to add to main.py
# Line to add at top with other router imports:
from routers import users, tasks, tags, recurrence

# Line to add after app.include_router(tags.router):
app.include_router(recurrence.router)
WHAT JUST HAPPENED: Gemini read main.py, saw the pattern of three existing imports and three include_router calls, and told you exactly what to change. It didn't rewrite the whole file — it gave you a surgical two-line diff. That's the appropriate response to "show me the minimal diff."

Code That Fits Existing Patterns

Pattern consistency isn't about style preferences — it's about maintainabilityThe ease with which a codebase can be understood, modified, and extended by someone who didn't write it. Consistent patterns reduce cognitive load: a developer who understands tasks.py can immediately understand recurrence.py because they use the same patterns.. When every router does auth the same way, every developer knows exactly where to look. Let's examine the specific patterns Gemini learned from TaskFlow.

The Three Patterns Gemini Observed

Pattern 1 — Query Style (from routers/tasks.py)
# TaskFlow's query pattern — always:
task = db.query(models.Task).filter(
    models.Task.id == task_id,
    models.Task.owner_id == current_user.id,
).first()

# NOT the SQLAlchemy 2.0 session.execute() style:
# result = db.execute(select(models.Task).where(...))  # ← Gemini avoided this
# task = result.scalar_one_or_none()  # ← Gemini avoided this
WHY THIS MATTERS: SQLAlchemy 2.0 introduced session.execute(select(...)) as the new recommended style. But TaskFlow uses the legacy db.query() style throughout. If Gemini switched styles mid-codebase, it would create an inconsistency that future developers would need to resolve. Consistency > modernity in an existing codebase.
Pattern 2 — Pydantic Config (from schemas.py lines 15, 44, 58)
# TaskFlow's schema pattern — every response schema has:
class RecurringTask(RecurringTaskBase):
    id: int
    task_id: int
    next_run_at: datetime
    is_active: bool
    created_at: datetime
    model_config = {"from_attributes": True}  # ← Pydantic v2 ORM mode

# NOT the Pydantic v1 style:
# class Config:
#     orm_mode = True  # ← Gemini avoided this deprecated pattern
GOTCHA PREVENTED: model_config = {"from_attributes": True} is Pydantic v2 syntax. class Config: orm_mode = True is Pydantic v1 (deprecated). Without reading the existing schemas.py, an AI model might generate v1 syntax — which fails silently in some Pydantic configurations. Context prevents this category of bug.
Pattern 3 — Auth Dependency (all routers)
# Every protected endpoint in TaskFlow uses:
current_user: models.User = Depends(get_current_user)

# And verifies ownership before any operation:
task = db.query(models.Task).filter(
    models.Task.id == task_id,
    models.Task.owner_id == current_user.id,  # ← ownership check baked into query
).first()
if not task:
    raise HTTPException(status_code=404, detail="Task not found")
# Note: returns 404, not 403 — intentional: don't reveal that the task exists
SECURITY PATTERN: Returning 404 instead of 403 for unauthorized access prevents enumeration attacks — a user can't discover another user's task IDs by getting 403 responses. Gemini observed this pattern in tasks.py and replicated it in recurrence.py. This is a security decision made once, then propagated consistently through AI assistance.

Commit Message Generation

Commit messages are documentation. A good commit message answers "why was this change made?" not "what files changed?" (git already knows what changed). Gemini CLI can generate conventional commitsA commit message format: type(scope): description. Types: feat (new feature), fix (bug fix), docs (documentation), refactor, test, chore. Enables automated changelog generation and semantic versioning. Example: feat(recurrence): add POST endpoint to set recurring task schedule. by reading the staged diff — so you get messages grounded in what actually changed.

PowerShell / Terminal
# Stage the new files
git add routers/recurrence.py models.py schemas.py main.py

# Generate the commit message from the actual staged diff
$diff = git diff --staged --stat
gemini "Write a conventional commit message for these staged changes:
$diff

The change adds recurring task support to TaskFlow.
Format: type(scope): short summary (max 72 chars)
Followed by a blank line and a 2-3 sentence body explaining WHY,
not just WHAT. Use 'feat' type."
WHAT: git diff --staged --stat produces a summary of all staged files and line counts. We embed it in the prompt as variable $diff. Gemini sees the actual files changed and generates a message that references them.
WHY NOT JUST WRITE IT: Commit messages written after the fact tend to be generic ("update files", "add feature"). A message generated from the diff tends to be specific. More importantly, it sets a high standard for the entire team — every commit gets a meaningful message without extra effort.

Generated vs. Typical Commit Messages

Gemini Output — Commit Message
feat(recurrence): add recurring task schedule endpoints

Adds POST/GET/DELETE /tasks/{id}/recurrence endpoints to TaskFlow via
a new routers/recurrence.py module. Introduces the RecurringTask SQLAlchemy
model (one-to-one with Task) and corresponding Pydantic v2 schemas.

The RecurringTask model stores schedule_type (daily/weekly/monthly),
interval_value, and next_run_at so the background scheduler can poll
for due tasks without modifying existing task CRUD logic.

Co-authored-by: Gemini CLI
Tip — Use it Directly

Pipe the output to git commit: gemini "..." | git commit -F - on Linux/Mac. On Windows PowerShell: capture to a temp file, then git commit -F tempfile.txt. Either way, the commit message comes from the diff, not a rushed afterthought.

Branch-Per-Feature Workflow

The right place to use Gemini CLI is on a feature branchA git branch created specifically for one feature or change. Work is isolated from main until ready. The branch is merged via a pull request after review, keeping main always in a working state.. This protects the main branch during experimental generation and makes it easy to review what Gemini changed before merging.

Branch-Per-Feature Workflow With Gemini CLI
git checkout -b feat/recurring-tasks
Isolate work from main
gemini "@spec/... @models.py Implement..."
Generate with full context
gemini "@routers/recurrence.py Review for issues before I open a PR"
Pre-PR review with Gemini
git add routers/recurrence.py models.py schemas.py main.py
Stage specific files
git commit -m "feat(recurrence): add recurring task endpoints"
Conventional commit
git push origin feat/recurring-tasks && gh pr create
Open PR for human review

Pre-PR Review With Gemini

Before opening a pull request, ask Gemini to review the new file for issues — while it can still see the codebase context:

PowerShell / Terminal
gemini "@routers/recurrence.py @routers/tasks.py @models.py
Review routers/recurrence.py before I open a PR.
Check for:
1. Auth: is every endpoint protected with get_current_user?
2. Ownership: does every endpoint verify task.owner_id == current_user.id?
3. Error handling: are all 404 cases covered?
4. Consistency: does it follow the same patterns as tasks.py?
5. Edge cases: what happens if task_id doesn't exist vs. recurrence doesn't exist?
List specific issues with line numbers if you find any."
WHY THIS STEP: You're asking Gemini to review code it helped generate — but with fresh context from the existing codebase. This catches cases where Gemini was inconsistent in the generation step. Think of it as a second pass: generate, then review, then commit.

Iterative Refinement

Gemini's first implementation isn't always perfect — and that's expected. The key skill is the feedback loop: run the code, get an error, paste the error back, get a fix. This loop is usually 1-2 iterations for most generation tasks.

Iterative Refinement Cycle — Generate, Test, Fix
Generate
Gemini generates routers/recurrence.py from spec + context
gemini "@routers/ @models.py @schemas.py Create recurrence.py..."
First Test Run Fails
ImportError: cannot import name 'RecurringTask' from 'schemas'
pytest tests/test_recurrence.py -v
🔧
Paste Error to Gemini
Feed the exact error output and ask for the specific fix
gemini "@schemas.py This error: ImportError: cannot import name 'RecurringTask'..."
Fix Applied — Tests Pass
Gemini adds missing RecurringTask class to schemas.py
pytest tests/test_recurrence.py -v # ✓ 3 passed

Error-Driven Refinement

When a test fails or the server throws an error, paste the entire error output — traceback, error message, and context — to Gemini with the affected file loaded:

PowerShell / Terminal — Paste the actual error
# Run tests and capture output
pytest tests/ -v 2>&1 | Tee-Object test-output.txt

# Feed the error to Gemini with context
$err = Get-Content test-output.txt | Select-Object -Last 30
gemini "@routers/recurrence.py @schemas.py @models.py
This test failure occurred after implementing recurrence.py:

$err

Identify the root cause and show the minimal code change to fix it.
Reference specific line numbers in the affected file."
WHAT: Select-Object -Last 30 gets the last 30 lines of test output — usually where the failure traceback is. Tee-Object saves to file AND shows in terminal simultaneously.
WHY "MINIMAL CHANGE": Without this instruction, Gemini sometimes rewrites the entire function when one line needs to change. "Minimal code change" focuses the response on the actual fix.
GOTCHA: Always include the failing file (@routers/recurrence.py) in the prompt. Gemini needs to see the current state of the code, not what it generated initially — you may have already made manual edits.

Common First-Pass Fixes

Here are the most common issues Gemini CLI generates on the first pass, and the prompt pattern to fix each:

Common Fix 1 — Missing Schema Import
# Error: ImportError: cannot import name 'RecurringTaskCreate' from 'schemas'
gemini "@schemas.py @routers/recurrence.py
recurrence.py imports schemas.RecurringTaskCreate but it doesn't exist in schemas.py.
Add the RecurringTaskCreate and RecurringTask schemas following the
Tag/TagCreate/TagBase pattern (lines 7-16 of schemas.py).
Output only the new schema classes."
Common Fix 2 — Missing Router Registration
# Error: 404 on POST /tasks/1/recurrence (router not registered)
gemini "@main.py
I get 404 on the recurrence endpoints. The router is in routers/recurrence.py
but the /docs page doesn't show the endpoints.
Show the 2 lines to add to main.py to register it — follow the tasks/tags pattern."
Common Fix 3 — next_run_at Timezone Mismatch
# Error: can't compare offset-naive and offset-aware datetimes
gemini "@routers/recurrence.py
Line X calculates next_run_at using datetime.now() which returns
a naive datetime. The next_run_at column is DateTime(timezone=True).
Fix the line to use datetime.now(tz=timezone.utc) instead."
WHY THESE HAPPEN: These are the three most common first-pass issues in FastAPI + SQLAlchemy projects: forgotten schema, forgotten router registration, and naive vs. aware datetime. All three are fixable in under 5 minutes with targeted Gemini prompts. The key is specificity: name the exact error, load the exact file, ask for the minimal fix.
What Just Happened?

You've gone from a JSON spec to a working, pattern-consistent implementation in one module. The workflow was: scaffold from spec → generate router from multi-file context → wire into main.py → generate commit message → review before PR → fix test failures iteratively. Each step used Gemini CLI with real TaskFlow files as context. The output is code that a senior developer would recognize as belonging to the same codebase — not generic AI output bolted on.

Lab: Implement the Recurring Tasks Feature

LAB End-to-End Implementation: Spec → Working Endpoints

Prerequisites: complete M05 lab (spec/recurring-tasks.json exists) and M06 lab (docs/recurring-task-model.py exists). By the end, the three recurrence endpoints will respond on the running server.

1
Create the feature branch
PowerShell
cd sample-project\taskflow
git checkout -b feat/recurring-tasks
git status  # Confirm you're on the new branch
2
Add the RecurringTask model and schemas
PowerShell / Terminal
gemini "@models.py @spec/recurring-tasks.json
Add the RecurringTask class to models.py. Include the 2-line modification
to the Task class. Match all existing patterns. Python only." >> models.py

gemini "@schemas.py @spec/recurring-tasks.json
Add RecurringTaskBase, RecurringTaskCreate, and RecurringTask schemas
following the TagBase/TagCreate/Tag pattern. Python only." >> schemas.py
3
Generate the recurrence router
PowerShell / Terminal
gemini "@routers/ @models.py @schemas.py @spec/recurring-tasks.json
Create routers/recurrence.py with POST/GET/DELETE /tasks/{task_id}/recurrence.
Match all patterns from existing routers. Python only." > routers/recurrence.py
# Review what was generated
cat routers/recurrence.py | head -40
4
Wire the router into main.py
PowerShell / Terminal
gemini "@main.py Show the 2 lines to add to register routers/recurrence.py.
Follow the tasks/tags pattern."
# Manually add the 2 lines to main.py based on Gemini's output
5
Start the server and verify
PowerShell
uvicorn main:app --reload
# In a new terminal, check the docs page shows the new endpoints:
# Open http://localhost:8000/docs
# You should see: POST /tasks/{task_id}/recurrence
#                 GET  /tasks/{task_id}/recurrence
#                 DELETE /tasks/{task_id}/recurrence
# If you see a 500 on startup, check models.py for duplicate imports
6
Fix any errors, then commit
PowerShell / Terminal
# If there are errors:
# $err = uvicorn main:app 2>&1 | Select-Object -Last 20
# gemini "@models.py @schemas.py This error: $err Fix the root cause."

# Once the server starts cleanly:
git add routers/recurrence.py models.py schemas.py main.py
$diff = git diff --staged --stat
gemini "Write a conventional commit message for: $diff
The change adds recurring task support. Use feat(recurrence): prefix."
# Copy the message and commit:
# git commit -m "feat(recurrence): add POST/GET/DELETE recurrence endpoints"
Write-Host "Done! Three new endpoints live at /tasks/{id}/recurrence"

Expected: server starts with no errors, /docs shows three new recurrence endpoints, and you have a clean commit on feat/recurring-tasks.

Quiz

Check Your Understanding 0 / 5
1. You run gemini "@routers/ @models.py Create recurrence.py". What does @routers/ load?
A. Only routers/__init__.py if it exists
B. All Python files inside the routers/ directory (tasks.py, users.py, tags.py)
C. The directory listing only, not the file contents
D. @routers/ is invalid syntax — only individual files work with @
2. The generated recurrence.py returns 404 when a task exists but belongs to a different user. Why is 404 (not 403 Forbidden) the right choice here?
A. FastAPI defaults to 404 for all authorization failures
B. 404 prevents enumeration attacks — the caller can't tell whether the task_id exists for another user versus doesn't exist at all
C. 403 is only for admin-level access control, not user data ownership
D. The existing tasks.py uses 404 so Gemini copied the pattern without understanding it
3. After generating recurrence.py, you run pytest and get: ImportError: cannot import name 'RecurringTask' from 'schemas'. What is the most efficient next step?
A. Regenerate recurrence.py from scratch with a better prompt
B. Run gemini "@schemas.py This error: ImportError... Add the missing RecurringTask schemas following the Tag pattern."
C. Manually write the RecurringTask schema from scratch
D. Check if Pydantic v2 is installed correctly
4. Why is model_config = {"from_attributes": True} important in the RecurringTask response schema?
A. It enables Pydantic to serialize JSON responses
B. It allows Pydantic v2 to read data from SQLAlchemy model attributes (ORM mode), enabling direct conversion of DB objects to response schemas
C. It tells SQLAlchemy to use the schema for database validation
D. It's a FastAPI requirement for response_model schemas
5. You use >> models.py to append Gemini's output. What is the most important thing to check before running the server?
A. That the file encoding is UTF-8
B. That the appended code doesn't duplicate existing imports or add a conflicting class name — review the diff before running
C. That Alembic migrations are up to date
D. That the append target is in the project root