Day 04–06 — All 11 Pages, a Verification Pause, and Every Interactive Feature

Day 04–06 — All 11 Pages, a Verification Pause, and Every Interactive Feature

Date: 2026-05-23 – 2026-05-24
Status at end of day: Plans 2 and 3 complete. All 11 public pages built and navigable with real data, then made fully interactive — search, Cook Mode, ingredient checklist, newsletter, contact, comments, bonus gallery — each backed by a custom API route. 73 tests, 0 lint errors, 0 TS errors. Plan 3 merged to main.


This whole project is coding with AI — specifically Claude Code. But the point I keep coming back to is how I'm using it. I'm deliberately not going yolo: not firing off a vague prompt, accepting whatever comes back, and shipping it because the page rendered. The goal is to build something logical and maintainable — an app another developer (or future me) can actually reason about — with the AI as a collaborator, not an oracle. Plans on disk before code. Verification after every step. Audits that produce no commits but catch real gaps. That's the distinction I care about: not vibe coding, but vibe engineering. The three days below are a fair test of it — 30-plus subagent dispatches, a deliberate no-code verification session sandwiched in the middle, and the cleanup work that happens every single time I let a verification step slide.

TLDR

  • Built (Day 04): All 11 public pages wired to real data — Home, All Recipes, Single Recipe, My Story, Workshops, Contact, Bonus Recipes, Disclosures, plus every layout component. Site fully browsable for the first time against the 130 recipes and 16 categories from the migration. 58 tests passing.
  • Built (Day 06): All 11 interactive features — mobile menu, search overlay, Cook Mode, ingredient checklist, grid/list toggle, newsletter form, contact form, comments, bonus gallery — each backed by a custom API route. 73 tests, 0 lint errors.
  • Day 05 was deliberately code-free: a verification session to confirm the Plan 2 foundation before building interactivity on top of it. No commits — and it was the right call.
  • Flop: A subagent committed the MyStory global to main instead of the worktree branch and reported success. Caught by checking git log, recovered via cherry-pick.
  • Flop: Images returned 400 on first page load — the Media collection had no public read access. One-line fix that should have been in Plan 1.
  • Flop: SocialShareBar was in the plan's file map but no task ever built it. Caught in a post-session file-map audit.
  • Flop: Probed the category filter with the display name (cheesecakes) instead of the slug (cheesecake), suspected a bug, and chased a non-existent problem before realising the filter was correct all along.
  • Flop: Subagents skipped npm run lint, so four components had ESLint errors that only surfaced at the final Task 11 audit.
  • Flop: Plan 3 was never merged to main and npm run lint was scanning worktree files (222 spurious errors) — both caught in a follow-up audit.

Day 04 — Plan 2: All 11 Pages, Real Data, Working Images

Status: All 11 public pages built and navigable with real data. 49 tests. Images loading. The site is fully browsable for the first time.

What we built

Plan 2 in full: every public-facing page of the site, wired to the 130 recipes and 16 categories sitting in Postgres from the Plan 0 migration. Home, All Recipes, Single Recipe, My Story, Workshops, Contact, Bonus Recipes, Disclosures — plus Header, CategoryNav, Footer, MobileMenu, RecipeCard, Pagination, WorkshopCard, and the Payload data-fetching helpers that glue it all together.

Before we could start, CLAUDE.md had flagged two things Plan 1 missed: a bonus-recipes Payload collection (the Bonus Recipes gallery page needs somewhere to store the actual recipes) and a my-story global (the My Story page needed a CMS source for the bio content). Both prerequisites landed first, with tests.

Then: 18 Plan 2 tasks. 20 commits by end of day. 0 lint errors.

AI flops

Flop #1: Subagent committed to the wrong branch

The my-story global was the second prerequisite. I dispatched a subagent to implement it. The subagent reported success:

Commit: 00ad2ef — feat: add MyStory global for My Story page content

I checked git log in the worktree. It wasn't there.

git log --oneline -3
# dbe2471 fix: add category field and required slug to BonusRecipes
# 4ea79e9 feat: add BonusRecipes collection
# 5d0552c chore: update CLAUDE.md with pre-Plan 2 audit learnings

The commit had landed on main. The subagent lost working-directory context somewhere and committed to the main repo checkout rather than the worktree branch.

Fixing it:

# Cherry-pick into the worktree branch
git cherry-pick 00ad2ef

# Conflict — both branches had modified payload.config.ts
# (worktree added BonusRecipes, main added MyStory)
# Resolve: keep both
git add src/payload.config.ts && git cherry-pick --continue --no-edit

