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
RecurringTaskmodel directly fromspec/recurring-tasks.json - Generate routers/recurrence.py by loading the entire
routers/folder for style matching - Understand why Gemini matches
db.query(...).filter(...).first()andmodel_configpatterns - 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.
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
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
>> 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
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
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.
Generate the Recurrence Router
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
@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
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()
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
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."
# 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)
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
# 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
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.# 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
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.# 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
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.
# 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."
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
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
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.
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:
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."
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.
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:
# 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."
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:
# 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."
# 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."
# 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."
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
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.
cd sample-project\taskflow
git checkout -b feat/recurring-tasks
git status # Confirm you're on the new branch
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
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
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
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
# 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
gemini "@routers/ @models.py Create recurrence.py". What does @routers/ load?routers/__init__.py if it existsrouters/ directory (tasks.py, users.py, tags.py)@routers/ is invalid syntax — only individual files work with @pytest and get: ImportError: cannot import name 'RecurringTask' from 'schemas'. What is the most efficient next step?gemini "@schemas.py This error: ImportError... Add the missing RecurringTask schemas following the Tag pattern."model_config = {"from_attributes": True} important in the RecurringTask response schema?response_model schemas>> models.py to append Gemini's output. What is the most important thing to check before running the server?