Module 4 · the centerpiece

Brownfield Lab — Add “Due Date” to the To-Do App

Adopt SPECTRA on an existing codebase the way Spec Kit’s existing-projects guidance recommends: start from a reviewable baseline, write down only the rules that are already true, specify one bounded change, and decide how specs will age. Along the way you’ll separate convention from cruft — and never spec the whole system.

Time 2–3 hours
Format Hands-on lab
Prerequisites Modules 1–3; SPECTRA CLI, Git, Node.js 18.18+
★
Lab codebase — lab-codebase.zip
A small, fully working Next.js 15 + TypeScript + Tailwind to-do app with no specs, no tests, no Git history, and no due dates. About 200 lines across five files.
Download

Learning objectives

By the end of this module, you will be able to:

  1. Adopt Spec Kit and SPECTRA in an existing repository from a reviewable baseline, and confirm from the diff exactly what initialization added.
  2. Write a constitution from rules that are already true in the code, and name cruft separately from conventions.
  3. Specify one bounded change — its outcome and its compatibility limits — without specifying the whole system.
  4. Run the full loop on existing code (clarify, plan against the repository, tasks, analyze, implement, converge) and review code and artifacts together.
  5. Decide, during validation, whether to fix the code or fix the spec.
  6. Choose how specs will age in this project and record the choice in the constitution.

Why this lab is the centerpiece

Greenfield SDD is the easy case. Tutorials and demo videos almost always show greenfield. Most real engineering work is brownfield — you have a codebase, it has conventions (some good, some weird), and you need to add a feature without breaking the rest of it.

The brownfield case is where most teams either give up on SDD because the tools “don't fit,” or generate giant codebase-summary specs that nobody reviews. There's a third path, and it's the one Spec Kit's own guidance for existing projects recommends: initialize in place, write down the rules that genuinely hold, and point the workflow at the next bounded change.

If you only do one lab in this course, do this one. Each agent step ends at a human gate from SPECTRA's agentic SDLC — agents draft, people decide — and in this lab every one of those people is you. Commands are shown in the Claude Code / GitHub Copilot trigger form (/speckit-specify); for other agents, see the command-spelling note in Module 1.

1.Run the starter app (15 min)

1.1 Get the lab codebase

Download lab-codebase.zip using the link above. Unzip somewhere you can work in:

unzip lab-codebase.zip -d ~/sdd-brownfield-lab
cd ~/sdd-brownfield-lab/lab-codebase

1.2 Install dependencies and run

You need Node.js 18.18 or newer.

npm install
npm run dev

Open http://localhost:3000. You should see “My To-Dos” — an input box, an “Add” button, and (after you add some) a list of tasks. Try it out:

  1. Add a few tasks.
  2. Mark some done. Notice the strikethrough.
  3. Refresh the page. Tasks should persist.
  4. Delete a task.

Leave a few tasks in the list: saved before due dates existed, they're exactly the data your compatibility requirement must protect. Stop the dev server with Ctrl+C.

1.3 Read the code

Spend 10 minutes reading the source. Don't skim. The whole codebase is about 200 lines across:

While you read, keep a small note open and write down what you see. Specifically:

This list is the heart of the brownfield work. Your constitution will encode the conventions. The cruft you'll either tolerate (out of scope), fix during the work (in scope), or note for later (a follow-up spec). But you have to decide, which means you have to name them first.

A hint, not a spoiler. The codebase is small but it does have at least three pieces of intentional cruft. If you find none, you're skimming. If you “find” ten, you may be classifying conventions as cruft. The right number is small.

2.Make a reviewable baseline (5 min)

Before any tool touches this repository, commit what's there. Everything that follows — initialization, the constitution, each spec, every line of generated code — then shows up as an ordinary diff you can read and revert.

The zip ships with no .git and no .gitignore, and npm install just created node_modules/ and .next/. Ignore those first, or your baseline will contain thousands of dependency files:

printf 'node_modules/\n.next/\nnext-env.d.ts\n*.tsbuildinfo\n' > .gitignore
git init && git add -A && git commit -m "baseline"