# Undo the commit from main
git -C /path/to/main-repo reset --hard HEAD~1

CLAUDE.md already has a warning about this: "Subagent commit verification — after any subagent reports a commit, verify with git log before moving on." It happened again anyway. The fix took maybe 10 minutes once I knew what had happened, but it's annoying because you have to know to check.

Flop #2: Plan code had a TypeScript error

The All Recipes page has a pagination component that needs to preserve category filter params in its Next/Previous links. The plan's template:

const extraParams = category ? { category } : {}

TypeScript rejected it. {} doesn't satisfy Record<string, string> — the inferred type is { category?: undefined }, which doesn't match. The fix is one explicit annotation:

const extraParams: Record<string, string> = category ? { category } : {}

The subagent reported DONE_WITH_CONCERNS and called it out explicitly:

"The plan's template used const extraParams = category ? { category } : {} which caused a TS2322 that required fixing."

That's the right behavior. The problem is that plans get written before collections and types fully exist, so there's always some drift between what the plan specifies and what strict TypeScript actually accepts. This is a pattern, not a one-off.

Flop #3: Images returned 400 — the media access control gap

When I loaded the home page for the first time, recipe cards rendered correctly: titles, category tags, dates, links. Images were broken — not showing placeholder grey boxes, but IMG_0918, IMG_4793... the raw alt text. Browser console:

Failed to load resource: 400 (Bad Request)
http://localhost:3000/_next/image?url=%2Fapi%2Fmedia%2Ffile%2FIMG_8040-800x533.jpg&w=1200&q=75

The /_next/image optimizer was returning 400. That means it tried to fetch the source image and got something it couldn't use. Testing the media endpoint directly:

curl -s -o /dev/null -w "%{http_code}" "http://localhost:3000/api/media/file/IMG_8040-800x533.jpg"
# 403
  1. Payload's default access control for collections is "authenticated users only". The Media collection had no explicit access config, so unauthenticated requests — including the Next.js image optimizer, which runs server-side — got a 403. Which the optimizer treats as "not a valid image source", which returns 400 to the browser.

Fix: one line in src/collections/Media.ts:

access: {
  read: () => true,
},

This should have been in Plan 1. It's a fundamental requirement for any public site — if you can't read media without logging in, nothing works. The symptom was subtle enough that it didn't surface until we had a real page rendering real images.

Flop #4: Worktree missing the media directory

After fixing access control, images returned 500 instead of 403. Server log:

ERROR: File IMG_8040-800x533.jpg for collection media is missing on the disk.
Expected path: /path/to/worktree/media/IMG_8040-800x533.jpg

The 130 images from the Plan 0 migration live in the main repo's media/ directory. The git worktree is an isolated directory — it doesn't inherit the main repo's untracked files. So from Payload's perspective, the files just don't exist.

Fix: symlink.

ln -sf /Users/.../yummy-stuff/media \
       /Users/.../yummy-stuff/.claude/worktrees/plan2-pages-and-navigation/media

Not a production problem — production uses Cloudflare R2. But it's a worktree development gotcha that's now in CLAUDE.md so we don't debug it again.

After the symlink: the home page loaded with all recipe images. 130 real photos, first load.

Flop #5: SocialShareBar — in the file map, missing from every task

After merging the Plan 2 branch and declaring it done, I ran a cross-check: every file in the plan's file map against what was actually on disk. One file was missing.

src/components/ui/SocialShareBar.tsx   ← listed in the plan, never built

The plan's file map listed it clearly: "Static share link row (no JS SDK)." The design had it too — in design/editorial.jsx, line 462, between the Method section and the Comments section. But none of the 18 tasks ever said "create SocialShareBar." The file map was aspirational; the tasks were what actually ran. Nobody noticed the gap during the session because nothing imported it, nothing broke without it, and the pages all loaded correctly.

Built it after the fact: 6 tests, Facebook/Twitter/Pinterest/LinkedIn URL-based links, wired into the Single Recipe page between Method and Comments. Commit cba7cf9.

While fixing the test I found a second problem: a CLAUDE.md gotcha I'd written earlier was wrong. After adding environmentMatchGlobs to vitest, I noted "no // @vitest-environment jsdom docblock needed." That turned out to be incorrect in practice — without the docblock, document is not defined. All existing component tests use the docblock. I just hadn't tested the claim. Reverted the gotcha.