git status should now report a clean working tree.

On a real repository.Commit or stash work in flight and create a branch for the adoption, so it goes through normal review. In this solo lab, the baseline commit is enough.

3.Install SPECTRA and review the diff (10 min)

3.1 Initialize in place

spectra install

Same command as Module 3. It notices this folder isn't a Spec Kit project yet and offers to initialize it; accept. What it runs is exactly specify init --here --force, the command Spec Kit's guide prescribes for an existing repository. --here targets the current folder; --force allows a non-empty folder and may replace files at the paths Spec Kit manages. It won't delete your application, but that permission to overwrite is why you made the baseline first.

When prompted for an AI agent, pick the one you used in Module 3. SPECTRA then registers its catalog and installs its extension. Restart the agent afterwards. Everything in this lab except /speckit-spectra-domain-analyzer and the pull-request offer after implement is plain Spec Kit.

3.2 Review the diff

git status

Read what changed before going further. You should see only new, untracked folders:

No modified files: app/, lib/, and package.json are untouched. And notice what's missing: no specs/ folder. Initialization infers no specs for the behaviour your app already has. Commit it:

git add -A && git commit -m "Add Spec Kit and SPECTRA"

Keep that habit: commit after each step you approve, so the next git diff shows exactly what the next agent run changed.

3.3 The new spec describes the change, not the system

Don't fill that gap with a spec of the whole to-do app. The existing code stays implementation context that every later command reads; your first spec describes only the change you're about to make. Documenting the entire system is the right first feature only when that inventory is itself the deliverable.

Your first job after init needn't be a feature at all: a bug fix (Module 6) or an idea assessment (Module 5) can start straight away. This lab takes the feature path.

4.Write the constitution from what's already true (20 min)

In a greenfield project, the constitution comes before the code. Here the code came first, so you extract the constitution from what you observe — the most distinctive brownfield activity, and the one most likely to go wrong if rushed.

4.1 Only rules that are true today, or explicitly agreed

Every rule has to pass one test: can you point at the code, README, config, or team decision that makes it true? Here your evidence is the source, package.json, tsconfig.json, the Tailwind config, and the README.

Don't invent standards to fill the template. The constitution governs everything downstream, and /speckit-analyze treats any conflict with it as critical. Write “every feature MUST have unit tests” into a repo with no test runner, and every plan either fails analysis or quietly grows a test framework nobody agreed to. That's noise, not a guardrail. If the team does agree to start testing, record that as the decision it is.

4.2 Let the agent gather evidence, not decide

Don't just ask your agent to “generate a constitution from this codebase.” It will pick up cruft as convention, because it can't tell deliberate from accidental, and it will over-include “just in case” — the markdown-monster failure mode from Module 2. Ask for evidence first, in plain chat (this isn't a Spec Kit command, and it shouldn't write anything):

Read app/page.tsx, app/components/*, lib/storage.ts, package.json and
README.md. List the patterns that repeat and look deliberate: file layout,
component conventions, styling, data shapes, dependencies. Cite the file
that shows each one. List separately anything that looks accidental.
Don't create or edit any files.

Compare its lists with your notes from step 1. Where you're unsure, ask:

You don't have to be right. The point is to decide, then revise as you learn more.

4.3 Run /speckit-constitution

Now hand over only the rules you accepted:

/speckit-constitution Record only rules that are already true in this
codebase: <the conventions you accepted>. Add a "Known cruft" section
listing <the cruft you named> as defects to fix when convenient, not
conventions to follow. Keep it short.

The agent fills in .specify/memory/constitution.md in Spec Kit's template layout (named principles, governance, a version line), so compare content with the sample below, not layout.

Human gate — Foundation.Architects and engineering leads approve every standard; here that's you. For each rule, can you point at the file that makes it true? Cut anything you can't, then commit.
Optional second opinion./speckit-spectra-domain-analyzer proposes candidate guardrails from the code, each citing its evidence, in .specify/memory/domain-analysis.md. All start unchecked; you tick the ones you accept and pass the file to /speckit-constitution. It never edits the constitution itself.

4.4 Compare with a sample

Write your own version first, before peeking. Then compare.

# Todo App Constitution

## Stack
- Next.js 15 (App Router) with TypeScript in strict mode
- React 18
- Tailwind CSS for styling
- localStorage for persistence — no backend, no API routes

## Code organisation
- Page lives in `app/page.tsx`; it owns state and passes handlers down
- Reusable components live in `app/components/`
- Types and storage helpers live in `lib/storage.ts`
- Components using browser APIs declare `"use client"` at the top

## Styling conventions
- Tailwind utility classes only — no separate CSS files for layout
- `app/globals.css` is just the three Tailwind directives
- Default Tailwind palette; no custom theme extension yet

## Data conventions
- Todos shape: { id: string, text: string, completed: boolean, createdAt: string }
- `createdAt` is an ISO 8601 string
- Newest tasks render at the top of the list
- Completed tasks remain in the list with strikethrough; they are not removed
- Empty task text is rejected silently (no error message); no toasts

## State conventions
- Page owns state; components are presentational and receive callbacks as props
- localStorage is read once on mount inside a `useEffect`; written on every change
  inside a separate `useEffect`. This is required to avoid hydration mismatch.

## Testing
- There is no automated test suite. Features are verified by hand against
  the acceptance scenarios in their spec.
- Adding a test framework is a separate, explicit decision, never a side
  effect of a feature.

## Known cruft (NOT conventions — fix when convenient)
- Magic string "todos" repeated in `lib/storage.ts` — extract a constant
- Leftover `console.log` in the page's mount effect — remove
- IDs generated with `Date.now().toString()` — switch to `crypto.randomUUID()`
  (collision-safe; available in modern browsers)

Notice the last section: the constitution names the cruft as cruft. That stops the agent preserving it as if it were intentional — without it, an agent that sees Date.now() IDs would happily use them in the due-date feature — and it gives you a known list of follow-up work (the bonus exercise takes one item).

Notice, too, what's not there: no coverage target, no accessibility standard, no performance budget. Good ideas, perhaps, but none is true of this code or agreed by anyone. The Testing section records the honest state instead.

5./speckit-specify — one bounded change (20 min)

You are not specifying the whole to-do app. You are specifying the due-date addition: an outcome, plus the compatibility limits that must hold while you deliver it. The existing app is context, not subject.

5.1 Run /speckit-specify

/speckit-specify Add a due date to to-do items. When creating a task,
the user must pick a due date, and it must be in the future: tomorrow
or later, never today. Each task in the list shows its due date. When a
task's due date has passed and the task isn't completed, its due date
shows in red. Compatibility limits: adding, completing, deleting and
persisting tasks keep working exactly as they do today. To-dos saved
before this change have no due date; they must still load and display,
and are never shown as overdue. Nothing else about how to-dos are
stored changes.

The agent creates a feature directory such as specs/001-due-date/ with spec.md and a built-in quality checklist, checklists/requirements.md. Specify and clarify tick that checklist's items themselves, so read those ticks as the agent's claim, not your sign-off. The most common failure mode is over-scoping: sorting by due date, date-range filters, recurring to-dos, reminders, “due soon” colours. Push back hard on each one.

5.2 Review the draft

Apply the six-element checklist from Module 2. Spec Kit's template uses its own headings (user scenarios, requirements, success criteria, assumptions), so look for each element's content rather than its name. In this spec, check:

Outcomes — must be observable. Not “users can set due dates.” Closer to: “When creating a task, the user must pick a due date in the future. The due date is visible on each task in the list. Tasks whose due date has passed and that are not yet completed display the due date in red.”

In-scope — minimal, surgical. This feature adds a date field, validation that it's in the future, display of the date, and a red treatment when overdue. That's it.

Compatibility limits — the brownfield element. A brownfield spec also says what must not change. Check that each limit survived as its own numbered requirement, not just a sentence in the overview; otherwise nothing downstream can trace to it, and analysis can't flag a missing task.

Out-of-scope — write at least 5. This is the section that prevents an afternoon's feature from becoming a week. Suggested entries:

Constraints — reuse the constitution, don't repeat it. “Next.js + Tailwind + localStorage” is in the constitution; the spec doesn't restate it. Spec-level constraints are behaviour specific to this feature, such as “the date is picked with the browser's own date picker.” The stored format belongs in the plan.

Decisions already made. This is where you cut off rabbit holes. Examples:

Verification criteria. At least 6, with at least 2 edge cases. In Spec Kit's template they're the acceptance scenarios under each user story.

  • Submitting a task with a due date of tomorrow succeeds and the task appears in the list with that date.
  • Submitting a task with today's date is rejected with an inline error; no task is added.
  • Submitting a task with a date in the past is rejected with an inline error; no task is added.
  • Submitting a task with no date set is rejected with an inline error; no task is added.
  • A task with a due date in the past and completed: false displays the due date in red.
  • A task with a due date in the past and completed: true does not display in red.
  • Edge: a task created today with tomorrow's due date displays in normal colour today, and would display in red two days from now (verify by adjusting system clock or using a far-past test date).
  • Edge: refreshing the browser preserves all due dates; existing tasks (created before this change, with no dueDate field) load and display without errors and are never red.

The pattern: each line is a single, runnable check that passes or fails in the browser with no interpretation needed. None says “works correctly.”

5.3 The spec smell to watch for

If your spec mentions component file names, function signatures, Tailwind class strings, or the storage format, you've drifted into the plan. Pull those out. The Module 2 worked example shows the difference if you need a refresher.

Human gate — Plan.The product owner approves intent, scope, and business alignment. You're playing that role: fix anything you wouldn't sign off as the person who asked for the feature.

6./speckit-clarify — settle what the spec leaves open (10 min)

/speckit-clarify

Clarify looks for decisions the spec leaves open and asks about them — one question at a time, at most five per pass, each with a recommended answer. Your answers are written into spec.md under ## Clarifications, and the affected requirements are updated. Plausible questions here (yours will differ):

Three rules for answering:

If an area still feels vague, run it again with a focus, e.g. /speckit-clarify Focus on how to-dos saved before this change behave. Then commit the spec.

Human gate — Plan.Still the product owner's gate. The spec you approve here is the contract everything downstream is checked against.

7./speckit-plan — against the repository (15 min)

/speckit-plan Build on the existing codebase. Reuse its architecture,
dependencies, and conventions as recorded in the constitution, and add no
new dependencies.

Now you make implementation decisions, anchored to the code that exists. Besides plan.md, the agent writes supporting files such as research.md and data-model.md.

7.1 What the plan should describe

7.2 Check that it reuses what exists

The brownfield question for every plan: does it build on this repository, or on a generic idea of a Next.js app?

7.3 Traps to watch for

Human gate — Design.Architects approve the design and its decisions. Move on only when you'd defend every choice in plan.md to someone who knows this codebase. Then commit.

8./speckit-tasks, then /speckit-analyze (10 min)

8.1 Break the plan into tasks

/speckit-tasks

The agent writes a dependency-ordered tasks.md in phases: Setup, Foundational, one phase per user story in priority order, then Polish. A reasonable shape for this feature:

Setup (probably empty — the project already exists) Foundational T001 Add optional dueDate to Todo (lib/storage.ts) US1 (P1) T002 Accept and store dueDate in addTodo (app/page.tsx) Add a task T003 Date input, local state, inline error (TodoForm.tsx) with a date T004 Validate "tomorrow or later" on submit US2 (P2) T005 Show the due date (TodoItem.tsx) See overdue T006 Red date when overdue and not completed; to-dos without a date are never red Polish T007 Walk the spec's acceptance scenarios in the browser

About 7–8 tasks for an afternoon's work. If your agent produces 4 mega-tasks or 25 micro-tasks, ask it to re-decompose. Also delete Setup tasks that scaffold a project that already exists (“initialize the project,” “configure linting”), test-framework tasks nobody asked for, and cruft fixes that crept in.

8.2 Check consistency before any code exists

/speckit-analyze

Analyze cross-checks spec.md, plan.md, and tasks.md against each other and the constitution, and reports findings by severity. It's read-only. On a brownfield change, look especially for a compatibility requirement with no task, a task with no requirement (often cruft cleanup or something you ruled out of scope), and plan choices that conflict with the constitution. Those conflicts are always critical — which is why an invented rule in step 4 would have turned into noise right here.

Fix each finding in the step that owns it: requirements with /speckit-specify or /speckit-clarify, design with /speckit-plan, the task list by re-running /speckit-tasks. Re-run /speckit-analyze until it comes back clean. (Spec Kit's full path also offers /speckit-checklist after plan, a reviewer-owned check of the spec's wording; this lab skips it to stay on time.)

Human gate — Implement.Engineers review every change. Analyze proposes; you decide which findings are real and where each gets fixed. Commit when it's clean.

9./speckit-implement — review code and artifacts together (30–40 min)

The longest phase. Watch for the same pitfalls as in Module 3, plus brownfield-specific ones.

9.1 Implement in stages

/speckit-implement Implement only the Setup, Foundational, and first
user-story phases, then stop and report progress.

First, implement counts checklist items; if any are unchecked it pauses to ask whether to continue. Read them before you answer — implement never changes a checkbox itself. When the run stops, restart the dev server and try:

Then run /speckit-implement again, without arguments, to finish. Once the display tasks are done, edit a task's dueDate in localStorage to a past date (devtools → Application → Local Storage) and refresh. The date should display in red; mark the task done and the red should disappear.

9.2 Brownfield-specific things to watch for

9.3 Review code and artifacts together

git status
git diff

Because you committed after each gate, this diff is exactly what implement did: source changes, tasks marked [X] in tasks.md, and any fixes you made in spec.md or plan.md. Review them as one change. Every code change should trace to a task and every task to a requirement — code with no task is scope creep, even when it's an improvement. Every [X] needs code behind it.

When implement finishes, SPECTRA offers to open a pull request (/speckit-spectra-create-pr). Decline: you haven't converged or validated, and this repo has no GitHub remote, which that command needs.

Human gate — Implement.Engineers review every change — code and artifacts in the same sitting, because either can be the thing that's wrong. Then commit.

9.4 If something doesn't work

Ask two questions, in order. Does the spec describe what you actually want? If the behaviour surprises you, the spec was often vague: update it, then re-implement just the affected task. Does the code implement the spec? If the spec is right, fix the code. Never fix code without checking the spec first — that's how spec-anchored repos drift back into code-as-truth repos.

10./speckit-converge — until it reports Converged (10 min)

/speckit-converge

Converge compares the code with the spec, plan, and tasks and summarizes what's missing by severity. It's append-only: it never edits or deletes code, and the only thing it can write is new tasks at the end of tasks.md. It ends one of two ways:

“Converged” means the code matches the artifacts. It can't tell you whether the artifacts match what you wanted — that's the next step, and it's yours.

Human gate — Implement.Appended tasks are proposals. Each should trace to a requirement; one asking for something the spec never said is a scope question for you, not work for the agent. Review each extra implement pass as you did in step 9.

11.Validate against the spec (10 min)

This is the part most teams skip. Don't.

11.1 Walk through the verification criteria

Go through each acceptance scenario in your spec and verify it holds in the browser. For edge cases, set up the precondition deliberately (edit localStorage, change the system clock if you can). The tasks you added in step 1 are genuine pre-change data — they should still be in the list, without a due date.

11.2 Spec-vs-implementation checklist

11.3 What if there's a gap?

If you find a behaviour your criteria don't cover, add the criterion to the spec, then verify. If the spec missed it, future you (or a teammate) would have missed it too.

If you find a bug, classify it. This is the habit sometimes called the SpecOps loop: treat every escaped bug as feedback on the harness — the spec and the checks around it — not only on the code. Module 9 goes deeper; the core is two categories:

The first kind means your validation is too loose; the second, that your spec-writing is missing something. Both are useful signals.

Human gate — Test.QE decides what a finding means — bug, spec gap, or acceptable. Here that call is yours.

12.Decide how specs will age (5 min)

The feature works. One question remains that the tooling deliberately leaves to you: what happens to specs/001-due-date/ when requirements change next month? It's a team convention, not a CLI setting — exactly what the constitution is for. Pick one of the persistence models from Module 1:

Then record your choice:

/speckit-constitution Add a spec lifecycle rule: <the model you chose, and
how this project applies it>.
Human gate — Foundation.It's a standard, so it's a lead's call. Review the amendment and commit it.
## Spec lifecycle
- Model: flow-forward. A feature directory under specs/ can be edited
  until its change merges; after that it is a record and is not edited.
- A later change to the same behaviour gets a new feature directory, and
  its spec names the directory it builds on or supersedes.
- specs/ describes changes, not the whole system. Behaviour that predates
  specs/001-due-date is documented by the code and the README.

Why flow-forward here. Every spec in this repo is a change on top of behaviour nobody specified, so a chain of small change records matches reality; the bonus exercise's cruft fix gets its own directory instead of editing this one. The cost is scattered context, which the linking rule pays down.

When you'd choose differently. Living spec, if you want one current contract per capability and will regenerate plans (keep key decisions somewhere durable, such as an ADR). Flow-back, for a small team that reliably reconciles artifacts at review. None is wrong; leaving the choice unrecorded is.

13.Reflection (10 min)

Same format as Module 3 — write REFLECTION.md answers honestly, don't share unless you want to.

  1. The constitution-from-existing-code activity — was it harder or easier than you expected? What did you almost include that wasn't actually true of the code?
  2. The cruft list — did you find the three pieces planted in the codebase? Did you find others that turned out to be conventions on closer inspection?
  3. How tempted were you to spec the entire app? How did you push back against that temptation?
  4. The agent's first-draft spec — what did it over-include? What did /speckit-clarify ask that you hadn't thought about?
  5. Did /speckit-analyze or /speckit-converge find anything your own review had missed? What does that say about where your attention went?
  6. The first time you had to choose between “fix the code” and “fix the spec” — what was the situation, and which did you pick? Why?
  7. What would have happened to this feature if you'd skipped the spec entirely and just told the agent “add a due date field”? What problems would you have caught at code review instead of spec review?
  8. What's the smallest change to your team's current workflow that would let you do this kind of brownfield SDD on a real ticket next week?

The last one is the most important. SDD is only worth learning if it changes what you do at work. Keep your answer: Module 9 closes with discussion prompts that ask the same question at team scale.

14.Bonus exercise (optional, 30 min)

If you finished early and want to push deeper:

Pick one piece of cruft from the codebase (the magic "todos" string, the leftover console.log, or the Date.now() ID generation) and write a small spec to fix it, following the lifecycle rule you just recorded. Cruft fixes are scope creep during feature work, but they become tractable when you spec them as their own focused changes.

Recommendation: start with Date.now() → crypto.randomUUID(). It has the most concrete benefit (collision safety) and forces you to think about whether existing IDs in localStorage need migration. (Spoiler: they don't, because IDs are opaque strings — only new IDs need the new generator. Say so in the spec's compatibility limits.)

Run it through the same loop, closing with the /speckit-converge check you met in Modules 1 and 3. This is how a brownfield codebase gains spec coverage without anyone writing a system-wide spec: each change adds a small one, frequently touched areas accumulate coverage, and over time the codebase starts behaving more like a greenfield project — not because you specced the whole thing, but because you specced each change.


What you should now know

What's next

In Module 5 you step back to the question that comes before any spec: should this be built at all? You'll run Spec Kit's assess extension on an idea for the same to-do app — five stages, from intake to a recorded verdict of go, needs-clarification, or kill — and see how a go hands off to /speckit-specify.