Two things missed in the same session: one a spec gap (file in the map, no task), one a documentation error (gotcha I hadn't verified). Both caught in the post-session audit.

AI wins

The categories were real, immediately

CategoryNav is an async Server Component that calls getCategories() on every render. The moment the home page first loaded, all 16 categories from the migration appeared as horizontal scrolling pill buttons: waffles, toppings, sourdough, snacks, scones, pastry, pancakes, Extras, desserts, cookies, cheesecakes, cakes... Zero seed data. Zero mocking. The 130-recipe migration paid off the instant pages existed to surface it.

That's the thing about building against real data from the start. You don't find out in week 4 that your category nav doesn't handle 16 items — you find out in week 2, when you can still fix the layout.

The code review caught a missing field it found in the design files

The first version of BonusRecipes had no category field. The code quality reviewer subagent — dispatched to read the design files and check implementation quality independently — came back with:

"The design (design/editorial-extras.jsx) shows each bonus recipe tile displaying a category label (b.c) overlaid on the cover image... The collection has no category field, so when the Plan 2 bonus-recipes page is built against Payload data there will be nothing to render in those slots."

It had read design/editorial-extras.jsx, found the sample data structure { t: 'Sticky Cardamom Buns', c: 'Yeasted' }, traced that to the tile overlay, and connected it to the missing field. I wouldn't have caught that until Plan 3 when we try to render real data in the lightbox. Saved a future debugging session.

TDD actually caught the right things

The Pagination component has one tricky requirement: when a category filter is active, the Next/Previous links need to preserve the ?category=sourdough param alongside the ?page=2 they add. The test:

it('preserves existing query params', () => {
  render(
    <Pagination currentPage={1} totalPages={3} basePath="/recipes"
      extraParams={{ category: 'sourdough' }} />,
  )
  expect(screen.getByRole('link', { name: /next/i })).toHaveAttribute(
    'href',
    '/recipes?category=sourdough&page=2',
  )
})

The buildUrl implementation uses URLSearchParams with spread: { ...extra, page: String(page) }. The test confirmed the params merge correctly and in the right order. Implementation passed on first try — the test was specific enough to encode the exact behavior that matters.

Day 04 honest take

Plan 2 was a coordination problem more than a coding problem. Dispatching 20+ subagents across a single session, tracking which branch each commit landed on, reading design files and cross-referencing them against collection schemas — the actual TypeScript was the easy part.

The pattern that worked: dispatch → verify commit in the right branch → read the actual files, not just the report → proceed. Every shortcut on verification created cleanup work. The MyStory wrong-branch incident took 10 minutes to fix and would have been zero if I'd checked immediately after the agent reported done.

One thing that held up well: building against real data from day one. When the home page loaded for the first time and 16 real categories appeared in the nav row, that was the payoff for the migration work. You don't design a category pill row for 16 items the same way you design it for 3.


Day 05 — Pre-Plan 3 Audit: All Pages Verified, Three New Gotchas

Status: No new code written. Plan 2 work verified clean against the running app. Plan 3 ready to start.

This is the session that produces no commits — and is exactly the kind of step that "vibe engineering" means and "vibe coding" skips. Plan 2 merged yesterday; today was orienting for Plan 3 and making sure the foundation is solid before building interactive features on top of it.

Steps:

  1. Read the full Plan 3 spec — 11 tasks covering Cook Mode, search overlay, ingredient checklist, grid/list toggle, newsletter form, contact form, comments, bonus recipes gate, and all the backing API routes.
  2. Ran the verify skill: started the dev server, drove all 11 pages with Playwright, screenshotted each one, probed edge cases.
  3. Updated CLAUDE.md with three findings from the verification.

What the verification found

All 11 pages render correctly with real data. No regressions from the Plan 2 merge.

Specific checks:

  • Home, All Recipes, Single Recipe — real recipes, real images, correct layout
  • My Story — CMS placeholder ("add your story via the Payload admin") — correct, no content entered yet
  • Workshops — empty state ("No workshops scheduled yet") — correct, no workshops seeded
  • Contact — form fields, topic pills, Send Message button all present
  • Bonus Recipes — 8 locked tiles with padlock icons — correct (gate logic comes in Plan 3)
  • Disclosures — static affiliate + copyright text, renders clean
  • Mobile @ 390px — hamburger icon, search icon, single-column cards, horizontal pill scroll all working

Probe: bad recipe slug (/recipes/this-slug-does-not-exist) → Next.js default 404, no unhandled errors. notFound() fires correctly.

Flop: Category filter probe used the name, not the slug

I probed the category filter manually by typing /recipes?category=cheesecakes into the browser. Got "No recipes in this category yet." Looked like a bug in getPublishedRecipes.

Checked the DB:

{ name: "cheesecakes", slug: "cheesecake" }
{ name: "desserts", slug: "dessert" }

The migration created categories where the slug doesn't match the display name. The filter in payload.ts queries category.slug, which is correct. The CategoryNav pills also link using cat.slug, which is correct. Both things work; my probe was using the wrong value.

Retried with /recipes?category=cheesecake → 2 recipes, pill highlighted. Filter works.

The real issue this surfaced: the category page <title> uses the raw slug param, so the browser tab shows "cheesecake Recipes" instead of "Cheesecakes Recipes". The <h1> capitalises correctly; generateMetadata doesn't fetch the display name. Minor SEO gap. Noted in CLAUDE.md for Plan 4.

AI win: Playwright verification caught a real data mismatch before Plan 3 started

The category slug ≠ name discrepancy in the migrated data wasn't obvious from reading the code — the code is correct. It only surfaced by actually driving the app with real inputs. If that had sat undocumented until Plan 3 when someone builds a category page with a breadcrumb that reads "cheesecake", it would have looked like a CMS data quality problem. Now it's documented, root cause known, and the metadata gap is flagged for the right plan.

CLAUDE.md updates

Added three gotchas from today's session:

  1. tsx scripts must live inside the project — scripts in /tmp/ fail with Cannot find module 'payload'.
  2. Category slug ≠ category name — migrated categories have divergent slugs. Check actual slugs with payload.find({ collection: 'categories' }) before debugging empty filter results.
  3. Category page <title> uses raw sluggenerateMetadata in recipes/page.tsx builds the title from the URL param, not the display name. Known gap; fix in Plan 4.

Day 05 honest take

This kind of session — read the plan, verify the foundation, document what you found — doesn't produce commits. It's easy to skip in favor of just starting. The category slug/name finding is a good argument for not skipping it: a 20-minute verification surfaced a data quirk that would have cost more time mid-Plan 3 when you're debugging why a category breadcrumb reads wrong.

The verify skill's insistence on "runtime observation, not test runs" is the right call here. Running npm test would have told me the tests pass. It wouldn't have told me that manually typing a category name into a URL returns zero results, or that the page title reads differently from the heading.


Day 06 — Plan 3 Complete: All Interactive Features

Status: Plan 3 done. All 11 tasks complete. 73 tests, 0 lint errors, 0 TS errors.

What we did

Started Plan 3 — the interactive features pass. Plans 0, 1, and 2 built the foundation (infrastructure, content migration, all 11 pages). Plan 3 is where the site actually becomes usable: search, Cook Mode, ingredient checklist, newsletter signups, contact form, comments, bonus recipe gating. 11 tasks total.

The session opened with setup:

  1. Created a new git worktree at .claude/worktrees/plan3-interactive-features
  2. Copied .env and .env.local from the main repo, created the media symlink
  3. Ran the pre-plan audit gate: npm test && npm run lint && npx tsc --noEmit — all clean (58 tests, 0 errors)

Task 1 — Install nodemailer. Two commands, committed, done.

Task 2 — Interactive mobile menu. The MobileMenu.tsx from Plan 2 was a static shell. Replaced it with a 'use client' component that renders its own hamburger trigger (so the static duplicate came out of Header.tsx), manages isOpen with useState, locks body scroll when open, closes on Escape via useEffect, and has an inline newsletter form posting to /api/newsletter.

Tasks 3-4: Search and Cook Mode

Task 3 — Search overlay + /api/search. Two pieces: a backend route that queries Payload for matching recipes, and a frontend overlay with debounced live results. TDD first — search.test.ts with 3 cases (short query → early return, missing query → empty, valid query → calls payload.find), tests failed, created src/app/api/search/route.ts using Payload's find with a like filter on title + tags, tests passed. Then built SearchOverlay.tsx (300ms debounce, focus-on-open, Escape-to-close, backdrop click) and SearchButton.tsx, and replaced the static search button in Header.tsx.

One deviation from the plan spec: the route uses standard Request + new URL(request.url) instead of NextRequest.nextUrl. The test harness passes plain new Request(...) objects — NextRequest.nextUrl isn't available without the Next.js runtime. The right call; the route works identically in production.

Task 4 — Cook Mode. A full-screen step-by-step overlay for following a recipe without touching the phone: Screen Wake Lock API to keep the screen on, keyboard navigation (← →) for desktop, a sticky "Enter Cook Mode" CTA pinned to the bottom on mobile, an inline button on desktop, and graceful degradation if Wake Lock isn't supported. Replaced the static dead button in recipes/[slug]/page.tsx. 61 tests passing after both tasks.

Tasks 5-6: Ingredient checklist and grid/list toggle

Task 5 — Ingredient checklist. Replaced the static ingredient list with <IngredientChecklist>: useState + useEffect to restore checked state from localStorage on mount (key checklist-{slug}), each ingredient a <button> with aria-pressed (accessible, ≥44px, checkmark + strikethrough when checked), a "Clear all" button that appears only when something is checked, and all localStorage access wrapped in try/catch.

Task 6 — Grid/list toggle. TDD: GridListToggle.test.tsx first (3 tests), failed, built the component, passed. Then RecipesClient.tsx — a 'use client' wrapper that owns view state and renders the toggle plus the grid/list — and updated recipes/page.tsx to use it. One gotcha: the // @vitest-environment jsdom docblock was needed despite environmentMatchGlobs being configured. CLAUDE.md had two contradicting entries about this; in practice it's required. Resolved. 64 tests passing.

Tasks 7-8: Newsletter form and contact form

Task 7 — Newsletter form + /api/newsletter. TDD: newsletter.test.ts (missing email → 400, invalid email → 400, valid → calls Kit API), failed, created the route proxying to Kit API v4 (X-Kit-Api-Key auth), passed. Built NewsletterForm.tsx with a variant prop — sidebar (white) and inline (burnt-orange with white text). Added the inline variant to the single recipe page. 67 tests.

Task 8 — Contact form + /api/contact. TDD: contact.test.ts (missing fields → 400, complete → 200), nodemailer mocked, failed, created the route: builds a nodemailer transporter from SMTP env vars, sends an email on every submission, and if newsletter: true is included, fires a Kit signup as fire-and-forget (doesn't block or fail the contact response). Built ContactForm.tsx replacing the entirely static form — topic pills switched from radio buttons to <button type="button"> toggling a topic state variable. 69 tests.

Tasks 9-10: Comments and bonus gallery

Task 9 — Comments + /api/comments. TDD: comments.test.ts with 4 cases (GET no recipeId → empty, no Payload call; GET with recipeId → only approved comments for that recipe; POST missing fields → 400; POST complete → creates comment with approved: false, returns 201). Two Payload issues caught during implementation:

  1. Payload IDs are numbers, not strings. The plan spec used recipe.id as string and a string recipe: recipeId. Needed String(recipe.id) for the page prop and recipe: Number(recipeId) in the route so the relationship field gets a number.
  2. overrideAccess: true needed in payload.create() — public comment submission has to bypass access control.

Added CommentList.tsx (fetches approved comments on mount) and CommentForm.tsx (submits, shows a moderation notice on success), replacing the "Comments coming soon." placeholder. 73 tests.

Task 10 — Bonus gallery + /api/bonus-unlock. The route is simple: validate email, proxy to Kit, return success. The complexity is in BonusGallery.tsx, which has three localStorage-driven UI states: locked (padlock on each tile, click triggers the gate), gate modal (email input → /api/bonus-unlock → write bonus-recipes-unlocked=true on success), and unlocked (tiles without locks, click opens a lightbox with title + description). Placeholder data (8 tiles) for now; real content comes from the bonus-recipes collection later.

Task 11: Full integration verification

Ran the full audit gate in the worktree:

npm test        → 73 tests, 19 files, all passing
npm run lint    → 0 errors (had 4 errors to fix first — see below)
npx tsc --noEmit → 0 errors (3 pre-existing importMap noise, expected)

The lint errors were caught during verification, not during task implementation:

  1. CookMode.tsxhandleClose was declared after a useEffect that called it (react-hooks/immutability, "Cannot access before declaration"). Fix: moved it to a useCallback above the effect.
  2. BonusGallery.tsx, IngredientChecklist.tsx, SearchOverlay.tsxreact-hooks/set-state-in-effect fires when setState is called synchronously inside useEffect. All intentional (SSR-safe localStorage init, resetting search state on open). Fix: // eslint-disable-next-line on the line immediately before each setState call.

Lesson: eslint-disable-next-line disables only the very next source line. Placing it before useEffect(...) does nothing for setState calls inside the effect body. Classic mistake. All fixed in commit 91561bd.

AI flops

Lint errors introduced by Plan 3 components. Four components passed TypeScript but failed ESLint. The implementer subagents didn't run npm run lint as part of their verification — only npx tsc --noEmit. Caught in Task 11. The fix was straightforward; the real gap is the task template. Future task prompts should include npm run lint in the verification step.

Payload ID type mismatch. The plan spec passed relationship field IDs as strings. Payload expects numbers. The agent caught it during the TypeScript check (the right outcome), but the plan spec was wrong from the start.

AI wins

Executed 10 implementation tasks back-to-back with zero manual intervention. Each task had a fresh subagent context, so there was no drift or cross-contamination between tasks. The TDD tasks (search, grid/list, newsletter, contact, comments) all followed the test-first pattern cleanly.

The two-stage review (spec compliance + code quality) would have caught the lint errors if I'd enabled lint in the review step. Next plan I'll add npm run lint to the implementer verification template.

Day 06 honest take

Plan 3 is the most satisfying plan so far — it's the point where the site actually does things. Every task was self-contained enough for subagents to handle cleanly. The one discipline failure (not running lint in each task) created cleanup work at the end. The right lesson isn't "AI writes buggy code" — it's "the verification step was underspecified." Fix the template, not the trust level.

Post-session audit (same day, new context)

Came back to start Plan 4 and ran a progress check first — the right call, it turns out.

Plan 3 was never merged to main. All 11 tasks done, 73 tests passing in the worktree branch, but the branch was still sitting there unmerged. CLAUDE.md said "pending merge to main" at the top of the status section. The pre-plan-4 checklist literally says "Merge Plan 3 branch to main first." Neither happened. The subagents did their work correctly — every commit landed on the worktree branch as intended. The failure was mine for not running the merge after verifying Task 11.

npm run lint was scanning worktree files. The ESLint config had design/ in the ignores array but not .claude/. Since git worktrees live at .claude/worktrees/, every npm run lint from the main repo scanned all the design canvas JSX in the plan3 worktree — 222 errors unrelated to app code. The src/-scoped lint was clean (0 errors), but the top-level command would have blocked the Plan 4 audit gate. Nobody noticed because the gate was never run.

One leftover unused eslint-disable in SearchOverlay.tsx. The Task 11 fix added two suppress comments inside the effect; the linter only fires on the first call in a block, so the second was dead weight.

What we fixed:

  1. Merged the branch. git merge worktree-plan3-interactive-features --no-ff — clean, 29 files, 1457 insertions, all Plan 3 code now on main.
  2. Fixed the ESLint ignore list. Added .claude/ to ignores in eslint.config.mjs.
  3. Removed the dead disable comment in SearchOverlay.tsx.
  4. Ran the full pre-plan audit gate on main — all three checks passed (73 tests, 0 lint errors, 0 TS errors).
  5. Cleaned up the worktree. Branch removed, worktree pruned.

Why this keeps happening: the "merge branch" step is always the last item in the worktree workflow, and it's the one that keeps getting skipped. Subagents execute tasks → tests pass → blog written → session ends. The merge is a meta-step outside the task list, so it doesn't get a green checkbox, and it's easy to skip when the tests already feel like the win. Fix going forward: make the merge a numbered task in the plan itself, not a prerequisite note in a status section nobody reads.


Three-day take: vibe engineering, not vibe coding

Across these three days the AI wrote a lot of correct code — but almost every problem that mattered was caught by a verification step, not by the model second-guessing itself. The wrong-branch commit, the 400-ing images, the missing SocialShareBar, the skipped lint, the unmerged branch: none of them surfaced from a clever prompt. They surfaced from checking git log, driving the real app, diffing the file map, and running the audit gate. Day 05 produced zero commits and was one of the most valuable sessions of the three.

That's the whole distinction I care about. Going yolo with AI means trusting the report and shipping when the page renders. Vibe engineering means treating the AI as a fast, tireless implementer whose work you still verify like you would any teammate's — plans on disk so decisions survive context resets, real data from day one so layout problems show up early, and a verification habit that assumes the gap is in the process, not the model. The output is the same kind of logical, maintainable app you'd build by hand; the AI just gets you there faster, as long as you keep the discipline that makes it trustworthy.


This is part of an ongoing series documenting the rebuild of a friend's food blog from WordPress to a custom Next.js stack, built with AI assistance.

Kumudu
Written by

Kumudu

Distributed systems, infra, and small AI experiments on weekends. New posts roughly monthly, mostly long.

Comments

Adjacent reading

all essays →
press esc to close · enter to search